Skip to content

finalscale: upscale on OpenCL - #22901

Closed
nicknezis wants to merge 4 commits into
darktable-org:masterfrom
nicknezis:opencl-finalscale-upscale
Closed

nicknezis wants to merge 4 commits into
darktable-org:masterfrom
nicknezis:opencl-finalscale-upscale

Conversation

@nicknezis

Copy link
Copy Markdown
Contributor

Summary

When an export upscales, modify_roi_in() turns OpenCL off for finalscale and process_cl() refuses scale > 1. The pipe then copies the image back to the host for the last step. dt_interpolation_resample_cl() has nothing specific to downscaling: it uses the same resampling plan as the CPU, and for an upscale that plan has fewer taps per pixel. This drops both guards. The CL_INVALID_WORK_GROUP_SIZE fallback in dt_iop_clip_and_zoom_roi_cl() still covers small work groups.

Checklist

  • I have read CONTRIBUTING.md and the coding style.
  • I have not merged master into the topic branch.
  • The pull request is one logical change, and every commit compiles on its own (also built with OpenCL off).
  • I ran the relevant tests.
  • No new strings or preferences.
  • A RELEASE_NOTES.md entry was added (Performance Improvements).

Test instructions

Export of mire1.cr2 with 0001-exposure's xmp, --width 6000 --height 6000 --upscale true --hq true (6000x3994 out) and the integration runner's core options. The table shows the median of 3 runs on an M4 Pro, Apple OpenCL:

export finalscale
CPU 146 ms 73 ms
OpenCL, before 141 ms 82 ms, on the CPU
OpenCL, after 109 ms 35 ms, on the GPU

Float output vs the CPU, same on PoCL and Apple: under 1e-5 everywhere except a few pixels around three demosaic outliers, which the upscale spreads. The outputs before the change have the same outliers. Tiling was forced with --opencl-tiling on PoCL, with the same result.

No integration test upscales, since they all export at 2048 px. Not tested: darkroom zoom above 100%, which goes through dt_iop_clip_and_zoom_cl().

AI assistance

Written with Claude Code. I checked it on PoCL and on my M4 Pro.

modify_roi_in() turned OpenCL off for the module whenever the export
upscaled, and process_cl() refused scale > 1, so an upscaling export
copied the image back to the host for the last step. Nothing in
dt_interpolation_resample_cl() is specific to downscaling: it uses the
same resampling plan as the CPU code, and for an upscale that plan has
fewer taps per pixel. Drop both guards.

The CL_INVALID_WORK_GROUP_SIZE fallback in dt_iop_clip_and_zoom_roi_cl()
still covers devices whose work group is too small for the vertical taps.
@jenshannoschwalm

Copy link
Copy Markdown
Collaborator

No integration test upscales, since they all export at 2048 px. Not tested: darkroom zoom above 100%, which goes through dt_iop_clip_and_zoom_cl().

  1. Do you really propose a PR without testing in darkroom including HQ mode?
  2. Have you tested exports with upscaling a lot and how that behaves that on devices with 4GB of discrete cl_mem?

