Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
opencl-math-accuracy.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/* Measures how accurate each OpenCL device's math library is under each kernel build option,
20 * and prints the `building` line to paste into anselrc for any device that needs one.
21 *
22 * Why this exists: `-cl-unsafe-math-optimizations` (and `-cl-fast-relaxed-math`, which implies
23 * it) permits the driver to substitute a low-precision implementation for any libm function.
24 * Whether that is harmless or catastrophic is a property of the driver, not of the flag, so it
25 * cannot be decided in the source -- it has to be measured on the machine that will run it.
26 * On an Intel HD Graphics P630 the flag makes erf() return exactly 0.0 for |x| < 1e-3, which
27 * silently corrupts the neural raw denoiser's GELU. On NVIDIA the same flag leaves erf() exact
28 * but costs four orders of magnitude on log(). See doc/opencl-math-accuracy.md.
29 *
30 * Build (needs only an OpenCL ICD loader and libm; it does not link against Ansel):
31 *
32 * gcc -O2 -o opencl-math-accuracy tools/opencl-math-accuracy.c -lOpenCL -lm
33 *
34 * If your distribution ships no libOpenCL.so development symlink, link the runtime directly:
35 *
36 * gcc -O2 -o opencl-math-accuracy tools/opencl-math-accuracy.c /lib64/libOpenCL.so.1 -lm
37 *
38 * Run it with no arguments. The CPU row is the control: it is this program's own host
39 * arithmetic, so building the tool at -O2 and again at -O3 -ffast-math also answers "does the
40 * CPU path lose accuracy at higher optimisation levels" for the same expressions.
41 */
42
43#define CL_TARGET_OPENCL_VERSION 300
44#include <CL/cl.h>
45
46#include <ctype.h>
47#include <math.h>
48#include <stdio.h>
49#include <stdlib.h>
50#include <string.h>
51
52// Samples spanning the range a convolutional network's activations actually occupy. The small
53// |x| end is the interesting one: that is where a relaxed erf() implementation flushes to zero
54// and where most activations live.
55#define NSAMPLES 200000
56#define XMIN (-8.0f)
57#define XMAX (8.0f)
58
59// A device is reported as failing if any probed function exceeds this relative error. Correct
60// single precision lands around 1e-7; a genuinely relaxed-but-usable implementation lands
61// around 1e-5. Anything past 1e-4 is a different function, not a rounding difference.
62#define FAIL_THRESHOLD 1e-4
63// Past this the implementation is not merely relaxed, it is returning something else entirely
64// (Intel's relaxed erf() flushes small arguments to exactly 0.0 and scores ~1.0 here).
65#define BROKEN_THRESHOLD 1e-2
66
67static const char *KERNEL_SRC =
68 "__kernel void probe(__global const float *in, __global float *out, const int op)\n"
69 "{\n"
70 " const int i = get_global_id(0);\n"
71 " const float x = in[i];\n"
72 " float r;\n"
73 " switch(op)\n"
74 " {\n"
75 " case 0: r = erf(x); break;\n"
76 " case 1: r = 0.5f * x * (1.0f + erf(x * 0.70710678118654752f)); break;\n"
77 " case 2: r = exp(x); break;\n"
78 " case 3: r = log(fabs(x) + 1.0f); break;\n"
79 " case 4: r = tanh(x); break;\n"
80 " case 5: r = pow(fabs(x) + 1.0f, 2.4f); break;\n"
81 " case 6: r = sqrt(fabs(x)); break;\n"
82 " case 7: r = 1.0f / (fabs(x) + 1.0f); break;\n"
83 " default: r = x; break;\n"
84 " }\n"
85 " out[i] = r;\n"
86 "}\n";
87
89
90static const char *OP_NAME[N_OPS] = { "erf", "GELU", "exp", "log", "tanh", "pow", "sqrt", "divide" };
91
92// Reference in double precision. Kept in its own function so that a fast-math build of this
93// tool relaxes the probe and the reference alike -- which is exactly what we want to see when
94// the tool is used to compare CPU optimisation levels.
95static double reference(const op_t op, const double x)
96{
97 switch(op)
98 {
99 case OP_ERF: return erf(x);
100 case OP_GELU: return 0.5 * x * (1.0 + erf(x * 0.70710678118654752));
101 case OP_EXP: return exp(x);
102 case OP_LOG: return log(fabs(x) + 1.0);
103 case OP_TANH: return tanh(x);
104 case OP_POW: return pow(fabs(x) + 1.0, 2.4);
105 case OP_SQRT: return sqrt(fabs(x));
106 case OP_DIV: return 1.0 / (fabs(x) + 1.0);
107 default: return x;
108 }
109}
110
111// The host control, mirroring the kernel expression by expression.
112static void host_probe(const float *const in, float *const out, const int n, const op_t op)
113{
114 for(int i = 0; i < n; i++)
115 {
116 const float x = in[i];
117 switch(op)
118 {
119 case OP_ERF: out[i] = erff(x); break;
120 case OP_GELU: out[i] = 0.5f * x * (1.0f + erff(x * 0.70710678118654752f)); break;
121 case OP_EXP: out[i] = expf(x); break;
122 case OP_LOG: out[i] = logf(fabsf(x) + 1.0f); break;
123 case OP_TANH: out[i] = tanhf(x); break;
124 case OP_POW: out[i] = powf(fabsf(x) + 1.0f, 2.4f); break;
125 case OP_SQRT: out[i] = sqrtf(fabsf(x)); break;
126 case OP_DIV: out[i] = 1.0f / (fabsf(x) + 1.0f); break;
127 default: out[i] = x; break;
128 }
129 }
130}
131
132/* Candidate build option sets, in the order a reader should think about them: nothing, then one
133 * flag at a time, then the two compound flags, then what Ansel actually ships. `safe` marks the
134 * sets we would be willing to fall back to. */
135typedef struct
136{
137 const char *flags;
138 const char *label;
141
142static const option_set_t OPTION_SETS[] = {
143 { "", "(none)", 0 },
144 { "-cl-mad-enable", "-cl-mad-enable", 0 },
145 { "-cl-no-signed-zeros", "-cl-no-signed-zeros", 0 },
146 { "-cl-denorms-are-zero", "-cl-denorms-are-zero", 0 },
147 { "-cl-finite-math-only", "-cl-finite-math-only", 0 },
148 { "-cl-unsafe-math-optimizations", "-cl-unsafe-math-optimizations", 0 },
149 { "-cl-fast-relaxed-math", "-cl-fast-relaxed-math", 0 },
150 { "-cl-mad-enable -cl-no-signed-zeros", "SAFE SET (mad-enable + no-signed-zeros)", 0 },
151 { "-cl-fast-relaxed-math -cl-no-signed-zeros -cl-unsafe-math-optimizations",
152 "ANSEL LEGACY DEFAULT (fast-relaxed + unsafe)", 1 },
153};
154#define N_OPTION_SETS ((int)(sizeof(OPTION_SETS) / sizeof(OPTION_SETS[0])))
155
156// Same rule as _ascii_str_canonical() in src/common/opencl.c: keep alphanumerics, lowercase.
157// Reproduced here so the tool can print the exact anselrc key without linking against Ansel.
158static void canonical_name(const char *in, char *out, const size_t maxlen)
159{
160 size_t len = 0;
161 for(; *in != '\0' && len + 1 < maxlen; in++)
162 if(isalnum((unsigned char)*in)) out[len++] = (char)tolower((unsigned char)*in);
163 out[len] = '\0';
164}
165
166/* Error relative to the reference, but with the denominator floored at a thousandth of the
167 * function's own peak over the sweep.
168 *
169 * A plain relative error is unusable for GELU: 0.5*x*(1+erf(x/sqrt(2))) is a difference of two
170 * nearly equal numbers once erf saturates to -1, so for x well below zero the result is ~1e-9
171 * built out of cancellation and ANY implementation, including a perfect one, scores a huge
172 * relative error there. Flooring the denominator says "an absolute error this far below the
173 * function's working range is not interesting", which suppresses that artifact.
174 *
175 * It deliberately does NOT suppress the failure we are hunting. erf peaks at 1, so its floor is
176 * 1e-3; Intel's relaxed erf returns exactly 0 where the true value is 1.13e-3, which still
177 * scores ~1.0. Sensitivity is kept exactly where the mechanism lives. */
178static double max_error(const float *const got, const float *const in, const int n, const op_t op)
179{
180 double peak = 0.0;
181 for(int i = 0; i < n; i++)
182 {
183 const double mag = fabs(reference(op, (double)in[i]));
184 if(mag > peak) peak = mag;
185 }
186 const double floor_mag = peak * 1e-3;
187
188 double worst = 0.0;
189 for(int i = 0; i < n; i++)
190 {
191 const double ref = reference(op, (double)in[i]);
192 const double denom = fmax(fabs(ref), floor_mag);
193 if(denom <= 0.0) continue;
194 const double rel = fabs((double)got[i] - ref) / denom;
195 if(rel > worst) worst = rel;
196 }
197 return worst;
198}
199
200static void print_header(void)
201{
202 printf("%-46s", "build options");
203 for(int o = 0; o < N_OPS; o++) printf("%11s", OP_NAME[o]);
204 printf("\n");
205 for(int i = 0; i < 46 + 11 * N_OPS; i++) putchar('-');
206 printf("\n");
207}
208
209int main(void)
210{
211 float *in = (float *)malloc(sizeof(float) * NSAMPLES);
212 float *out = (float *)malloc(sizeof(float) * NSAMPLES);
213 if(!in || !out) { fprintf(stderr, "out of memory\n"); return 1; }
214 for(int i = 0; i < NSAMPLES; i++)
215 in[i] = XMIN + (XMAX - XMIN) * (float)i / (float)(NSAMPLES - 1);
216
217 printf("Ansel OpenCL math accuracy probe\n");
218 printf("max relative error against a double-precision reference, x in [%.0f, %.0f]\n"
219 "a correct single-precision implementation scores ~1e-7; anything above %.0e is a\n"
220 "different function, not a rounding difference\n\n", (double)XMIN, (double)XMAX, FAIL_THRESHOLD);
221
222 // ---- host control -------------------------------------------------------------------
223 printf("================ CPU (this tool's own build) ================\n");
224 print_header();
225 printf("%-46s", "host libm");
226 for(int o = 0; o < N_OPS; o++)
227 {
228 host_probe(in, out, NSAMPLES, (op_t)o);
229 printf("%11.2e", max_error(out, in, NSAMPLES, (op_t)o));
230 }
231 printf("\n\nRebuild this tool at -O2, at -O3, and at -O3 -ffast-math to compare the CPU path\n"
232 "across optimisation levels; the row above reflects whichever was used.\n");
233
234 // ---- devices ------------------------------------------------------------------------
235 cl_platform_id platforms[16];
236 cl_uint n_platforms = 0;
237 if(clGetPlatformIDs(16, platforms, &n_platforms) != CL_SUCCESS || n_platforms == 0)
238 {
239 printf("\nNo OpenCL platform found.\n");
240 free(in); free(out);
241 return 0;
242 }
243
244 int device_index = 0; // Ansel numbers usable devices in enumeration order
245 for(cl_uint p = 0; p < n_platforms; p++)
246 {
247 cl_device_id devices[8];
248 cl_uint n_devices = 0;
249 if(clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_GPU, 8, devices, &n_devices) != CL_SUCCESS) continue;
250
251 for(cl_uint d = 0; d < n_devices; d++)
252 {
253 char name[256] = { 0 };
254 clGetDeviceInfo(devices[d], CL_DEVICE_NAME, sizeof(name), name, NULL);
255
256 cl_int err = CL_SUCCESS;
257 cl_context ctx = clCreateContext(NULL, 1, &devices[d], NULL, NULL, &err);
258 if(err != CL_SUCCESS) { printf("\n%s: cannot create a context (%d), skipping\n", name, err); continue; }
259 cl_command_queue queue = clCreateCommandQueueWithProperties(ctx, devices[d], NULL, &err);
260 cl_mem d_in = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
261 sizeof(float) * NSAMPLES, in, &err);
262 cl_mem d_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, sizeof(float) * NSAMPLES, NULL, &err);
263
264 printf("\n================ device %d: %s ================\n", device_index, name);
265 print_header();
266
267 int best_safe = -1; // richest option set that stayed accurate
268 int default_fails = 0;
269 for(int f = 0; f < N_OPTION_SETS; f++)
270 {
271 cl_program program = clCreateProgramWithSource(ctx, 1, &KERNEL_SRC, NULL, &err);
272 if(clBuildProgram(program, 1, &devices[d], OPTION_SETS[f].flags, NULL, NULL) != CL_SUCCESS)
273 {
274 printf("%-46s build failed\n", OPTION_SETS[f].label);
275 clReleaseProgram(program);
276 continue;
277 }
278 cl_kernel kernel = clCreateKernel(program, "probe", &err);
279
280 printf("%-46s", OPTION_SETS[f].label);
281 int fails = 0, broken = 0;
282 for(int o = 0; o < N_OPS; o++)
283 {
284 clSetKernelArg(kernel, 0, sizeof(cl_mem), &d_in);
285 clSetKernelArg(kernel, 1, sizeof(cl_mem), &d_out);
286 clSetKernelArg(kernel, 2, sizeof(int), &o);
287 size_t global = NSAMPLES;
288 clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global, NULL, 0, NULL, NULL);
289 clFinish(queue);
290 clEnqueueReadBuffer(queue, d_out, CL_TRUE, 0, sizeof(float) * NSAMPLES, out, 0, NULL, NULL);
291
292 const double rel = max_error(out, in, NSAMPLES, (op_t)o);
293 if(rel > FAIL_THRESHOLD) fails++;
294 if(rel > BROKEN_THRESHOLD) broken++;
295 printf("%11.2e", rel);
296 }
297 printf("%s\n", broken ? " <-- BROKEN" : (fails ? " <-- degraded" : ""));
298
299 if(!fails) best_safe = f;
300 if(OPTION_SETS[f].is_ansel_default && broken) default_fails = 1;
301
302 clReleaseKernel(kernel);
303 clReleaseProgram(program);
304 }
305
306 char cname[256];
307 canonical_name(name, cname, sizeof(cname));
308 printf("\n");
309 if(default_fails)
310 {
311 printf("VERDICT: this device's math library is NOT accurate under Ansel's legacy default.\n");
312 printf("Put this in anselrc (it is per device, and Ansel rewrites it only if absent):\n\n");
313 printf(" cldevice_v4/%d/%s/building=%s\n\n", device_index, cname,
314 best_safe >= 0 ? OPTION_SETS[best_safe].flags : "-cl-mad-enable -cl-no-signed-zeros");
315 printf("then delete %s's cached kernels so they rebuild:\n", name);
316 printf(" rm -rf ~/.cache/ansel/cached_kernels_for_*\n");
317 }
318 else
319 {
320 printf("VERDICT: accurate under every option set probed; no anselrc change needed.\n");
321 printf("Current key would be: cldevice_v4/%d/%s/building=...\n", device_index, cname);
322 }
323
324 clReleaseMemObject(d_in);
325 clReleaseMemObject(d_out);
326 clReleaseCommandQueue(queue);
327 clReleaseContext(ctx);
328 device_index++;
329 }
330 }
331
332 free(in);
333 free(out);
334 return 0;
335}
static const float x
const float f
const dt_colormatrix_t dt_aligned_pixel_t out
static float kernel(const float *x, const float *y)
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
#define XMAX
static void host_probe(const float *const in, float *const out, const int n, const op_t op)
#define N_OPTION_SETS
static void canonical_name(const char *in, char *out, const size_t maxlen)
#define XMIN
static const option_set_t OPTION_SETS[]
#define NSAMPLES
int main(void)
static double reference(const op_t op, const double x)
static double max_error(const float *const got, const float *const in, const int n, const op_t op)
static const char * KERNEL_SRC
static const char * OP_NAME[N_OPS]
#define FAIL_THRESHOLD
static void print_header(void)
#define BROKEN_THRESHOLD
const char * name
Definition pdf.h:90
GQueue * log
Definition supervisor.c:121