CaptureMoment 0.1.0
Free and open-source non-destructive photo editor
Loading...
Searching...
No Matches
benchmarking_backend_decider.cpp
Go to the documentation of this file.
1
10#include <spdlog/spdlog.h>
11#include "Halide.h"
12#include <array>
13#include <string>
14
16
17// ============================================================
18// Helper Functions
19// ============================================================
20
26static std::string feature_to_string(Halide::Target::Feature feature)
27{
28 switch (feature)
29 {
30 case Halide::Target::CUDA: return "CUDA";
31 case Halide::Target::OpenCL: return "OpenCL";
32 case Halide::Target::Vulkan: return "Vulkan";
33 case Halide::Target::Metal: return "Metal";
34 case Halide::Target::D3D12Compute: return "DirectX12";
35 default: return "Unknown";
36 }
37}
38
44static bool should_test_feature(Halide::Target::Feature feature)
45{
46 switch (feature)
47 {
48 case Halide::Target::Metal:
49#ifdef __APPLE__
50 return true;
51#else
52 spdlog::debug("[BackendDecider] Metal skipped - not available on this platform");
53 return false;
54#endif
55
56 case Halide::Target::D3D12Compute:
57#ifdef _WIN32
58 return true;
59#else
60 spdlog::debug("[BackendDecider] DirectX12 skipped - not available on this platform");
61 return false;
62#endif
63
64 case Halide::Target::CUDA:
65 case Halide::Target::OpenCL:
66 case Halide::Target::Vulkan:
67 return true;
68
69 default:
70 return false;
71 }
72}
73
83static Halide::Buffer<float> create_fresh_test_buffer(int width, int height, int channels)
84{
85 Halide::Buffer<float> buffer(width, height, channels);
86
87 for (size_t c = 0; c < channels; ++c)
88 {
89 for (size_t y = 0; y < height; ++y)
90 {
91 for (size_t x = 0; x < width; ++x)
92 {
93 buffer(x, y, c) = static_cast<float>((x + y + c) % 256) / 255.0f;
94 }
95 }
96 }
97
98 return buffer;
99}
100
109static Halide::Func create_benchmark_pipeline(const Halide::Buffer<float>& input,
110 Halide::Var& x, Halide::Var& y, Halide::Var& c)
111{
112 Halide::Func pipeline("benchmark_pipeline");
113 pipeline(x, y, c) = input(x, y, c) * 1.1f + 0.05f;
114 return pipeline;
115}
116
123static bool try_compile_jit(Halide::Func& pipeline, const Halide::Target& target)
124{
125 try
126 {
127 pipeline.compile_jit(target);
128 return true;
129 }
130 catch (...)
131 {
132 return false;
133 }
134}
135
137{
138 spdlog::info("[BenchmarkingBackendDecider::decide]: Starting backend performance benchmark...");
139
140 // --- Phase 1: Log Host Target Info ---
141 Halide::Target host_target { Halide::get_host_target() };
142 spdlog::info("[BenchmarkingBackendDecider::decide]: Host target: {}", host_target.to_string());
143
144 // --- Phase 2: CPU Benchmark ---
145 auto cpu_time { benchmark_cpu() };
146
147 if (cpu_time == std::chrono::milliseconds::max())
148 {
149 spdlog::warn("[BenchmarkingBackendDecider::decide]: CPU benchmark failed completely. Defaulting to CPU.");
150 m_winning_target = host_target;
152 }
153 spdlog::info("[BenchmarkingBackendDecider::decide]: CPU Baseline: {} ms", cpu_time.count());
154
155 // --- Phase 3: Detect & Benchmark GPUs (Strict Priority Order) ---
156
157 std::array<std::pair<Halide::Target::Feature, std::string>, 5> gpu_priorities =
158 { {
159 {Halide::Target::CUDA, "CUDA"},
160 {Halide::Target::D3D12Compute, "DirectX12"},
161 {Halide::Target::Metal, "Metal"},
162 {Halide::Target::Vulkan, "Vulkan"},
163 {Halide::Target::OpenCL, "OpenCL"}
164 } };
165
166 std::optional<std::chrono::milliseconds> best_gpu_time;
167 std::string best_gpu_name { "None" };
168 Halide::Target::Feature best_gpu_feature { Halide::Target::OpenCL };
169
170 for (const auto& [feature, name] : gpu_priorities)
171 {
172 // Skip backends that are not available on this platform
173 if (!should_test_feature(feature))
174 {
175 continue;
176 }
177
178 spdlog::info("[BenchmarkingBackendDecider::decide]: Testing {} backend...", name);
179
180 // CRITICAL: Create a FRESH buffer for each backend test
181 // Reusing buffers across different GPU interfaces causes:
182 // "halide_copy_to_device does not support switching interfaces"
183 Halide::Buffer<float> fresh_buffer { create_fresh_test_buffer(
184 k_benchmark_width, k_benchmark_height, k_benchmark_channels) };
185
186 auto result = benchmark_gpu_feature(feature, fresh_buffer);
187
188 if (result.has_value())
189 {
190 if (!best_gpu_time.has_value() || result < best_gpu_time)
191 {
192 best_gpu_time = result;
193 best_gpu_name = name;
194 best_gpu_feature = feature;
195 spdlog::info("[BenchmarkingBackendDecider::decide]: {} benchmarked in {} ms (Current Best)",
196 name, result.value().count());
197 }
198 else
199 {
200 spdlog::debug("[BenchmarkingBackendDecider::decide]: {} benchmarked in {} ms (Slower than {})",
201 name, result.value().count(), best_gpu_name);
202 }
203 }
204 }
205
206 // --- Phase 4: Final Decision ---
207 if (!best_gpu_time.has_value())
208 {
209 spdlog::info("[BenchmarkingBackendDecider::decide]: No GPU benchmark succeeded. Using CPU backend.");
210 m_winning_target = host_target;
212 }
213
214 spdlog::info("[BenchmarkingBackendDecider::decide]: Best GPU: {} at {} ms", best_gpu_name, best_gpu_time.value().count());
215
216 long long threshold_ms = static_cast<long long>(cpu_time.count() * k_gpu_advantage_threshold);
217
218 if (best_gpu_time.value().count() < threshold_ms)
219 {
220 spdlog::info("[BenchmarkingBackendDecider::decide]: GPU ({} ms) is significantly faster than CPU ({} ms). SELECTING GPU.",
221 best_gpu_time.value().count(), cpu_time.count());
222
223 m_winning_target = host_target;
224 m_winning_target.set_feature(best_gpu_feature);
225
227 }
228 else
229 {
230 spdlog::info("[BenchmarkingBackendDecider::decide]: CPU ({} ms) is comparable or faster than GPU ({} ms). SELECTING CPU.",
231 cpu_time.count(), best_gpu_time.value().count());
232 m_winning_target = host_target;
234 }
235}
236
237std::chrono::milliseconds BenchmarkingBackendDecider::benchmark_cpu() const
238{
239 try
240 {
241 Halide::Var x, y, c;
242 Halide::Buffer<float> buffer { create_fresh_test_buffer(
243 k_benchmark_width, k_benchmark_height, k_benchmark_channels) };
244
245 auto pipeline = create_benchmark_pipeline(buffer, x, y, c);
246
247 auto start = std::chrono::high_resolution_clock::now();
248 pipeline.realize(buffer);
249 auto end = std::chrono::high_resolution_clock::now();
250
251 return std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
252 }
253 catch (const std::exception& e)
254 {
255 spdlog::error("[BenchmarkingBackendDecider::decide]: CPU Benchmark Exception: {}", e.what());
256 return std::chrono::milliseconds::max();
257 }
258}
259
260std::optional<std::chrono::milliseconds>
261BenchmarkingBackendDecider::benchmark_gpu_feature(Halide::Target::Feature feature,
262 const Halide::Buffer<float>& ref_buffer) const
263{
264 try
265 {
266 // Step 1: Create target with GPU feature
267 Halide::Target target { Halide::get_host_target() };
268 target.set_feature(feature);
269
270 // Step 2: Create pipeline
271 // Note: ref_buffer is already fresh (created in decide() for each backend)
272 Halide::Buffer<float> work_buffer { ref_buffer };
273 Halide::Var x, y, c, xo, yo, xi, yi;
274 auto pipeline = create_benchmark_pipeline(work_buffer, x, y, c);
275 pipeline.gpu_tile(x, y, xo, yo, xi, yi, 16, 16);
276
277 // Step 3: Try JIT compilation FIRST
278 if (!try_compile_jit(pipeline, target))
279 {
280 spdlog::debug("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} - JIT compilation failed (not supported by Halide build)",
281 feature_to_string(feature));
282 return std::nullopt;
283 }
284
285 spdlog::debug("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} - JIT compilation successful", feature_to_string(feature));
286
287 // Step 4: Copy to device
288 // The buffer is fresh, so no interface conflict can occur
289 work_buffer.set_host_dirty();
290 int copy_res = work_buffer.copy_to_device(target);
291 if (copy_res != 0)
292 {
293 spdlog::debug("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} - copy_to_device failed (err: {}), no GPU device available",
294 feature_to_string(feature), copy_res);
295 return std::nullopt;
296 }
297
298 // Step 5: Benchmark execution
299 auto start = std::chrono::high_resolution_clock::now();
300
301 Halide::Buffer<float> result;
302 try
303 {
304 result = pipeline.realize(
305 {k_benchmark_width, k_benchmark_height, k_benchmark_channels}, target);
306 }
307 catch (const Halide::Error& e)
308 {
309 spdlog::debug("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} - realize failed: {}", feature_to_string(feature), e.what());
310 return std::nullopt;
311 }
312
313 auto end = std::chrono::high_resolution_clock::now();
314
315 auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
316 spdlog::info("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} benchmark success: {} ms",
317 feature_to_string(feature), duration.count());
318 return duration;
319 }
320 catch (const Halide::Error& e)
321 {
322 spdlog::debug("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} Halide error: {}", feature_to_string(feature), e.what());
323 return std::nullopt;
324 }
325 catch (const std::exception& e)
326 {
327 spdlog::debug("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} exception: {}", feature_to_string(feature), e.what());
328 return std::nullopt;
329 }
330 catch (...)
331 {
332 spdlog::debug("[BenchmarkingBackendDecider::benchmark_gpu_feature]: {} - unknown exception prevented", feature_to_string(feature));
333 return std::nullopt;
334 }
335}
336
337} // namespace CaptureMoment::Core::ImageProcessing
Concrete implementation of IBackendDecider that chooses backend based on performance benchmarks.
Common::MemoryType decide() override
Decides the optimal memory type (CPU or GPU) by running benchmarks.
MemoryType
Enum representing the memory type where image data is stored.
Definition memory_type.h:24
@ GPU_MEMORY
Data is stored in GPU memory.
@ CPU_RAM
Data is stored in main CPU RAM.
static Halide::Func create_benchmark_pipeline(const Halide::Buffer< float > &input, Halide::Var &x, Halide::Var &y, Halide::Var &c)
Creates a simple brightness adjustment pipeline for benchmarking.
static bool should_test_feature(Halide::Target::Feature feature)
Checks if a GPU feature should be tested on the current platform.
static Halide::Buffer< float > create_fresh_test_buffer(int width, int height, int channels)
Creates a fresh test buffer filled with gradient data.
static std::string feature_to_string(Halide::Target::Feature feature)
Converts a Halide Target::Feature to a human-readable string.
static bool try_compile_jit(Halide::Func &pipeline, const Halide::Target &target)
Attempts to compile a pipeline for a GPU target.
CaptureMoment — footer