Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
nn_model.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18#include "common/nn_model.h"
19
20// the only dependency this unit takes beyond libm/glib/json-glib: the
21// multi-versioning attribute, duplicated there so darktable.h stays out
23
24#include <glib/gstdio.h>
25#include <json-glib/json-glib.h>
26#include <limits.h>
27#include <math.h>
28#include <stdint.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32
33#define NN_MAX_DEPTH 8
34#define NN_MIN(a, b) ((a) < (b) ? (a) : (b))
35
36typedef struct nn_conv_t
37{
38 const float *w; // (out_ch, in_ch, k, k), row-major
39 const float *b; // (out_ch)
40 int out_ch, in_ch, k;
42
43#ifdef HAVE_OPENCL
44#define NN_MAX_DEVICES 16
45#endif
46
47// one wired U-Net (weights point into the model's shared blob)
56
58{
59 nn_unet_t fine; // stage 0: the mosaic net (the only net for arch "unet")
60 nn_unet_t coarse; // stage 1: the superpixel-RGB net (arch "unet-ms" only)
62 int bin_bayer, bin_xtrans; // superpixel bin factors per CFA family
63 int anchor; // low-band anchor scale in sensor px (0 = none)
64 float *blob; // whole payload, tensors point into it
65 size_t blob_floats; // number of floats in blob (for device upload)
66#ifdef HAVE_OPENCL
67 cl_mem dev_weights[NN_MAX_DEVICES]; // blob uploaded per device, lazily
68 dt_pthread_mutex_t cl_lock;
69#endif
70};
71
72/* injected pixel-buffer allocator (see nn_model.h); malloc fallback keeps the
73 * standalone fixture test dependency-free */
76
82
83/* Pixel buffers come from the injected arena — the pixelpipe cache memory
84 * arena in the application — and ONLY from it: the arena is the application's
85 * memory-budget control, and a malloc escape hatch would simply move the
86 * failure to the OS OOM killer, which kills the process instead of one tile.
87 * An arena refusal therefore fails the forward, and the caller falls back to
88 * an unprocessed tile — the tiling engine's job is to plan tiles small enough
89 * that this cannot happen (see dt_nn_unet_scratch_per_px and the module's
90 * tiling_callback). The malloc path below exists solely for the standalone
91 * fixture test, which runs outside the application with no allocator set. */
92
93static void *_nn_alloc(size_t floats, int long_lived)
94{
95 const size_t bytes = floats * sizeof(float);
97}
98
99static void _nn_free(void *p)
100{
101 if(!p) return;
102 if(_nn_free_fn)
103 _nn_free_fn(p);
104 else
105 free(p);
106}
107
108static void _err(char *err, size_t err_len, const char *msg)
109{
110 if(err && err_len) snprintf(err, err_len, "%s", msg);
111}
112
113/* ------------------------------------------------------------------------
114 * loader
115 * ------------------------------------------------------------------------ */
116
117typedef struct nn_header_t
118{
120 const float *payload;
123
124// resolve one conv layer's weight+bias by pytorch state-dict name prefix,
125// validating dimensions against the expectation
126static int _wire_conv(const nn_header_t *h, const char *prefix, int out_ch, int in_ch, int k, nn_conv_t *cv,
127 char *err, size_t err_len)
128{
129 char name[128];
130 const float *w = NULL, *b = NULL;
131 const guint n = json_array_get_length(h->tensors);
132 for(int part = 0; part < 2; part++)
133 {
134 snprintf(name, sizeof(name), "%s.%s", prefix, part == 0 ? "weight" : "bias");
135 const size_t want = (part == 0 ? (size_t)out_ch * in_ch * k * k : (size_t)out_ch) * sizeof(float);
136 const float *found = NULL;
137 for(guint i = 0; i < n; i++)
138 {
139 // the file comes from the user's config dir: never trust its structure.
140 // A non-object element or a missing/typeless member must be a clean
141 // reject, not a NULL dereference.
143 if(!node || !JSON_NODE_HOLDS_OBJECT(node)) continue;
145 if(!json_object_has_member(t, "name") || !json_object_has_member(t, "offset")
146 || !json_object_has_member(t, "size"))
147 continue;
148 if(g_strcmp0(json_object_get_string_member(t, "name"), name)) continue;
149 const gint64 offset = json_object_get_int_member(t, "offset");
150 const gint64 size = json_object_get_int_member(t, "size");
151 if(size != (gint64)want || offset < 0 || (size_t)(offset + size) > h->payload_size)
152 {
153 _err(err, err_len, "tensor with unexpected size or offset");
154 return 1;
155 }
156 found = (const float *)((const uint8_t *)h->payload + offset);
157 break;
158 }
159 if(!found)
160 {
161 if(err && err_len) snprintf(err, err_len, "missing tensor %s", name);
162 return 1;
163 }
164 if(part == 0)
165 w = found;
166 else
167 b = found;
168 }
169 cv->w = w;
170 cv->b = b;
171 cv->out_ch = out_ch;
172 cv->in_ch = in_ch;
173 cv->k = k;
174 return 0;
175}
176
177// wire one full U-Net from the tensor table; stage_prefix is "" for arch
178// "unet" and "coarse." / "fine." for arch "unet-ms" (pytorch submodule names)
179static int _wire_unet(const nn_header_t *h, const char *stage_prefix, int base, int depth, int in_ch, int out_ch,
180 nn_unet_t *u, char *err, size_t err_len)
181{
182 u->base = base;
183 u->depth = depth;
184 u->in_ch = in_ch;
185 u->out_ch = out_ch;
186 char prefix[96];
187 int bad = 0, cin = in_ch;
188 for(int l = 0; l < depth && !bad; l++)
189 {
190 const int w = base << l;
191 snprintf(prefix, sizeof(prefix), "%senc.%d.0", stage_prefix, l);
192 bad |= _wire_conv(h, prefix, w, cin, 3, &u->enc1[l], err, err_len);
193 snprintf(prefix, sizeof(prefix), "%senc.%d.2", stage_prefix, l);
194 bad |= _wire_conv(h, prefix, w, w, 3, &u->enc2[l], err, err_len);
195 snprintf(prefix, sizeof(prefix), "%sdown.%d", stage_prefix, l);
196 bad |= _wire_conv(h, prefix, w, w, 2, &u->down[l], err, err_len);
197 cin = w;
198 }
199 const int wb = base << depth;
200 snprintf(prefix, sizeof(prefix), "%sbottleneck.0", stage_prefix);
201 bad |= _wire_conv(h, prefix, wb, base << (depth - 1), 3, &u->bot1, err, err_len);
202 snprintf(prefix, sizeof(prefix), "%sbottleneck.2", stage_prefix);
203 bad |= _wire_conv(h, prefix, wb, wb, 3, &u->bot2, err, err_len);
204 // decoder ModuleLists were built from the deepest level down: up.0/dec.0
205 // operate on the bottleneck output, up.(depth-1)/dec.(depth-1) on level 0
206 for(int i = 0; i < depth && !bad; i++)
207 {
208 const int w_skip = base << (depth - 1 - i);
209 const int w_in = w_skip << 1;
210 snprintf(prefix, sizeof(prefix), "%sup.%d", stage_prefix, i);
211 bad |= _wire_conv(h, prefix, w_skip, w_in, 1, &u->up[i], err, err_len);
212 snprintf(prefix, sizeof(prefix), "%sdec.%d.0", stage_prefix, i);
213 bad |= _wire_conv(h, prefix, w_skip, 2 * w_skip, 3, &u->dec1[i], err, err_len);
214 snprintf(prefix, sizeof(prefix), "%sdec.%d.2", stage_prefix, i);
215 bad |= _wire_conv(h, prefix, w_skip, w_skip, 3, &u->dec2[i], err, err_len);
216 }
217 snprintf(prefix, sizeof(prefix), "%shead", stage_prefix);
218 bad |= _wire_conv(h, prefix, out_ch, base, 3, &u->head, err, err_len);
219 return bad;
220}
221
222// read {base, depth, in_channels, out_channels} from a (sub-)cfg object with
223// range validation; out_ch_max 1 keeps the historic fine-stage contract
224static int _read_net_cfg(JsonObject *cfg, int out_ch_max, int *base, int *depth, int *in_ch, int *out_ch)
225{
226 if(!cfg || !json_object_has_member(cfg, "base") || !json_object_has_member(cfg, "depth")
227 || !json_object_has_member(cfg, "in_channels") || !json_object_has_member(cfg, "out_channels"))
228 return 1;
229 *base = (int)json_object_get_int_member(cfg, "base");
230 *depth = (int)json_object_get_int_member(cfg, "depth");
231 *in_ch = (int)json_object_get_int_member(cfg, "in_channels");
232 *out_ch = (int)json_object_get_int_member(cfg, "out_channels");
235}
236
237dt_nn_model_t *dt_nn_model_load(const char *path, char *err, size_t err_len)
238{
239 FILE *f = g_fopen(path, "rb");
240 if(!f)
241 {
242 _err(err, err_len, "cannot open model file");
243 return NULL;
244 }
245 uint8_t magic[8];
246 uint32_t header_len = 0;
247 if(fread(magic, 1, 8, f) != 8 || memcmp(magic, "ANSELDN1", 8) || fread(&header_len, 4, 1, f) != 1
248 || header_len == 0 || header_len > (64u << 20))
249 {
250 _err(err, err_len, "not an ANSELDN1 model file");
251 fclose(f);
252 return NULL;
253 }
254 char *header = malloc(header_len + 1);
255 if(!header || fread(header, 1, header_len, f) != header_len)
256 {
257 _err(err, err_len, "truncated model header");
258 free(header);
259 fclose(f);
260 return NULL;
261 }
262 header[header_len] = '\0';
263
264 fseek(f, 0, SEEK_END);
265 const long file_size = ftell(f);
266 if(file_size < 12 + (long)header_len)
267 {
268 _err(err, err_len, "truncated model file");
269 free(header);
270 fclose(f);
271 return NULL;
272 }
273 const size_t payload_size = (size_t)file_size - 12 - header_len;
274 float *blob = malloc(payload_size);
275 fseek(f, 12 + (long)header_len, SEEK_SET);
276 const int payload_ok = blob && fread(blob, 1, payload_size, f) == payload_size;
277 fclose(f);
278 if(!payload_ok)
279 {
280 _err(err, err_len, "truncated model payload");
281 free(header);
282 free(blob);
283 return NULL;
284 }
285
289 {
290 _err(err, err_len, "invalid model header JSON");
291 goto out;
292 }
294 if(!root || !json_object_has_member(root, "cfg") || !json_object_has_member(root, "tensors"))
295 {
296 _err(err, err_len, "model header missing cfg or tensors");
297 goto out;
298 }
300 const char *arch = cfg ? json_object_get_string_member(cfg, "arch") : NULL;
301 const int is_ms = !g_strcmp0(arch, "unet-ms");
302 if(!is_ms && g_strcmp0(arch, "unet"))
303 {
304 _err(err, err_len, "unsupported model architecture");
305 goto out;
306 }
307 int f_base = 0, f_depth = 0, f_in = 0, f_out = 0;
308 int c_base = 0, c_depth = 0, c_in = 0, c_out = 0;
309 int bin_bayer = 1, bin_xtrans = 1;
310 if(is_ms)
311 {
312 if(!json_object_has_member(cfg, "coarse") || !json_object_has_member(cfg, "fine")
313 || !json_object_has_member(cfg, "bin")
316 {
317 _err(err, err_len, "model config out of supported range");
318 goto out;
319 }
321 if(!bin || !json_object_has_member(bin, "bayer") || !json_object_has_member(bin, "xtrans"))
322 {
323 _err(err, err_len, "model config missing bin factors");
324 goto out;
325 }
326 bin_bayer = (int)json_object_get_int_member(bin, "bayer");
327 bin_xtrans = (int)json_object_get_int_member(bin, "xtrans");
329 {
330 _err(err, err_len, "model bin factors out of supported range");
331 goto out;
332 }
333 }
334 else if(_read_net_cfg(cfg, 1, &f_base, &f_depth, &f_in, &f_out))
335 {
336 _err(err, err_len, "model config out of supported range");
337 goto out;
338 }
339 // the OpenCL path indexes weights with int offsets into the float blob
340 if(payload_size / sizeof(float) > (size_t)INT_MAX)
341 {
342 _err(err, err_len, "model payload too large");
343 goto out;
344 }
345 JsonNode *tensors_node = json_object_get_member(root, "tensors");
347 {
348 _err(err, err_len, "model header tensors is not an array");
349 goto out;
350 }
351
352 m = calloc(1, sizeof(dt_nn_model_t));
353 if(!m) goto out;
354 m->has_coarse = is_ms;
355 m->bin_bayer = bin_bayer;
356 m->bin_xtrans = bin_xtrans;
357 if(is_ms && json_object_has_member(cfg, "anchor"))
358 {
359 const int anchor = (int)json_object_get_int_member(cfg, "anchor");
360 if(anchor >= 8 && anchor <= 256) m->anchor = anchor;
361 }
362 m->blob = blob;
363 m->blob_floats = payload_size / sizeof(float);
364 // cfg "sigma_calibration" documents the noise convention the weights were
365 // trained under; it is deliberately NOT read here — the module's sigma
366 // conditioning is carried entirely by user-visible GUI values, never by a
367 // factor hidden inside the model file.
368#ifdef HAVE_OPENCL
369 dt_pthread_mutex_init(&m->cl_lock, NULL);
370#endif
371
372 const nn_header_t h
373 = { .tensors = json_object_get_array_member(root, "tensors"), .payload = blob, .payload_size = payload_size };
374 int bad = _wire_unet(&h, is_ms ? "fine." : "", f_base, f_depth, f_in, f_out, &m->fine, err, err_len);
375 if(is_ms && !bad) bad = _wire_unet(&h, "coarse.", c_base, c_depth, c_in, c_out, &m->coarse, err, err_len);
376
377 if(bad)
378 {
379#ifdef HAVE_OPENCL
380 dt_pthread_mutex_destroy(&m->cl_lock);
381#endif
382 free(m); // blob freed below through the common error path
383 m = NULL;
384 }
385
386out:
387 if(!m) free(blob);
388 free(header);
390 return m;
391}
392
393#ifdef HAVE_OPENCL
395{
396 for(int d = 0; d < NN_MAX_DEVICES; d++)
397 if(m->dev_weights[d])
398 {
399 dt_opencl_release_mem_object(m->dev_weights[d]);
400 m->dev_weights[d] = NULL;
401 }
402}
403#endif
404
406{
407 if(!m) return;
408#ifdef HAVE_OPENCL
410 dt_pthread_mutex_destroy(&m->cl_lock);
411#endif
412 free(m->blob);
413 free(m);
414}
415
417{
418 return m->fine.in_ch;
419}
420
422{
423 return m->fine.out_ch;
424}
425
427{
428 if(!m->has_coarse) return 1;
429 return is_xtrans ? m->bin_xtrans : m->bin_bayer;
430}
431
433{
434 return m->has_coarse ? m->coarse.in_ch : 0;
435}
436
438{
439 return m->has_coarse ? m->coarse.out_ch : 0;
440}
441
443{
444 return m->anchor;
445}
446
447static int _lcm(int a, int b)
448{
449 if(a <= 0 || b <= 0) return a > b ? a : b; // degenerate inputs: no zero division
450 int x = a, y = b;
451 while(y)
452 {
453 const int t = x % y;
454 x = y;
455 y = t;
456 }
457 return a / x * b;
458}
459
461{
462 // a padded tile must divide by the fine net's stride pyramid AND, for a
463 // multi-scale model, its binned version must divide by the coarse net's —
464 // for either CFA family, since the model file is CFA-agnostic
465 int align = 1 << m->fine.depth;
466 if(m->has_coarse)
467 {
468 align = _lcm(align, m->bin_bayer << m->coarse.depth);
469 align = _lcm(align, m->bin_xtrans << m->coarse.depth);
470 }
471 // ...and, when the model asks for the low-band fusion, by that pyramid too.
472 // The fusion runs at 16/32/64 sensor px (DT_NN_FUSION_COARSEST); a tile the
473 // coarsest band does not divide would silently fall back to a two-level
474 // pyramid, which is NOT what the model was fused against at training time
475 // and — since the tile grid depends on free RAM/vRAM — makes the rendered
476 // result depend on the machine and on whether the pipe tiled at all.
477 if(m->anchor > 0) align = _lcm(align, DT_NN_FUSION_COARSEST);
478 return align;
479}
480
481/* ------------------------------------------------------------------------
482 * executor
483 * ------------------------------------------------------------------------ */
484
485// Number of output channels computed together in the conv inner loop. Each
486// input value is loaded once and reused across this many weight-broadcast FMAs
487// (register-blocking), turning the memory-bound single-channel saxpy into
488// arithmetic-bound work. 4 keeps the accumulators + broadcasts within the SIMD
489// register file on AVX2/NEON. All layer widths here are multiples of 4 except
490// the 1-channel head, which falls through to the scalar-remainder path.
491#define NN_OC_BLOCK 4
492
493/* NOTE on a measured dead end: an 8-wide-strip row-pair microkernel (2 output
494 * rows x 4 output channels, the CPU translation of the GPU quad kernel) was
495 * implemented and benchmarked 2.6x SLOWER than the long-row formulation below
496 * (12.5 vs 4.9 s/MP at 512x512): the short strips defeat the compiler's
497 * full-width row vectorization and the 64-float accumulator block spills.
498 * With compiler-driven SIMD, long streaming rows win; beating them would take
499 * explicit intrinsics, not restructuring. */
500
501// out[oc] = bias[oc] + sum_ic conv(in[ic]); zero padding, any (k, stride).
502//
503// Output channels are blocked NN_OC_BLOCK at a time so each input load feeds
504// that many accumulators (one per output channel) before moving on; the input
505// plane is thus streamed out_ch/NN_OC_BLOCK times instead of out_ch times. The
506// (oy, oc-block) collapse keeps the k input rows hot across the block. The
507// per-output-element accumulation order (ic, ky, kx) is unchanged, so results
508// match the reference bit-for-bit under identical FP settings. The naive
509// formulation (shifted-plane saxpy, output revisited in_ch*k*k times) is
510// memory-bound and was measured ~20x slower at 512x512 tiles.
512static void _conv2d(const nn_conv_t *cv, const float *in, int w, int h, int stride, int pad, float *out)
513{
514 const int k = cv->k;
515 const int ow = (w + 2 * pad - k) / stride + 1;
516 const int oh = (h + 2 * pad - k) / stride + 1;
517 const size_t inhw = (size_t)w * h;
518 const size_t wstride = (size_t)cv->in_ch * k * k; // weight step between output channels
519#ifdef _OPENMP
520#pragma omp parallel
521#endif
522 {
523 float *const acc = malloc(sizeof(float) * NN_OC_BLOCK * ow);
524#ifdef _OPENMP
525#pragma omp for collapse(2) schedule(static)
526#endif
527 for(int oy = 0; oy < oh; oy++)
528 for(int ocb = 0; ocb < cv->out_ch; ocb += NN_OC_BLOCK)
529 {
530 const int nb = NN_MIN(NN_OC_BLOCK, cv->out_ch - ocb);
531 for(int r = 0; r < nb; r++)
532 {
533 float *const ar = acc + (size_t)r * ow;
534 const float bias = cv->b[ocb + r];
535 for(int ox = 0; ox < ow; ox++) ar[ox] = bias;
536 }
537 for(int ic = 0; ic < cv->in_ch; ic++)
538 {
539 const float *const ip = in + (size_t)ic * inhw;
540 for(int ky = 0; ky < k; ky++)
541 {
542 const int iy = oy * stride + ky - pad;
543 if(iy < 0 || iy >= h) continue;
544 const float *const irow = ip + (size_t)iy * w;
545 for(int kx = 0; kx < k; kx++)
546 {
547 const int shift = kx - pad;
548 int ox0 = 0, ox1 = ow;
549 while(ox0 < ow && ox0 * stride + shift < 0) ox0++;
550 while(ox1 > ox0 && (ox1 - 1) * stride + shift >= w) ox1--;
551 const float *const wbase = cv->w + ((size_t)ocb * cv->in_ch + ic) * k * k + ky * k + kx;
552
553 if(nb == NN_OC_BLOCK)
554 {
555 const float w0 = wbase[0], w1 = wbase[wstride];
556 const float w2 = wbase[2 * wstride], w3 = wbase[3 * wstride];
557 float *const a0 = acc, *const a1 = acc + ow;
558 float *const a2 = acc + 2 * ow, *const a3 = acc + 3 * ow;
559 if(stride == 1)
560 {
561 const float *const is = irow + shift;
562#ifdef _OPENMP
563#pragma omp simd
564#endif
565 for(int ox = ox0; ox < ox1; ox++)
566 {
567 const float xv = is[ox];
568 a0[ox] += w0 * xv;
569 a1[ox] += w1 * xv;
570 a2[ox] += w2 * xv;
571 a3[ox] += w3 * xv;
572 }
573 }
574 else
575 for(int ox = ox0; ox < ox1; ox++)
576 {
577 const float xv = irow[ox * stride + shift];
578 a0[ox] += w0 * xv;
579 a1[ox] += w1 * xv;
580 a2[ox] += w2 * xv;
581 a3[ox] += w3 * xv;
582 }
583 }
584 else // remainder block (out_ch not a multiple of NN_OC_BLOCK, e.g. the head)
585 for(int r = 0; r < nb; r++)
586 {
587 const float wv = wbase[(size_t)r * wstride];
588 float *const ar = acc + (size_t)r * ow;
589 if(stride == 1)
590 {
591 const float *const is = irow + shift;
592#ifdef _OPENMP
593#pragma omp simd
594#endif
595 for(int ox = ox0; ox < ox1; ox++) ar[ox] += wv * is[ox];
596 }
597 else
598 for(int ox = ox0; ox < ox1; ox++) ar[ox] += wv * irow[ox * stride + shift];
599 }
600 }
601 }
602 }
603 for(int r = 0; r < nb; r++)
604 memcpy(out + ((size_t)(ocb + r) * oh + oy) * ow, acc + (size_t)r * ow, sizeof(float) * ow);
605 }
606 free(acc);
607 }
608}
609
610// exact GELU, matching pytorch nn.GELU(approximate='none')
612static void _gelu(float *x, size_t n)
613{
614#ifdef _OPENMP
615#pragma omp parallel for schedule(static)
616#endif
617 for(size_t i = 0; i < n; i++) x[i] = 0.5f * x[i] * (1.0f + erff(x[i] * (float)M_SQRT1_2));
618}
619
620
621/* Peak live scratch of one net's forward, in floats: the ledger of the EXACT
622 * allocate/free sequence of the forwards. cl_variant selects which one:
623 * the CPU path (cl_variant 0) reads dec1's concat in place via _conv2d_cat2
624 * and allocates neither the physical concat nor an upsample staging buffer;
625 * the CL path (cl_variant 1) materializes both (cat + us). Any edit to either
626 * forward must be reflected here, or the tiling engine plans against the
627 * wrong number. */
628static size_t _unet_peak_floats(const nn_unet_t *u, size_t wh, int cl_variant)
629{
630 const size_t base = (size_t)u->base;
631 size_t live = 0, peak = 0, cur = 0;
632#define NN_LEDGER(delta) \
633 do \
634 { \
635 live += (delta); \
636 if(live > peak) peak = live; \
637 } while(0)
638 // encoder
639 for(int l = 0; l < u->depth; l++)
640 {
641 const size_t lvl = base * wh >> l;
642 NN_LEDGER(lvl); // tmp
643 NN_LEDGER(lvl); // skips[l] — stays live until its decoder concat
644 NN_LEDGER(lvl >> 2); // next
645 live -= lvl; // tmp freed
646 live -= cur; // previous level's input freed
647 cur = lvl >> 2;
648 }
649 // bottleneck
650 const size_t bot = base * wh >> u->depth;
651 NN_LEDGER(bot);
652 NN_LEDGER(bot);
653 live -= bot;
654 live -= cur;
655 cur = bot;
656 // decoder
657 for(int i = 0; i < u->depth; i++)
658 {
659 const int l = u->depth - 1 - i;
660 const size_t half = base * wh >> l;
661 NN_LEDGER(half >> 2); // v (1x1 output on the coarse grid)
662 live -= cur; // cur freed
663 if(cl_variant)
664 {
665 NN_LEDGER(2 * half); // cat
666 live -= half; // skips[l] freed after its copy
667 NN_LEDGER(half); // us
668 live -= half >> 2; // v freed
669 live -= half; // us freed
670 NN_LEDGER(half); // d1
671 live -= 2 * half; // cat freed
672 }
673 else
674 {
675 NN_LEDGER(half); // d1 (dec1 reads skip + v in place)
676 live -= half >> 2; // v freed
677 live -= half; // skips[l] freed
678 }
679 NN_LEDGER(half); // new cur (dec2 output)
680 live -= half; // d1 freed
681 cur = half;
682 }
683 NN_LEDGER((size_t)u->out_ch * wh); // head
684#undef NN_LEDGER
685 return peak;
686}
687
688static float _scratch_per_px(const dt_nn_model_t *m, int cl_variant)
689{
690 /* A large reference area keeps the integer ledger exact (every term is
691 * base*wh >> k); the result is the dimensionless factor of the input image
692 * size the tiling engine wants — never absolute bytes. The coarse and fine
693 * stages never run concurrently (the caller frees the coarse buffers before
694 * the fine forward), so the model peak is the MAX of the two, not the sum. */
695 const size_t ref = (size_t)1 << 24;
696 float per_px = (float)_unet_peak_floats(&m->fine, ref, cl_variant) / (float)ref;
697 if(m->has_coarse)
698 {
699 const int bin = NN_MIN(m->bin_bayer, m->bin_xtrans); // smaller bin = larger coarse buffer
700 const float coarse
701 = (float)_unet_peak_floats(&m->coarse, ref / ((size_t)bin * bin), cl_variant) / (float)ref;
702 if(coarse > per_px) per_px = coarse;
703 }
704 return per_px;
705}
706
708{
709 return _scratch_per_px(m, 0);
710}
711
713{
714 return _scratch_per_px(m, 1);
715}
716
718{
719 /* Largest SINGLE scratch tensor per input pixel, host path: one base*wh
720 * plane (dec1's output / a skip / an encoder tmp — all equal at level 0).
721 * The pixelpipe arena serves contiguous runs and its address space is
722 * partitioned by entries pinned during the pipe recursion, so the largest
723 * tensor — not the total — is what an allocation can actually get; the
724 * module's tiling_callback uses this to keep it comfortably below the
725 * planned budget. */
726 float per_px = (float)m->fine.base;
727 if(m->has_coarse)
728 {
729 const int bin = NN_MIN(m->bin_bayer, m->bin_xtrans);
730 const float coarse = (float)m->coarse.base / (float)(bin * bin);
731 if(coarse > per_px) per_px = coarse;
732 }
733 return per_px;
734}
735
737{
738 const size_t wh = (size_t)width * height;
739 size_t floats = _unet_peak_floats(&m->fine, wh, 0);
740 if(m->has_coarse)
741 {
742 const int bin = NN_MIN(m->bin_bayer, m->bin_xtrans);
743 const size_t coarse = _unet_peak_floats(&m->coarse, wh / ((size_t)bin * bin), 0);
744 if(coarse > floats) floats = coarse;
745 }
746 return floats * sizeof(float);
747}
748
749/* dec1 variant of _conv2d for k=3, stride=1, pad=1: the input is the channel
750 * concat [a (in_ch_a channels, full res) | b (cv->in_ch - in_ch_a channels,
751 * HALF resolution, read through a nearest-x2 upsample view)]. Reading b in
752 * place is bit-identical to materializing upsample(b) and running _conv2d on
753 * a physical concat — nearest upsampling replicates values, so every tap
754 * reads the same number in the same accumulation order — but the 2*base*wh
755 * concat tensor, the module's largest single allocation and therefore the
756 * arena's contiguity bottleneck, never exists. Keep the accumulation order
757 * identical to _conv2d: same (ic, ky, kx) nesting, same OC blocking. */
759static void _conv2d_cat2(const nn_conv_t *cv, const float *a, int in_ch_a, const float *b, int w, int h,
760 float *out)
761{
762 const int k = cv->k; // always 3 here, kept general for the tap arithmetic
763 const int pad = 1;
764 const int ow = w, oh = h;
765 const int bw = w / 2;
766 const size_t inhw = (size_t)w * h;
767 const size_t bhw = (size_t)bw * (h / 2);
768 const size_t wstride = (size_t)cv->in_ch * k * k;
769#ifdef _OPENMP
770#pragma omp parallel
771#endif
772 {
773 float *const acc = malloc(sizeof(float) * NN_OC_BLOCK * ow);
774#ifdef _OPENMP
775#pragma omp for collapse(2) schedule(static)
776#endif
777 for(int oy = 0; oy < oh; oy++)
778 for(int ocb = 0; ocb < cv->out_ch; ocb += NN_OC_BLOCK)
779 {
780 const int nb = NN_MIN(NN_OC_BLOCK, cv->out_ch - ocb);
781 for(int r = 0; r < nb; r++)
782 {
783 float *const ar = acc + (size_t)r * ow;
784 const float bias = cv->b[ocb + r];
785 for(int ox = 0; ox < ow; ox++) ar[ox] = bias;
786 }
787 for(int ic = 0; ic < cv->in_ch; ic++)
788 {
789 const int from_b = ic >= in_ch_a;
790 const float *const ip = from_b ? b + (size_t)(ic - in_ch_a) * bhw : a + (size_t)ic * inhw;
791 for(int ky = 0; ky < k; ky++)
792 {
793 const int iy = oy + ky - pad;
794 if(iy < 0 || iy >= h) continue;
795 const float *const irow = from_b ? ip + (size_t)(iy >> 1) * bw : ip + (size_t)iy * w;
796 for(int kx = 0; kx < k; kx++)
797 {
798 const int shift = kx - pad;
799 int ox0 = 0, ox1 = ow;
800 while(ox0 < ow && ox0 + shift < 0) ox0++;
801 while(ox1 > ox0 && (ox1 - 1) + shift >= w) ox1--;
802 const float *const wbase = cv->w + ((size_t)ocb * cv->in_ch + ic) * k * k + ky * k + kx;
803 if(nb == NN_OC_BLOCK)
804 {
805 // 4-way output-channel blocking, mirroring _conv2d's fast
806 // path: one pass over the row feeds four accumulators
807 const float w0 = wbase[0], w1 = wbase[wstride];
808 const float w2 = wbase[2 * wstride], w3 = wbase[3 * wstride];
809 float *const a0 = acc, *const a1 = acc + ow;
810 float *const a2 = acc + 2 * ow, *const a3 = acc + 3 * ow;
811 if(from_b)
812 for(int ox = ox0; ox < ox1; ox++)
813 {
814 const float xv = irow[(ox + shift) >> 1];
815 a0[ox] += w0 * xv;
816 a1[ox] += w1 * xv;
817 a2[ox] += w2 * xv;
818 a3[ox] += w3 * xv;
819 }
820 else
821 {
822 const float *const is = irow + shift;
823#ifdef _OPENMP
824#pragma omp simd
825#endif
826 for(int ox = ox0; ox < ox1; ox++)
827 {
828 const float xv = is[ox];
829 a0[ox] += w0 * xv;
830 a1[ox] += w1 * xv;
831 a2[ox] += w2 * xv;
832 a3[ox] += w3 * xv;
833 }
834 }
835 }
836 else
837 for(int r = 0; r < nb; r++)
838 {
839 const float wr = wbase[(size_t)r * wstride];
840 float *const ar = acc + (size_t)r * ow;
841 if(from_b)
842 for(int ox = ox0; ox < ox1; ox++) ar[ox] += wr * irow[(ox + shift) >> 1];
843 else
844 {
845 const float *const is = irow + shift;
846#ifdef _OPENMP
847#pragma omp simd
848#endif
849 for(int ox = ox0; ox < ox1; ox++) ar[ox] += wr * is[ox];
850 }
851 }
852 }
853 }
854 }
855 for(int r = 0; r < nb; r++)
856 memcpy(out + ((size_t)(ocb + r)) * inhw + (size_t)oy * ow, acc + (size_t)r * ow,
857 sizeof(float) * ow);
858 }
859 free(acc);
860 }
861}
862
863/* Full forward pass of one U-Net.
864 *
865 * Memory discipline — this is where the module's RAM peak lives, so every
866 * tensor is allocated at its EXACT size the moment it is needed and released
867 * on its last use, instead of the former three worst-case ping-pong arenas
868 * plus all skips held to the end (7.9*base*wh floats live). The live-set peak
869 * is now dec1 at level 0: ~2.25*base*wh floats, with the largest single
870 * allocation ONE base*wh plane — the binding constraint for the pixelpipe
871 * arena, which serves contiguous runs. _unet_peak_floats()
872 * is the ledger of this exact sequence — KEEP THE TWO IN SYNC.
873 *
874 * The 1x1 up-convs are applied BEFORE their nearest x2 upsample: a 1x1 conv
875 * is per-pixel and nearest upsampling replicates pixels, so conv(up(x)) and
876 * up(conv(x)) are the same values from the same FP operations — but computed
877 * on 4x fewer pixels, and the (2*w_skip)@full-res tensor never exists.
878 *
879 * residual_ch > 0 subtracts the head from the input's first residual_ch
880 * planes; residual_ch == 0 writes the raw head output. */
881static int _unet_forward(const nn_unet_t *u, const float *in, float *out, int width, int height, int residual_ch)
882{
883 const int align = 1 << u->depth;
884 if(width % align || height % align || width <= 0 || height <= 0) return 1;
885
886 const size_t wh = (size_t)width * height;
887 const size_t base = (size_t)u->base;
888 float *skips[NN_MAX_DEPTH] = { NULL };
889
890 // encoder: skip[l] = (base<<l) channels at (wh >> 2l) px = base*wh >> l floats
891 const float *src = in;
892 int cw = width, chh = height;
893 float *cur = NULL;
894 int ok = 1;
895 for(int l = 0; l < u->depth && ok; l++)
896 {
897 const size_t lvl = base * wh >> l;
898 float *tmp = _nn_alloc(lvl, 0);
899 skips[l] = _nn_alloc(lvl, 1);
900 float *next = _nn_alloc(lvl >> 2, 0);
901 if(!tmp || !skips[l] || !next)
902 {
903 _nn_free(tmp);
904 _nn_free(next);
905 ok = 0;
906 break;
907 }
908 _conv2d(&u->enc1[l], src, cw, chh, 1, 1, tmp);
909 _gelu(tmp, (size_t)u->enc1[l].out_ch * cw * chh);
910 _conv2d(&u->enc2[l], tmp, cw, chh, 1, 1, skips[l]);
911 _gelu(skips[l], (size_t)u->enc2[l].out_ch * cw * chh);
912 _nn_free(tmp);
913 _conv2d(&u->down[l], skips[l], cw, chh, 2, 0, next);
914 _nn_free(cur); // level l's input, dead now (never frees `in`: cur is NULL then)
915 cur = next;
916 cw /= 2;
917 chh /= 2;
918 src = cur;
919 }
920
921 // bottleneck: (base<<depth) channels at wh >> 2*depth px
922 if(ok)
923 {
924 const size_t bot = base * wh >> u->depth;
925 float *tmp = _nn_alloc(bot, 0);
926 float *bout = _nn_alloc(bot, 0);
927 if(!tmp || !bout)
928 {
929 _nn_free(tmp);
930 _nn_free(bout);
931 ok = 0;
932 }
933 else
934 {
935 _conv2d(&u->bot1, src, cw, chh, 1, 1, tmp);
936 _gelu(tmp, (size_t)u->bot1.out_ch * cw * chh);
937 _conv2d(&u->bot2, tmp, cw, chh, 1, 1, bout);
938 _gelu(bout, (size_t)u->bot2.out_ch * cw * chh);
939 _nn_free(tmp);
940 _nn_free(cur);
941 cur = bout;
942 }
943 }
944
945 // decoder: up.i / dec.i #i pairs with encoder level (depth-1-i). The 1x1
946 // up-conv runs on the coarse grid (see the doc comment), and dec1 reads its
947 // concat input IN PLACE via _conv2d_cat2 — skip at full resolution, the 1x1
948 // output through a nearest-upsample view — so the module's former largest
949 // tensor (the physical 2*w_skip concat) is never allocated at all.
950 for(int i = 0; i < u->depth && ok; i++)
951 {
952 const int l = u->depth - 1 - i;
953 const size_t w_skip = base << l;
954 const size_t half = w_skip * (size_t)(2 * cw) * (size_t)(2 * chh); // one concat half
955 float *v = _nn_alloc(half >> 2, 1); // top end: must not split the big-tensor churn area
956 if(!v) { ok = 0; break; }
957 _conv2d(&u->up[i], cur, cw, chh, 1, 0, v);
958 _nn_free(cur);
959 cur = NULL;
960 cw *= 2;
961 chh *= 2;
962 float *d1 = _nn_alloc(half, 0);
963 if(!d1) { _nn_free(v); ok = 0; break; }
964 _conv2d_cat2(&u->dec1[i], skips[l], (int)w_skip, v, cw, chh, d1);
965 _gelu(d1, w_skip * (size_t)cw * chh);
966 _nn_free(v);
967 _nn_free(skips[l]);
968 skips[l] = NULL;
969 float *d2 = _nn_alloc(half, 0);
970 if(!d2) { _nn_free(d1); ok = 0; break; }
971 _conv2d(&u->dec2[i], d1, cw, chh, 1, 1, d2);
972 _gelu(d2, w_skip * (size_t)cw * chh);
973 _nn_free(d1);
974 cur = d2;
975 }
976
977 if(ok)
978 {
979 float *head = _nn_alloc((size_t)u->out_ch * wh, 0);
980 if(!head)
981 ok = 0;
982 else
983 {
984 _conv2d(&u->head, cur, width, height, 1, 1, head);
985 if(residual_ch > 0)
986 {
987 // residual head: out = input planes - predicted noise
988#ifdef _OPENMP
989#pragma omp parallel for schedule(static)
990#endif
991 for(size_t i = 0; i < (size_t)residual_ch * wh; i++) out[i] = in[i] - head[i];
992 }
993 else
994 memcpy(out, head, (size_t)u->out_ch * wh * sizeof(float));
995 _nn_free(head);
996 }
997 }
998
999 for(int l = 0; l < u->depth; l++) _nn_free(skips[l]);
1000 _nn_free(cur);
1001 return ok ? 0 : 1;
1002}
1003
1004int dt_nn_unet_apply(const dt_nn_model_t *m, const float *in, float *out, int width, int height)
1005{
1006 return _unet_forward(&m->fine, in, out, width, height, m->fine.out_ch);
1007}
1008
1009int dt_nn_unet_apply_stage(const dt_nn_model_t *m, int stage, const float *in, float *out, int width,
1010 int height, int apply_residual)
1011{
1012 if(stage == 1)
1013 {
1014 if(!m->has_coarse) return 1;
1015 // coarse stage: the head predicts a correction to its RGB planes
1016 return _unet_forward(&m->coarse, in, out, width, height, apply_residual ? m->coarse.out_ch : 0);
1017 }
1018 return _unet_forward(&m->fine, in, out, width, height, apply_residual ? m->fine.out_ch : 0);
1019}
1020
1022void dt_nn_bin_planes(const float *planes, int pw, int ph, int bin, float *out_rgb, float *out_cnt)
1023{
1024 // planes = [mosaic, onehot_R, onehot_G, onehot_B, ...] as assembled for the
1025 // fine net. Each coarse pixel is the count-weighted mean of the block's
1026 // same-channel sensels — the exact contract of cfa.bin_mosaic_torch in the
1027 // training repo. With bin 4 (Bayer) / 6 (X-Trans) every count is > 0 by
1028 // construction; the max() is a numerical guard, not a fallback.
1029 const size_t plane = (size_t)pw * ph;
1030 const int cw = pw / bin, chh = ph / bin;
1031 const float *const mosaic = planes;
1032#ifdef _OPENMP
1033#pragma omp parallel for schedule(static) collapse(2)
1034#endif
1035 for(int c = 0; c < 3; c++)
1036 for(int cy = 0; cy < chh; cy++)
1037 {
1038 const float *const onehot = planes + (size_t)(1 + c) * plane;
1039 float *const orow = out_rgb + (size_t)c * cw * chh + (size_t)cy * cw;
1040 float *const crow = out_cnt + (size_t)c * cw * chh + (size_t)cy * cw;
1041 for(int cx = 0; cx < cw; cx++)
1042 {
1043 float sum = 0.0f, cnt = 0.0f;
1044 for(int y = cy * bin; y < (cy + 1) * bin; y++)
1045 for(int x = cx * bin; x < (cx + 1) * bin; x++)
1046 {
1047 const size_t i = (size_t)y * pw + x;
1048 sum += mosaic[i] * onehot[i];
1049 cnt += onehot[i];
1050 }
1051 crow[cx] = cnt;
1052 orow[cx] = sum / (cnt > 1.0f ? cnt : 1.0f);
1053 }
1054 }
1055}
1056
1058void dt_nn_upsample_nearest(const float *in, int ch, int w, int h, int factor, float *out)
1059{
1060#ifdef _OPENMP
1061#pragma omp parallel for schedule(static)
1062#endif
1063 for(int c = 0; c < ch; c++)
1064 {
1065 const float *const ip = in + (size_t)c * w * h;
1066 float *const op = out + (size_t)c * w * h * factor * factor;
1067 for(int y = 0; y < h * factor; y++)
1068 {
1069 const float *const irow = ip + (size_t)(y / factor) * w;
1070 float *const orow = op + (size_t)y * w * factor;
1071 for(int x = 0; x < w * factor; x++) orow[x] = irow[x / factor];
1072 }
1073 }
1074}
1075
1076/* ------------------------------------------------------------------------
1077 * OpenCL executor
1078 * ------------------------------------------------------------------------ */
1079#ifdef HAVE_OPENCL
1080
1087
1089{
1090 dt_nn_cl_t *cl = calloc(1, sizeof(dt_nn_cl_t));
1091 if(!cl) return NULL;
1092 cl->kernel_conv = dt_opencl_create_kernel(program, "nn_conv");
1093 cl->kernel_conv3x3 = dt_opencl_create_kernel(program, "nn_conv3x3");
1094 cl->kernel_upsample = dt_opencl_create_kernel(program, "nn_upsample");
1095 return cl;
1096}
1097
1099{
1100 if(!cl) return;
1104 free(cl);
1105}
1106
1107
1108// upload the weight blob to the device once, cached per device
1109static cl_mem _weights_cl(const dt_nn_model_t *m, int devid)
1110{
1112 dt_nn_model_t *mm = (dt_nn_model_t *)m; // cache mutation on a logically const model
1113 dt_pthread_mutex_lock(&mm->cl_lock);
1114 if(!mm->dev_weights[devid])
1115 {
1116 const size_t bytes = m->blob_floats * sizeof(float);
1117 cl_mem buf = dt_opencl_alloc_device_buffer(devid, bytes);
1118 if(buf && dt_opencl_write_buffer_to_device(devid, m->blob, buf, 0, bytes, CL_TRUE) == CL_SUCCESS)
1119 mm->dev_weights[devid] = buf;
1120 else if(buf)
1122 }
1123 cl_mem w = mm->dev_weights[devid];
1124 dt_pthread_mutex_unlock(&mm->cl_lock);
1125 return w;
1126}
1127
1128// enqueue one convolution (+ optional GELU) reading from `in`, writing `out`
1129static int _conv_cl(dt_nn_cl_t *cl, int devid, cl_mem weights, const float *blob_base, cl_mem in, cl_mem out,
1130 int w, int h, const nn_conv_t *cv, int stride, int pad, int do_gelu)
1131{
1132 const int ow = (w + 2 * pad - cv->k) / stride + 1;
1133 const int oh = (h + 2 * pad - cv->k) / stride + 1;
1134 const int weight_off = (int)(cv->w - blob_base);
1135 const int bias_off = (int)(cv->b - blob_base);
1136 const size_t slice_bytes = sizeof(float) * 4 * cv->in_ch * cv->k * cv->k;
1137 const int use_local = slice_bytes <= (30 << 10);
1138 const size_t lx = 128;
1139
1140 if(cv->k == 3 && stride == 1)
1141 {
1142 // fast quad kernel: 2x2 output pixels per work-item (see rawdenoiseai.cl).
1143 // Weights are staged in local memory `chunk` input channels at a time so
1144 // even the in_ch 256/512 layers never read weights per-item from global
1145 // (~24 KB of local keeps 2 work-groups per SM on common GPUs).
1146 const int chunk = NN_MIN(cv->in_ch, (24 << 10) / (int)(sizeof(float) * 4 * 9));
1147 const int K3 = cl->kernel_conv3x3;
1148 dt_opencl_set_kernel_arg(devid, K3, 0, sizeof(cl_mem), &in);
1149 dt_opencl_set_kernel_arg(devid, K3, 1, sizeof(cl_mem), &weights);
1150 dt_opencl_set_kernel_arg(devid, K3, 2, sizeof(cl_mem), &out);
1151 dt_opencl_set_kernel_arg(devid, K3, 3, sizeof(int), &w);
1152 dt_opencl_set_kernel_arg(devid, K3, 4, sizeof(int), &h);
1153 dt_opencl_set_kernel_arg(devid, K3, 5, sizeof(int), &cv->in_ch);
1154 dt_opencl_set_kernel_arg(devid, K3, 6, sizeof(int), &cv->out_ch);
1155 dt_opencl_set_kernel_arg(devid, K3, 7, sizeof(int), &weight_off);
1156 dt_opencl_set_kernel_arg(devid, K3, 8, sizeof(int), &bias_off);
1157 dt_opencl_set_kernel_arg(devid, K3, 9, sizeof(int), &do_gelu);
1158 dt_opencl_set_kernel_arg(devid, K3, 10, sizeof(int), &chunk);
1159 dt_opencl_set_kernel_arg(devid, K3, 11, sizeof(float) * 4 * 9 * chunk, NULL);
1160 const size_t quads = (size_t)((w + 1) / 2) * ((h + 1) / 2);
1161 size_t sizes3[3] = { (quads + lx - 1) / lx * lx, ((size_t)cv->out_ch + 3) / 4, 1 };
1162 size_t local3[3] = { lx, 1, 1 };
1164 }
1165
1166 const int K = cl->kernel_conv;
1167 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &in);
1168 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &weights);
1169 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(cl_mem), &out);
1170 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(int), &w);
1171 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(int), &h);
1172 dt_opencl_set_kernel_arg(devid, K, 5, sizeof(int), &ow);
1173 dt_opencl_set_kernel_arg(devid, K, 6, sizeof(int), &oh);
1174 dt_opencl_set_kernel_arg(devid, K, 7, sizeof(int), &cv->in_ch);
1175 dt_opencl_set_kernel_arg(devid, K, 8, sizeof(int), &cv->out_ch);
1176 dt_opencl_set_kernel_arg(devid, K, 9, sizeof(int), &cv->k);
1177 dt_opencl_set_kernel_arg(devid, K, 10, sizeof(int), &stride);
1178 dt_opencl_set_kernel_arg(devid, K, 11, sizeof(int), &pad);
1179 dt_opencl_set_kernel_arg(devid, K, 12, sizeof(int), &weight_off);
1180 dt_opencl_set_kernel_arg(devid, K, 13, sizeof(int), &bias_off);
1181 dt_opencl_set_kernel_arg(devid, K, 14, sizeof(int), &do_gelu);
1182 // 4 output channels per work-item (NN_OCB in the kernel); their weight
1183 // slices are staged in local memory when they fit the conservative 30 KB
1184 // budget — always true for the wide shallow layers where weight traffic
1185 // matters. (Staging the INPUT rows in local memory was tried and measured
1186 // slower: with the output-channel blocking in work dim 1, every oc-block
1187 // group re-stages the same rows and the per-channel barriers serialize,
1188 // while the L2 already absorbs the 3x3 neighbourhood overlap.)
1189 dt_opencl_set_kernel_arg(devid, K, 15, sizeof(int), &use_local);
1190 dt_opencl_set_kernel_arg(devid, K, 16, use_local ? slice_bytes : sizeof(float), NULL);
1191 size_t sizes[3] = { ((size_t)ow * oh + lx - 1) / lx * lx, ((size_t)cv->out_ch + 3) / 4, 1 };
1192 size_t local[3] = { lx, 1, 1 };
1193 return dt_opencl_enqueue_kernel_2d_with_local(devid, K, sizes, local);
1194}
1195
1196static int _upsample_cl(dt_nn_cl_t *cl, int devid, cl_mem in, cl_mem out, int w, int h, int ch)
1197{
1198 const int K = cl->kernel_upsample;
1199 dt_opencl_set_kernel_arg(devid, K, 0, sizeof(cl_mem), &in);
1200 dt_opencl_set_kernel_arg(devid, K, 1, sizeof(cl_mem), &out);
1201 dt_opencl_set_kernel_arg(devid, K, 2, sizeof(int), &w);
1202 dt_opencl_set_kernel_arg(devid, K, 3, sizeof(int), &h);
1203 dt_opencl_set_kernel_arg(devid, K, 4, sizeof(int), &ch);
1204 size_t sizes[3] = { ROUNDUPDWD((size_t)4 * w * h, devid), ROUNDUPDHT(ch, devid), 1 };
1205 return dt_opencl_enqueue_kernel_2d(devid, K, sizes);
1206}
1207
1208// forward one U-Net on the device. Writes the RAW head output (no residual):
1209// the fine stage's residual is applied by the caller after readback (the
1210// historic CPU/CL asymmetry), and the coarse stage's residual is applied
1211// host-side too so both paths share the same subtraction code bit-for-bit.
1212/* Device twin of _unet_forward: the same live-set discipline — every buffer
1213 * allocated at its exact size when needed and released on last use, the same
1214 * 1x1-before-upsample commutation — so VRAM peaks at the level-0 concat
1215 * (~3.25*base*wh floats) instead of holding three worst-case arenas plus
1216 * every skip to the end. All temporaries live at function scope and are released in
1217 * cleanup, so a device-OOM mid-sequence (the very case this path exists for:
1218 * fall back to CPU on a full card) cannot leak. Releases happen at enqueue
1219 * time and the runtime defers destruction until in-flight commands finish, so
1220 * on drivers that commit at clCreateBuffer the instantaneous footprint can
1221 * transiently exceed the ledger by the pending-release set; the factor_cl
1222 * headroom absorbs that, and the failure mode is the graceful CPU fallback. _unet_peak_floats() is the
1223 * shared ledger of this sequence: KEEP ALL THREE IN SYNC. */
1224static int _unet_forward_cl(const dt_nn_model_t *m, const nn_unet_t *u, dt_nn_cl_t *cl, int devid, cl_mem dev_in,
1225 cl_mem dev_out, int width, int height)
1226{
1227 const int align = 1 << u->depth;
1228 if(width % align || height % align || width <= 0 || height <= 0) return -1;
1229
1230 cl_mem weights = _weights_cl(m, devid);
1231 if(!weights) return -1;
1232
1233 const size_t wh = (size_t)width * height;
1234 const size_t base = (size_t)u->base;
1235 int err = CL_SUCCESS;
1236 cl_mem skips[NN_MAX_DEPTH] = { NULL };
1237 cl_mem cur = NULL, tmp = NULL, v = NULL, cat = NULL, us = NULL, d1 = NULL;
1238
1239#define NN_CL_ALLOC(var, floats) \
1240 do \
1241 { \
1242 var = dt_opencl_alloc_device_buffer(devid, (floats) * sizeof(float)); \
1243 if(!var) \
1244 { \
1245 err = -1; \
1246 goto cleanup; \
1247 } \
1248 } while(0)
1249#define NN_CL_FREE(var) \
1250 do \
1251 { \
1252 if(var) dt_opencl_release_mem_object(var); \
1253 var = NULL; \
1254 } while(0)
1255
1256 // encoder
1257 cl_mem src = dev_in;
1258 int cw = width, chh = height;
1259 for(int l = 0; l < u->depth && err == CL_SUCCESS; l++)
1260 {
1261 const size_t lvl = base * wh >> l;
1262 cl_mem next = NULL;
1264 NN_CL_ALLOC(skips[l], lvl);
1265 NN_CL_ALLOC(next, lvl >> 2);
1266 err |= _conv_cl(cl, devid, weights, m->blob, src, tmp, cw, chh, &u->enc1[l], 1, 1, 1);
1267 err |= _conv_cl(cl, devid, weights, m->blob, tmp, skips[l], cw, chh, &u->enc2[l], 1, 1, 1);
1268 NN_CL_FREE(tmp);
1269 err |= _conv_cl(cl, devid, weights, m->blob, skips[l], next, cw, chh, &u->down[l], 2, 0, 0);
1270 NN_CL_FREE(cur); // level l's input; never dev_in (cur is NULL on l == 0)
1271 cur = next;
1272 cw /= 2;
1273 chh /= 2;
1274 src = cur;
1275 }
1276
1277 // bottleneck (bout reuses the `v` slot so cleanup covers it)
1278 if(err == CL_SUCCESS)
1279 {
1280 const size_t bot = base * wh >> u->depth;
1282 NN_CL_ALLOC(v, bot);
1283 err |= _conv_cl(cl, devid, weights, m->blob, src, tmp, cw, chh, &u->bot1, 1, 1, 1);
1284 err |= _conv_cl(cl, devid, weights, m->blob, tmp, v, cw, chh, &u->bot2, 1, 1, 1);
1285 NN_CL_FREE(tmp);
1286 NN_CL_FREE(cur);
1287 cur = v;
1288 v = NULL;
1289 }
1290
1291 // decoder: up.i / dec.i pair with encoder level l = depth-1-i; the 1x1
1292 // up-conv runs on the coarse grid, then upsamples (see the doc comment)
1293 for(int i = 0; i < u->depth && err == CL_SUCCESS; i++)
1294 {
1295 const int l = u->depth - 1 - i;
1296 const size_t w_skip = base << l;
1297 const size_t half = w_skip * (size_t)(2 * cw) * (size_t)(2 * chh);
1298 NN_CL_ALLOC(v, half >> 2);
1299 err |= _conv_cl(cl, devid, weights, m->blob, cur, v, cw, chh, &u->up[i], 1, 0, 0);
1300 NN_CL_FREE(cur);
1301 NN_CL_ALLOC(cat, 2 * half);
1302 err |= dt_opencl_enqueue_copy_buffer_to_buffer(devid, skips[l], cat, 0, 0, half * sizeof(float));
1303 NN_CL_FREE(skips[l]);
1305 err |= _upsample_cl(cl, devid, v, us, cw, chh, u->up[i].out_ch);
1306 NN_CL_FREE(v);
1307 err |= dt_opencl_enqueue_copy_buffer_to_buffer(devid, us, cat, 0, half * sizeof(float),
1308 half * sizeof(float));
1309 NN_CL_FREE(us);
1310 cw *= 2;
1311 chh *= 2;
1313 err |= _conv_cl(cl, devid, weights, m->blob, cat, d1, cw, chh, &u->dec1[i], 1, 1, 1);
1314 NN_CL_FREE(cat);
1316 err |= _conv_cl(cl, devid, weights, m->blob, d1, cur, cw, chh, &u->dec2[i], 1, 1, 1);
1317 NN_CL_FREE(d1);
1318 }
1319
1320 // head: raw prediction (no activation) into dev_out
1321 if(err == CL_SUCCESS)
1322 err |= _conv_cl(cl, devid, weights, m->blob, cur, dev_out, width, height, &u->head, 1, 1, 0);
1323
1324cleanup:
1325 for(int l = 0; l < u->depth; l++)
1327 NN_CL_FREE(cur);
1328 NN_CL_FREE(tmp);
1329 NN_CL_FREE(v);
1330 NN_CL_FREE(cat);
1331 NN_CL_FREE(us);
1332 NN_CL_FREE(d1);
1333 return err;
1334#undef NN_CL_ALLOC
1335#undef NN_CL_FREE
1336}
1337
1338int dt_nn_unet_apply_stage_cl(const dt_nn_model_t *m, int stage, dt_nn_cl_t *cl, int devid, cl_mem dev_in,
1339 cl_mem dev_out, int width, int height)
1340{
1341 if(stage == 1)
1342 {
1343 if(!m->has_coarse) return -1;
1344 return _unet_forward_cl(m, &m->coarse, cl, devid, dev_in, dev_out, width, height);
1345 }
1346 return _unet_forward_cl(m, &m->fine, cl, devid, dev_in, dev_out, width, height);
1347}
1348
1349#endif // HAVE_OPENCL
void cleanup(dt_imageio_module_format_t *self)
Definition avif.c:170
#define m
Definition basecurve.c:282
static const float x
const float f
const int t
const float v
const dt_colormatrix_t dt_aligned_pixel_t out
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:385
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:370
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:390
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:375
#define w2
Definition lmmse.c:60
#define w1
Definition lmmse.c:59
#define w3
Definition lmmse.c:61
float *const restrict const size_t k
float *const restrict const size_t const size_t ch
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
size_t size
Definition mipmap_cache.c:3
void dt_nn_cl_destroy(dt_nn_cl_t *cl)
Definition nn_model.c:1098
int dt_nn_unet_apply_stage_cl(const dt_nn_model_t *m, int stage, dt_nn_cl_t *cl, int devid, cl_mem dev_in, cl_mem dev_out, int width, int height)
Definition nn_model.c:1338
int dt_nn_model_anchor(const dt_nn_model_t *m)
Definition nn_model.c:442
#define NN_CL_ALLOC(var, floats)
#define NN_MIN(a, b)
Definition nn_model.c:34
void dt_nn_model_free(dt_nn_model_t *m)
Definition nn_model.c:405
static dt_nn_alloc_f _nn_alloc_fn
Definition nn_model.c:74
#define NN_LEDGER(delta)
float dt_nn_unet_scratch_maxblock_per_px(const dt_nn_model_t *m)
Definition nn_model.c:717
static size_t _unet_peak_floats(const nn_unet_t *u, size_t wh, int cl_variant)
Definition nn_model.c:628
static cl_mem _weights_cl(const dt_nn_model_t *m, int devid)
Definition nn_model.c:1109
int dt_nn_model_in_channels(const dt_nn_model_t *m)
Definition nn_model.c:416
#define NN_OC_BLOCK
Definition nn_model.c:491
static void _err(char *err, size_t err_len, const char *msg)
Definition nn_model.c:108
#define NN_MAX_DEPTH
Definition nn_model.c:33
int dt_nn_model_coarse_in_channels(const dt_nn_model_t *m)
Definition nn_model.c:432
dt_nn_cl_t * dt_nn_cl_create(int program)
Definition nn_model.c:1088
static int _conv_cl(dt_nn_cl_t *cl, int devid, cl_mem weights, const float *blob_base, cl_mem in, cl_mem out, int w, int h, const nn_conv_t *cv, int stride, int pad, int do_gelu)
Definition nn_model.c:1129
static int _wire_conv(const nn_header_t *h, const char *prefix, int out_ch, int in_ch, int k, nn_conv_t *cv, char *err, size_t err_len)
Definition nn_model.c:126
static int _unet_forward(const nn_unet_t *u, const float *in, float *out, int width, int height, int residual_ch)
Definition nn_model.c:881
static int _unet_forward_cl(const dt_nn_model_t *m, const nn_unet_t *u, dt_nn_cl_t *cl, int devid, cl_mem dev_in, cl_mem dev_out, int width, int height)
Definition nn_model.c:1224
size_t dt_nn_unet_scratch_bytes(const dt_nn_model_t *m, int width, int height)
Definition nn_model.c:736
int dt_nn_unet_apply_stage(const dt_nn_model_t *m, int stage, const float *in, float *out, int width, int height, int apply_residual)
Definition nn_model.c:1009
static __DT_CLONE_TARGETS__ void _conv2d_cat2(const nn_conv_t *cv, const float *a, int in_ch_a, const float *b, int w, int h, float *out)
Definition nn_model.c:759
static __DT_CLONE_TARGETS__ void _conv2d(const nn_conv_t *cv, const float *in, int w, int h, int stride, int pad, float *out)
Definition nn_model.c:512
#define NN_MAX_DEVICES
Definition nn_model.c:44
static void dt_nn_model_free_cl(dt_nn_model_t *m)
Definition nn_model.c:394
static void * _nn_alloc(size_t floats, int long_lived)
Definition nn_model.c:93
static dt_nn_free_f _nn_free_fn
Definition nn_model.c:75
static void _nn_free(void *p)
Definition nn_model.c:99
float dt_nn_unet_scratch_per_px(const dt_nn_model_t *m)
Definition nn_model.c:707
dt_nn_model_t * dt_nn_model_load(const char *path, char *err, size_t err_len)
Definition nn_model.c:237
__DT_CLONE_TARGETS__ void dt_nn_upsample_nearest(const float *in, int ch, int w, int h, int factor, float *out)
Definition nn_model.c:1058
static __DT_CLONE_TARGETS__ void _gelu(float *x, size_t n)
Definition nn_model.c:612
int dt_nn_model_coarse_out_channels(const dt_nn_model_t *m)
Definition nn_model.c:437
static int _lcm(int a, int b)
Definition nn_model.c:447
float dt_nn_unet_scratch_per_px_cl(const dt_nn_model_t *m)
Definition nn_model.c:712
int dt_nn_model_alignment(const dt_nn_model_t *m)
Definition nn_model.c:460
int dt_nn_unet_apply(const dt_nn_model_t *m, const float *in, float *out, int width, int height)
Definition nn_model.c:1004
static int _read_net_cfg(JsonObject *cfg, int out_ch_max, int *base, int *depth, int *in_ch, int *out_ch)
Definition nn_model.c:224
__DT_CLONE_TARGETS__ void dt_nn_bin_planes(const float *planes, int pw, int ph, int bin, float *out_rgb, float *out_cnt)
Definition nn_model.c:1022
#define NN_CL_FREE(var)
int dt_nn_model_bin(const dt_nn_model_t *m, const int is_xtrans)
Definition nn_model.c:426
static int _wire_unet(const nn_header_t *h, const char *stage_prefix, int base, int depth, int in_ch, int out_ch, nn_unet_t *u, char *err, size_t err_len)
Definition nn_model.c:179
static float _scratch_per_px(const dt_nn_model_t *m, int cl_variant)
Definition nn_model.c:688
static int _upsample_cl(dt_nn_cl_t *cl, int devid, cl_mem in, cl_mem out, int w, int h, int ch)
Definition nn_model.c:1196
int dt_nn_model_out_channels(const dt_nn_model_t *m)
Definition nn_model.c:421
void dt_nn_set_allocator(dt_nn_alloc_f alloc_fn, dt_nn_free_f free_fn)
Definition nn_model.c:77
void *(* dt_nn_alloc_f)(size_t bytes, int long_lived)
Definition nn_model.h:65
void(* dt_nn_free_f)(void *ptr)
Definition nn_model.h:66
#define DT_NN_FUSION_COARSEST
Definition nn_model.h:98
int dt_opencl_enqueue_kernel_2d(const int dev, const int kernel, const size_t *sizes)
Definition opencl.c:2276
void * dt_opencl_alloc_device_buffer(const int devid, const size_t size)
Definition opencl.c:2692
int dt_opencl_create_kernel(const int prog, const char *name)
Definition opencl.c:2170
int dt_opencl_write_buffer_to_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2460
void dt_opencl_free_kernel(const int kernel)
Definition opencl.c:2213
int dt_opencl_set_kernel_arg(const int dev, const int kernel, const int num, const size_t size, const void *arg)
Definition opencl.c:2267
int dt_opencl_enqueue_kernel_2d_with_local(const int dev, const int kernel, const size_t *sizes, const size_t *local)
Definition opencl.c:2282
int dt_opencl_enqueue_copy_buffer_to_buffer(const int devid, cl_mem src_buffer, cl_mem dst_buffer, size_t srcoffset, size_t dstoffset, size_t size)
Definition opencl.c:2436
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2527
#define ROUNDUPDHT(a, b)
Definition opencl.h:83
#define ROUNDUPDWD(a, b)
Definition opencl.h:82
const float factor
Definition pdf.h:91
const char * name
Definition pdf.h:90
const float r
int kernel_upsample
Definition nn_model.c:1085
int kernel_conv
Definition nn_model.c:1083
int kernel_conv3x3
Definition nn_model.c:1084
dt_pthread_mutex_t cl_lock
Definition nn_model.c:68
cl_mem dev_weights[16]
Definition nn_model.c:67
nn_unet_t coarse
Definition nn_model.c:60
float * blob
Definition nn_model.c:64
size_t blob_floats
Definition nn_model.c:65
nn_unet_t fine
Definition nn_model.c:59
int in_ch
Definition nn_model.c:40
const float * b
Definition nn_model.c:39
int out_ch
Definition nn_model.c:40
const float * w
Definition nn_model.c:38
JsonArray * tensors
Definition nn_model.c:119
size_t payload_size
Definition nn_model.c:121
const float * payload
Definition nn_model.c:120
int depth
Definition nn_model.c:50
int base
Definition nn_model.c:50
nn_conv_t head
Definition nn_model.c:54
int in_ch
Definition nn_model.c:50
int out_ch
Definition nn_model.c:50
nn_conv_t dec2[8]
Definition nn_model.c:53
nn_conv_t bot2
Definition nn_model.c:52
nn_conv_t enc2[8]
Definition nn_model.c:51
nn_conv_t dec1[8]
Definition nn_model.c:53
nn_conv_t down[8]
Definition nn_model.c:51
nn_conv_t enc1[8]
Definition nn_model.c:51
nn_conv_t up[8]
Definition nn_model.c:53
nn_conv_t bot1
Definition nn_model.c:52
#define __DT_CLONE_TARGETS__