Without that having done and verified stability especiallly for (2) ... :-(((

_default_process_tiling_cl_roi() multiplied the tile row by the row
pitch in int before casting, so once a tile starts more than 2 GiB
into the host buffer the offset wraps and the readback writes outside
it. A 4x upscale of a 10 MP image to 15632x10408 (2.6 GB as float RGBA)
crashed with OpenCL limited to 1 GB, on the first tile starting at
row 8928. The CPU roi tiling already casts before multiplying.
@jenshannoschwalm

Copy link
Copy Markdown
Collaborator

What's clearly missing is a check for maximum image dimensions and maximum supported image size for the platform/device.

@nicknezis

Copy link
Copy Markdown
Contributor Author

No integration test upscales, since they all export at 2048 px. Not tested: darkroom zoom above 100%, which goes through dt_iop_clip_and_zoom_cl().

  1. Do you really propose a PR without testing in darkroom including HQ mode?
  2. Have you tested exports with upscaling a lot and how that behaves that on devices with 4GB of discrete cl_mem?

Without that having done and verified stability especiallly for (2) ... :-(((

You're right to ask, (2) wasn't safe. I've tested both now (Linux, PoCL).

Darkroom with high quality processing on: fit, 100%, zoomed in to 110% and 121%, and panned at 121%. Finalscale upscales on CL0 in the full pipe, and the screenshots match OpenCL off within 1/255.

Upscaled exports of a 10 MP raw at 2x, 3x and 4x (up to 15632x10408, 2.6 GB as float), with resourcelevel reference (2 GB of cl_mem, about what a 4 GB card gets at "default"), notebook (1 GB) and mini (200 MB):

  • 4x with 1 GB crashed. _default_process_tiling_cl_roi() multiplied the tile row by the pitch in int, so the readback offset wrapped once a tile started past 2 GiB of the output. 3f54f7a fixes it. The CPU roi tiling already casts first.
  • Untiled upscales (2x and 3x with 2 GB) match the CPU export.
  • Tiled upscales don't, but that's not new. The resample ignores the roi offsets, and the tilers can pick an output tile origin that isn't the input origin times the scale (e.g. input row 861 -> output row 2585 at 3x). The tile then shifts by up to 2 px. Master does this on the CPU too: a 3x export with resourcelevel=notebook and OpenCL off gets 3x3 CPU tiles and differs from the untiled export by up to 152/255 along those bands. The GPU tiles here show the same kind of shift.

The fix for that is #22912 (finalscale: place tiles of a scaled export where the image has them). It resamples each tile at its real offset, on CPU and OpenCL, and should go in before this PR. With both applied, tiled GPU upscales at 2 GB, 1 GB and 200 MB match the untiled export, on PoCL and on an M4 Pro with Apple OpenCL.

dt_opencl_image_fits_device() is only asked about roi_in. With finalscale
upscaling on OpenCL, an export wider or taller than the device's maximum
image size (16384 on many devices) has an input that fits, so the module
ran untiled, the output allocation failed and the pipe fell back to the
CPU. Treat such an output as not fitting, so the module is tiled on the
device instead.
const gboolean out_fits =
roi_out->width <= darktable.opencl->dev[pipe->devid].max_image_width
&& roi_out->height <= darktable.opencl->dev[pipe->devid].max_image_height;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also test for the dev max_mem_alloc here, no?

@jenshannoschwalm

Copy link
Copy Markdown
Collaborator

See comment in related PR too.

There is one thing itching me. Many dt users still have graphics cards in the 4-8GB range, have sufficient ram and mostly have images > 30Mpixels. This could end up in OpenCL tiling as technically possible but still much slower as plain processing on CPU. Your benchmarking might be very misleading in such cases.

nicknezis added a commit to nicknezis/darktable that referenced this pull request Nov 1, 2026
A tiled upscale reads every output tile back to the host, which is no
faster than the CPU path and slower over PCIe. Keep process_cl_ready
FALSE for an upscale whose output exceeds the device's max image size
or whose buffers don't fit the available cl_mem, so it runs on the CPU
as before darktable-org#22901.

Mirrors the fit check in finalscale-upscale-opencl (8c67c82).
@nicknezis

Copy link
Copy Markdown
Contributor Author

Agreed, I measured it on an M4 Pro (Apple OpenCL, median of 3, whole export in ms, reference / notebook):

export CPU only master, OpenCL this PR before the fit check
10 MP raw, 2x 248 / 249 353 / 266 208 / 285
10 MP raw, 4x 674 / 708 954 / 935 923 / 956
10 MP raw, 20000 px 1014 / 1077 1493 / 1677 1611 / 1839
26 MP raw, 20000 px 1224 / 1334 1707 / 1923 1715 / 1922

Untiled, the OpenCL upscale is faster (3x: finalscale 163 -> 113 ms). Once it has to tile, it's no faster than master's CPU upscale, and up to 1.4x slower with less memory. Every tile is read back to the host, and over PCIe that would only get worse.

So finalscale now upscales on OpenCL only if the whole upscale fits the device untiled: output within the max image size, and in + out + interpolation within the available cl_mem. Otherwise process_cl_ready stays FALSE and it runs as on master. On PoCL with 2 GB, a 3x export of the 10 MP raw upscales on CL0, while with 1 GB, or at 17000 px wide, it stays on the CPU. All of these match the untiled CPU export (max 9/255).

Something separate from this PR: after a large upscale, colorout tiles on OpenCL on master too, and costs 1.6-2.3x its CPU time (26 MP at 4x: 4.5 s vs 0.7 s). That's why no upscaled export in the table beats CPU only.

Moved into #22912 as one commit, as you suggested there. Closing this one.

@nicknezis nicknezis closed this Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants