Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
ansel-nn-parity/main.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel.
3 Copyright (C) 2026 Ansel developers.
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
19/* Three-way parity check for the .anselnn executor: torch (the golden fixture) against the CPU
20 * path and against the OpenCL path, on the same input.
21 *
22 * tests/nn_model_test.c already does torch-vs-CPU and builds standalone, without Ansel.
23 * The GPU side cannot: dt_nn_cl_create() wants a compiled program number, which only exists
24 * once dt_opencl_init() has run, so this half has to live in an application that links
25 * lib_ansel. It follows ansel-cltest's pattern of appending its own arguments to force the
26 * subsystem up.
27 *
28 * Usage:
29 * ansel-nn-parity <model.anselnn> <fixture-dir> [N] [--core <ansel options>]
30 *
31 * Fixtures come from scripts/make_fixture.py in the ansel-denoise training repo, and must be
32 * regenerated whenever the model files change -- they pin a model_sha256, and a stale fixture
33 * reports a large error for reasons that have nothing to do with the code under test.
34 */
35
36#include "darktable.h"
37#include "common/nn_model.h"
38#include "common/opencl.h"
39#include "system/macros.h"
40#include "system/mem_alloc.h"
41#include "develop/pixelpipe.h"
42
43#include <glib/gstdio.h>
44#include <json-glib/json-glib.h>
45
46#ifdef _WIN32
47#include "win/main_wrapper.h"
48#endif
49
50#include <math.h>
51#include <stdio.h>
52#include <stdlib.h>
53#include <string.h>
54
55#define NN_PROGRAM 39 // rawdenoiseai.cl, from data/kernels/programs.conf
56
57static float *read_f32(const char *dir, const char *name, const size_t count)
58{
59 char path[4096];
60 snprintf(path, sizeof(path), "%s/%s", dir, name);
61 FILE *f = g_fopen(path, "rb");
62 if(!f) { fprintf(stderr, "cannot open %s\n", path); return NULL; }
63 float *buf = (float *)malloc(sizeof(float) * count);
64 if(!buf) { fclose(f); return NULL; }
65 const size_t got = fread(buf, sizeof(float), count, f);
66 fclose(f);
67 if(got != count)
68 {
69 fprintf(stderr, "%s: expected %zu floats, got %zu -- stale or mismatched fixture\n", path, count, got);
70 dt_free(buf);
71 return NULL;
72 }
73 return buf;
74}
75
76/* Refuse to run a fixture against a model it was not generated from. Without this the
77 * comparison still runs and reports a large error that reads like a broken executor -- the
78 * failure mode that once made six good models look like six regressions. */
79static int check_model_hash(const char *dir, const char *model_path)
80{
81 char path[4096];
82 snprintf(path, sizeof(path), "%s/fixture-meta.json", dir);
83 JsonParser *parser = json_parser_new();
84 int rc = 0;
85 if(json_parser_load_from_file(parser, path, NULL))
86 {
87 JsonObject *root = json_node_get_object(json_parser_get_root(parser));
88 if(json_object_has_member(root, "model_sha256"))
89 {
90 const char *want = json_object_get_string_member(root, "model_sha256");
91 gchar *blob = NULL;
92 gsize len = 0;
93 if(g_file_get_contents(model_path, &blob, &len, NULL))
94 {
95 gchar *got = g_compute_checksum_for_data(G_CHECKSUM_SHA256, (const guchar *)blob, len);
96 if(g_strcmp0(got, want))
97 {
98 fprintf(stderr, "FAIL: fixture was generated from a different model\n pins %s\n got %s\n",
99 want, got);
100 rc = 1;
101 }
102 g_free(got);
103 g_free(blob);
104 }
105 }
106 }
107 g_object_unref(parser);
108 return rc;
109}
110
111static double max_abs_diff(const float *a, const float *b, const size_t n, size_t *where)
112{
113 double worst = 0.0;
114 for(size_t i = 0; i < n; i++)
115 {
116 const double d = fabs((double)a[i] - (double)b[i]);
117 if(d > worst) { worst = d; if(where) *where = i; }
118 }
119 return worst;
120}
121
122int main(int argc, char *arg[])
123{
124 if(argc < 3)
125 {
126 fprintf(stderr, "usage: %s <model.anselnn> <fixture-dir> [N]\n", arg[0]);
127 return 1;
128 }
129 const char *model_path = arg[1];
130 const char *fixture_dir = arg[2];
131 const int n = (argc > 3 && arg[3][0] != '-') ? atoi(arg[3]) : 96;
132
133 int result = 1;
134 float *in = NULL, *expected = NULL, *cpu = NULL, *gpu = NULL, *head = NULL;
135 dt_nn_model_t *model = NULL;
136 dt_nn_cl_t *nncl = NULL;
137 void *dev_in = NULL, *dev_out = NULL;
138
139 /* dt_init() treats every positional argument as an image to import and prints its usage if it
140 * does not recognise one, so it must never see ours. Everything after `--core` is Ansel's,
141 * everything before it is this tool's; we hand dt_init argv[0], Ansel's share, and the
142 * options that force the subsystem up. */
143 int core_at = argc;
144 for(int i = 1; i < argc; i++)
145 if(!strcmp(arg[i], "--core")) { core_at = i; break; }
146
147 char *m_arg[] = { "--library", ":memory:" };
148 const int m_argc = (int)(sizeof(m_arg) / sizeof(m_arg[0]));
149 const int passthrough = (core_at < argc) ? (argc - core_at - 1) : 0;
150 char **argv = (char **)malloc(sizeof(char *) * (size_t)(1 + passthrough + m_argc));
151 if(IS_NULL_PTR(argv)) return 1;
152 int dt_argc = 0;
153 argv[dt_argc++] = arg[0];
154 for(int i = 0; i < passthrough; i++) argv[dt_argc++] = arg[core_at + 1 + i];
155 for(int i = 0; i < m_argc; i++) argv[dt_argc++] = m_arg[i];
156 if(dt_init(dt_argc, argv, FALSE, FALSE)) { dt_free(argv); return 1; }
157
158 if(check_model_hash(fixture_dir, model_path)) goto done;
159
160 char err[256] = { 0 };
161 model = dt_nn_model_load(model_path, err, sizeof(err));
162 if(!model) { fprintf(stderr, "cannot load %s: %s\n", model_path, err); goto done; }
163
164 const int in_ch = dt_nn_model_in_channels(model);
165 const int out_ch = dt_nn_model_out_channels(model);
166 const size_t plane = (size_t)n * n;
167
168 in = read_f32(fixture_dir, "fixture-input.f32", plane * in_ch);
169 expected = read_f32(fixture_dir, "fixture-expected.f32", plane * out_ch);
170 cpu = (float *)malloc(sizeof(float) * plane * out_ch);
171 gpu = (float *)malloc(sizeof(float) * plane * out_ch);
172 head = (float *)malloc(sizeof(float) * plane * out_ch);
173 if(!in || !expected || !cpu || !gpu || !head) goto done;
174
175 printf("model %s: in=%d out=%d, fixture %dx%d\n", model_path, in_ch, out_ch, n, n);
176
177 // ---- CPU: stage 0 with the residual applied, matching what the module renders ----------
178 if(dt_nn_unet_apply_stage(model, 0, in, cpu, n, n, 1))
179 {
180 fprintf(stderr, "CPU stage failed\n");
181 goto done;
182 }
183 size_t w1 = 0;
184 const double cpu_err = max_abs_diff(cpu, expected, plane * out_ch, &w1);
185 printf(" torch vs CPU : max abs err %.3g (at %zu: %.6f vs %.6f)\n",
186 cpu_err, w1, cpu[w1], expected[w1]);
187
188 // ---- OpenCL: same stage, then apply the residual host-side ------------------------------
190 if(devid < 0)
191 {
192 printf(" torch vs OpenCL : SKIPPED (no OpenCL device available)\n");
193 result = (cpu_err > 2e-4) ? 1 : 0;
194 goto done;
195 }
196 printf(" using OpenCL device %d\n", devid);
197
199 if(!nncl) { fprintf(stderr, "dt_nn_cl_create failed\n"); dt_opencl_release_device(devid); goto done; }
200
201 dev_in = dt_opencl_alloc_device_buffer(devid, sizeof(float) * plane * in_ch);
202 dev_out = dt_opencl_alloc_device_buffer(devid, sizeof(float) * plane * out_ch);
203 if(!dev_in || !dev_out) { fprintf(stderr, "device allocation failed\n"); dt_opencl_release_device(devid); goto done; }
204
205 if(dt_opencl_write_buffer_to_device(devid, in, dev_in, 0, sizeof(float) * plane * in_ch, CL_TRUE) != CL_SUCCESS)
206 {
207 fprintf(stderr, "upload failed\n"); dt_opencl_release_device(devid); goto done;
208 }
209
210 const int rc = dt_nn_unet_apply_stage_cl(model, 0, nncl, devid, dev_in, dev_out, n, n);
211 if(rc != CL_SUCCESS)
212 {
213 fprintf(stderr, "dt_nn_unet_apply_stage_cl failed (%d)\n", rc);
215 goto done;
216 }
217
218 if(dt_opencl_read_buffer_from_device(devid, head, dev_out, 0, sizeof(float) * plane * out_ch, CL_TRUE) != CL_SUCCESS)
219 {
220 fprintf(stderr, "readback failed\n"); dt_opencl_release_device(devid); goto done;
221 }
223
224 /* The CL entry point writes the RAW head -- the predicted noise -- by contract, while the CPU
225 * twin above was asked to apply the residual. Close the gap here rather than by asking for a
226 * different CPU call, so both sides are compared against the same torch output. */
227 for(size_t i = 0; i < plane * out_ch; i++) gpu[i] = in[i] - head[i];
228
229 size_t w2 = 0, w3 = 0;
230 const double gpu_err = max_abs_diff(gpu, expected, plane * out_ch, &w2);
231 const double dev_err = max_abs_diff(gpu, cpu, plane * out_ch, &w3);
232 printf(" torch vs OpenCL : max abs err %.3g (at %zu: %.6f vs %.6f)\n",
233 gpu_err, w2, gpu[w2], expected[w2]);
234 printf(" CPU vs OpenCL : max abs err %.3g\n", dev_err);
235
236 {
237 const double tol = 2e-4;
238 const int pass = (cpu_err <= tol) && (gpu_err <= tol);
239 printf(" %s (tolerance %.0e)\n", pass ? "PASS" : "FAIL", tol);
240 result = pass ? 0 : 1;
241 }
242
243done:
244 if(dev_in) dt_opencl_release_mem_object(dev_in);
245 if(dev_out) dt_opencl_release_mem_object(dev_out);
246 if(nncl) dt_nn_cl_destroy(nncl);
248 dt_free(in); dt_free(expected); dt_free(cpu); dt_free(gpu); dt_free(head);
249 dt_cleanup();
250 dt_free(argv);
251 return result;
252}
253
254// clang-format off
255// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
256// vim: shiftwidth=2 expandtab tabstop=2 cindent
257// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
258// clang-format on
static int check_model_hash(const char *dir, const char *model_path)
static double max_abs_diff(const float *a, const float *b, const size_t n, size_t *where)
#define NN_PROGRAM
static float * read_f32(const char *dir, const char *name, const size_t count)
#define FALSE
Definition ashift_lsd.c:158
const float f
void dt_cleanup()
Definition darktable.c:1864
int dt_init(int argc, char *argv[], const gboolean init_gui, const gboolean load_data)
Definition darktable.c:885
const char * model
#define w2
Definition lmmse.c:60
#define w1
Definition lmmse.c:59
#define w3
Definition lmmse.c:61
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
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
void dt_nn_model_free(dt_nn_model_t *m)
Definition nn_model.c:405
int dt_nn_model_in_channels(const dt_nn_model_t *m)
Definition nn_model.c:416
dt_nn_cl_t * dt_nn_cl_create(int program)
Definition nn_model.c:1088
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
dt_nn_model_t * dt_nn_model_load(const char *path, char *err, size_t err_len)
Definition nn_model.c:237
int dt_nn_model_out_channels(const dt_nn_model_t *m)
Definition nn_model.c:421
void * dt_opencl_alloc_device_buffer(const int devid, const size_t size)
Definition opencl.c:2970
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:2738
int dt_opencl_read_buffer_from_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2727
int dt_opencl_reserve_device_for_pipe(const int pipetype)
Reserve a device for a pipe run: choose a free one by the pipe's priority list and take its lock....
Definition opencl.c:1887
void dt_opencl_release_device(const int devid)
Release a device reserved by either reserve function.
Definition opencl.c:1972
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2805
const char * name
Definition pdf.h:90
@ DT_DEV_PIXELPIPE_EXPORT
Definition pixelpipe.h:42
int main()
Definition prova.c:47