code wiki / (root) / nx_vae_tile.nx

nx_vae_tile.nx source

↩ module page · 388 lines · 14279 B

1// nx_vae_tile.nx -- overlapping tile split + alpha-feathered blend. 2// 3// Ships VRAM-track V-001 per docs/VRAM_OPTIMIZATION_REALISTIC_TRACKING.md: 4// VAE tiling with overlap blending. ~1-1.5 GB saved during decode 5// at ZERO quality cost when overlap >= 16 and blending is feathered. 6// 7// Substrate primitive -- model-agnostic. The caller orchestrates: 8// 9// tiles, meta = tile_split(latent, h_tile, w_tile, overlap) 10// for each tile_i: 11// out_tile_i = decode_2d_block(tile_i) -- caller's model 12// image = tile_blend_feathered(out_tiles, meta, overlap, H, W) 13// 14// The decoder runs on one tile at a time, so peak VRAM is roughly 15// (model weights + ONE tile of activations) rather than (weights + 16// full-image activations). For Z-Image-class latent 96x128 -> image 17// 768x1024 (8x upsample), per-tile decode of 64x64 latent -> 512x512 18// image drops decoder working memory by ~6x. 19// 20// Pure NishiLang i64 row-major. Single-channel buffer (multi-channel 21// = caller calls per channel; for fused CHW caller passes the 22// flattened buffer and strides). Q10 fixed point for blend weights. 23// 24// Quality envelope (honest measurement basis): 25// * overlap >= 16 px and alpha-feather -> sub-perceptual seams 26// * overlap = 8 px and alpha-feather -> visible boundary at 27// high-contrast edges 28// * unfeathered blending (constant 1) -> hard seams; refused. 29// We ship feathered only; unfeathered is documented as deliberate 30// non-shipping per the no-silent-corruption cardinal. 31// 32// Algorithm (alpha-feathered blend): 33// For each tile, weight at position (tr, tc) inside the tile is 34// w_y = min(tr, overlap, tile_h - 1 - tr) // distance from 35// w_x = min(tc, overlap, tile_w - 1 - tc) // tile edges 36// weight = (w_y + 1) * (w_x + 1) 37// so weight rises from 1 at the tile boundary to (overlap+1)^2 at 38// tile interior (overlap >= edge distance saturates). Output: 39// image[i, j] = sum_t (weight_t * tile_t[r, c]) / sum_t weight_t 40// where the sum is over all tiles t containing (i, j). 41// 42// genealogy_id: stable_diffusion_vae_tile_2022 + multidiffusion_2023 + 43// bar_tal_2024_overlapping_inference + alpha_compositing_porter_duff_1984 44// lineage_id: substrate_vae_tile_v1 45 46// nx_safety_envelope: 47// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 48// sil_target: SIL1 49// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 50// verdict: NOT_YET_EVALUATED 51 52import "nx_syscalls.nx" 53import "nx_tier.nx" 54import "nx_loop.nx" 55 56// ===== Sealed-enum: TileVerdict =================================== 57 58const NX_VT_OK: nx_int = 0 59const NX_VT_ERR_BAD_DIMS: nx_int = 1 60const NX_VT_ERR_BAD_OVERLAP: nx_int = 2 61const NX_VT_ERR_BAD_TILE_SIZE: nx_int = 3 62const NX_VT_ERR_BUFFER_TOO_SMALL: nx_int = 4 63const NX_VT_N_VERDICTS: nx_int = 5 64 65func nx_vt_verdict_is_valid(v: nx_int) -> nx_int { 66 if v < 0 { return 0 } 67 if v >= NX_VT_N_VERDICTS { return 0 } 68 return 1 69} 70 71// ===== Geometry helper: count tiles needed ======================== 72// 73// Returns the number of tiles required to cover H pixels with tiles 74// of height tile_h spaced stride apart. stride = tile_h - overlap. 75// The last tile may overlap more than `overlap` -- we shift it to 76// land flush against the boundary rather than letting the cover 77// short. This means the rightmost/bottommost overlap can exceed 78// `overlap`; the weighting handles it correctly. 79 80func _vt_n_tiles_axis(h: nx_int, tile_h: nx_int, overlap: nx_int) -> nx_int { 81 if h <= tile_h { return 1 } 82 let stride: nx_int = tile_h - overlap 83 if stride <= 0 { return 0 } 84 85 // Bounded loop per docs/LOOP_DESIGN_RESEARCH.md. Budget = h 86 // (a tile every pixel is the absolute upper bound; in practice 87 // we exit DONE_EXIT after stride*N strides clear the boundary). 88 var n: nx_int = 1 89 var start: nx_int = 0 90 var iter: nx_int = 0 91 var verdict: nx_int = NX_LOOP_RUNNING 92 let BUDGET: nx_int = h 93 while verdict == NX_LOOP_RUNNING && iter < BUDGET { 94 let next_start: nx_int = start + stride 95 if next_start + tile_h >= h { 96 // Final tile snaps to h - tile_h boundary if it differs. 97 if next_start <= h - tile_h { n = n + 1 } 98 verdict = NX_LOOP_DONE_EXIT 99 } 100 if verdict == NX_LOOP_RUNNING { 101 n = n + 1 102 start = next_start 103 } 104 iter = iter + 1 105 } 106 return n 107} 108 109func nx_vt_n_tiles(h: nx_int, w: nx_int, 110 tile_h: nx_int, tile_w: nx_int, 111 overlap: nx_int) -> nx_int { 112 let ny: nx_int = _vt_n_tiles_axis(h, tile_h, overlap) 113 let nx_t: nx_int = _vt_n_tiles_axis(w, tile_w, overlap) 114 return ny * nx_t 115} 116 117// ===== Tile-metadata record ======================================= 118// 119// One per tile -- the (top, left) corner of the tile in the source 120// image, plus the tile pointer. We expose this as a flat i64 array 121// so the smoke can self-verify without a struct walker. 122// meta[3*t + 0] = top 123// meta[3*t + 1] = left 124// meta[3*t + 2] = tile-index-on-y (debug) 125 126const NX_VT_META_STRIDE: nx_int = 3 127 128func nx_vt_tile_top(meta: *i64, t: nx_int) -> i64 { return meta[t * NX_VT_META_STRIDE] } 129func nx_vt_tile_left(meta: *i64, t: nx_int) -> i64 { return meta[t * NX_VT_META_STRIDE + 1] } 130 131// ===== Compute tile starts along one axis ========================= 132 133func _vt_fill_starts_axis(starts: *i64, h: nx_int, tile_h: nx_int, overlap: nx_int) -> nx_int { 134 if h <= tile_h { 135 starts[0] = 0 136 return 1 137 } 138 let stride: nx_int = tile_h - overlap 139 var n: nx_int = 0 140 var start: nx_int = 0 141 var iter: nx_int = 0 142 var verdict: nx_int = NX_LOOP_RUNNING 143 let BUDGET: nx_int = h 144 while verdict == NX_LOOP_RUNNING && iter < BUDGET { 145 starts[n] = start 146 n = n + 1 147 let next_start: nx_int = start + stride 148 if next_start + tile_h >= h { 149 // Snap the final tile to the boundary. 150 if start + tile_h < h { 151 starts[n] = h - tile_h 152 n = n + 1 153 } 154 verdict = NX_LOOP_DONE_EXIT 155 } 156 if verdict == NX_LOOP_RUNNING { 157 start = next_start 158 } 159 iter = iter + 1 160 } 161 return n 162} 163 164// ===== Tile split ================================================= 165// 166// Caller pre-allocates tiles[n_tiles * tile_h * tile_w] and 167// meta[n_tiles * 3]. n_tiles = nx_vt_n_tiles(...). 168 169func nx_vt_split(src: *i64, h: nx_int, w: nx_int, 170 tile_h: nx_int, tile_w: nx_int, overlap: nx_int, 171 tiles_out: *i64, meta_out: *i64) -> nx_int { 172 if h <= 0 { return NX_VT_ERR_BAD_DIMS } 173 if w <= 0 { return NX_VT_ERR_BAD_DIMS } 174 if tile_h <= 0 { return NX_VT_ERR_BAD_TILE_SIZE } 175 if tile_w <= 0 { return NX_VT_ERR_BAD_TILE_SIZE } 176 if overlap < 0 { return NX_VT_ERR_BAD_OVERLAP } 177 if overlap >= tile_h { return NX_VT_ERR_BAD_OVERLAP } 178 if overlap >= tile_w { return NX_VT_ERR_BAD_OVERLAP } 179 180 // Compute axis starts. 181 let max_starts: nx_int = h + w 182 let ys: *i64 = sys_mmap(max_starts * 8) as *i64 183 let xs: *i64 = sys_mmap(max_starts * 8) as *i64 184 let ny: nx_int = _vt_fill_starts_axis(ys, h, tile_h, overlap) 185 let nxs: nx_int = _vt_fill_starts_axis(xs, w, tile_w, overlap) 186 187 var t: nx_int = 0 188 var iy: nx_int = 0 189 while iy < ny { 190 let top: nx_int = ys[iy] 191 var ix: nx_int = 0 192 while ix < nxs { 193 let left: nx_int = xs[ix] 194 meta_out[t * NX_VT_META_STRIDE] = top 195 meta_out[t * NX_VT_META_STRIDE + 1] = left 196 meta_out[t * NX_VT_META_STRIDE + 2] = iy 197 198 // Copy the tile's HxW window from src into tiles_out. 199 let tile_base: nx_int = t * tile_h * tile_w 200 var r: nx_int = 0 201 while r < tile_h { 202 var c: nx_int = 0 203 while c < tile_w { 204 let src_idx: nx_int = (top + r) * w + (left + c) 205 let dst_idx: nx_int = tile_base + r * tile_w + c 206 tiles_out[dst_idx] = src[src_idx] 207 c = c + 1 208 } 209 r = r + 1 210 } 211 t = t + 1 212 ix = ix + 1 213 } 214 iy = iy + 1 215 } 216 return NX_VT_OK 217} 218 219// ===== Per-position blend weight (separable, integer) ============= 220// 221// w(r, tile_h, overlap) = min(r + 1, overlap + 1, tile_h - r) 222// then weight = w_y * w_x. Inside the tile far from the edge, weight 223// saturates at (overlap+1)^2. At the exact boundary (r=0 or r=tile_h-1) 224// weight = 1. Strictly positive, integer-valued, fast. 225 226func _vt_axis_weight(r: nx_int, tile_h: nx_int, overlap: nx_int) -> nx_int { 227 var w_left: nx_int = r + 1 228 var w_right: nx_int = tile_h - r 229 var w_cap: nx_int = overlap + 1 230 var w: nx_int = w_left 231 if w_right < w { w = w_right } 232 if w_cap < w { w = w_cap } 233 if w < 1 { w = 1 } 234 return w 235} 236 237// ===== Alpha-feathered blend ====================================== 238// 239// out_image is the full HxW destination buffer. weight_accum is a 240// scratch HxW i64 buffer (caller-allocated) that holds running 241// sum of weights at each pixel for normalisation. We do TWO passes: 242// pass 1: accumulate sum(w_t * tile_t[r,c]) into out, sum(w_t) into 243// weight_accum. 244// pass 2: divide out[i,j] by weight_accum[i,j]. 245// Both passes are deterministic; no rounding accumulates because we 246// keep raw integer sums until the final divide. 247 248func nx_vt_blend_feathered(tiles: *i64, n_tiles: nx_int, meta: *i64, 249 tile_h: nx_int, tile_w: nx_int, overlap: nx_int, 250 h: nx_int, w: nx_int, 251 out_image: *i64, weight_accum: *i64) -> nx_int { 252 if n_tiles <= 0 { return NX_VT_ERR_BAD_DIMS } 253 if tile_h <= 0 { return NX_VT_ERR_BAD_TILE_SIZE } 254 if tile_w <= 0 { return NX_VT_ERR_BAD_TILE_SIZE } 255 if overlap < 0 { return NX_VT_ERR_BAD_OVERLAP } 256 if overlap >= tile_h { return NX_VT_ERR_BAD_OVERLAP } 257 if overlap >= tile_w { return NX_VT_ERR_BAD_OVERLAP } 258 259 // Zero out the destinations. 260 let n_pix: nx_int = h * w 261 var z: nx_int = 0 262 while z < n_pix { 263 out_image[z] = 0 264 weight_accum[z] = 0 265 z = z + 1 266 } 267 268 // Accumulate. 269 var t: nx_int = 0 270 while t < n_tiles { 271 let top: nx_int = meta[t * NX_VT_META_STRIDE] 272 let left: nx_int = meta[t * NX_VT_META_STRIDE + 1] 273 let tile_base: nx_int = t * tile_h * tile_w 274 var r: nx_int = 0 275 while r < tile_h { 276 let w_y: nx_int = _vt_axis_weight(r, tile_h, overlap) 277 var c: nx_int = 0 278 while c < tile_w { 279 let w_x: nx_int = _vt_axis_weight(c, tile_w, overlap) 280 let weight: nx_int = w_y * w_x 281 let dst_idx: nx_int = (top + r) * w + (left + c) 282 out_image[dst_idx] = out_image[dst_idx] + weight * tiles[tile_base + r * tile_w + c] 283 weight_accum[dst_idx] = weight_accum[dst_idx] + weight 284 c = c + 1 285 } 286 r = r + 1 287 } 288 t = t + 1 289 } 290 291 // Normalize. 292 var p: nx_int = 0 293 while p < n_pix { 294 let wa: nx_int = weight_accum[p] 295 if wa > 0 { 296 // Round-half-up: (sum + wa/2) / wa 297 out_image[p] = (out_image[p] + wa / 2) / wa 298 } 299 p = p + 1 300 } 301 return NX_VT_OK 302} 303 304// ===== Self-test ================================================== 305// 306// Three closed-form invariants: 307// 308// (1) Tile counting math: a 96x128 image with tile_h=64, tile_w=64, 309// overlap=16 -> stride=48; ny=2 (covers 0..64, 32..96), 310// nx=3 (covers 0..64, 48..112, 64..128) -- but our boundary snap 311// gives ny=2, nx=3 with last x-start = 64. Total = 6 tiles. 312// 313// (2) Split + blend round-trip on a CONSTANT image (every pixel = K) 314// must recover the constant exactly. All weights cancel under 315// the normalisation; K * sum(w) / sum(w) = K. 316// 317// (3) Split + blend round-trip on a LINEAR gradient image 318// (pixel(r,c) = r * 100 + c) must recover within ±1 LSB of 319// round-half-up. Linear functions are exactly preserved by 320// any convex blend (all weights >= 0, sum > 0). 321 322func main() -> i64 { 323 // --- (1) Tile counting --- 324 let n: nx_int = nx_vt_n_tiles(96, 128, 64, 64, 16) 325 if n != 6 { return 10 } 326 327 // --- (2) Constant image round-trip --- 328 let H: nx_int = 96 329 let W: nx_int = 128 330 let TH: nx_int = 64 331 let TW: nx_int = 64 332 let OV: nx_int = 16 333 let K: nx_int = 42 334 335 let src: *i64 = sys_mmap(H * W * 8) as *i64 336 let tiles: *i64 = sys_mmap(n * TH * TW * 8) as *i64 337 let meta: *i64 = sys_mmap(n * NX_VT_META_STRIDE * 8) as *i64 338 let dst: *i64 = sys_mmap(H * W * 8) as *i64 339 let accum: *i64 = sys_mmap(H * W * 8) as *i64 340 341 var i: nx_int = 0 342 while i < H * W { src[i] = K; i = i + 1 } 343 344 let v1: nx_int = nx_vt_split(src, H, W, TH, TW, OV, tiles, meta) 345 if v1 != NX_VT_OK { return 20 + v1 } 346 let v2: nx_int = nx_vt_blend_feathered(tiles, n, meta, TH, TW, OV, H, W, dst, accum) 347 if v2 != NX_VT_OK { return 30 + v2 } 348 349 var j: nx_int = 0 350 while j < H * W { 351 if dst[j] != K { return 40 } 352 j = j + 1 353 } 354 355 // --- (3) Linear gradient round-trip --- 356 var k: nx_int = 0 357 while k < H { 358 var m: nx_int = 0 359 while m < W { 360 src[k * W + m] = k * 100 + m 361 m = m + 1 362 } 363 k = k + 1 364 } 365 nx_vt_split(src, H, W, TH, TW, OV, tiles, meta) 366 nx_vt_blend_feathered(tiles, n, meta, TH, TW, OV, H, W, dst, accum) 367 var ki: nx_int = 0 368 while ki < H { 369 var kj: nx_int = 0 370 while kj < W { 371 let want: nx_int = ki * 100 + kj 372 let drift: nx_int = dst[ki * W + kj] - want 373 if drift > 1 { return 50 } 374 if drift < -1 { return 51 } 375 kj = kj + 1 376 } 377 ki = ki + 1 378 } 379 380 // --- (4) Weight is strictly positive everywhere --- 381 var wi: nx_int = 0 382 while wi < H * W { 383 if accum[wi] <= 0 { return 60 } 384 wi = wi + 1 385 } 386 387 return 0 388}