Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
gpu_entry_point.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Shared GPU entry-point runner for the q3_gpu* binaries.
5 *
6 * The four GPU executables (Metal/Accelerate, CUDA, HIP, SYCL) differ only in
7 * which backend factories they link and in their banner/usage labels. Each
8 * main_gpu*.cpp builds a GPUEntryPointDescriptor and delegates to
9 * run_gpu_entry_point(), which owns the shared flow:
10 *
11 * parse CLI → init logging → create + validate backends → run the input's
12 * flavor on serial iterators with the GPU Davidson injected → write
13 * results → print profiling summary.
14 *
15 * Backend factories are passed as std::function so this header stays free of
16 * vendor headers; the factories are resolved at link time in each binary.
17 */
18
19#include <cmath>
20#include <cstdlib>
21#include <functional>
22#include <memory>
23#include <stdexcept>
24#include <string>
25#include <vector>
26
28#include "io/cli.hpp"
30#include "io/launcher.hpp"
31#include "io/logger.hpp"
43
44namespace q3 {
45namespace cli {
46
47/**
48 * Everything that distinguishes one GPU binary from another.
49 */
51 /// Banner label, e.g. "CUDA GPU" → "BEGIN (CUDA GPU): <timestamp>".
52 std::string backend_label;
53 /// Binary name for the usage line, e.g. "q3_gpu_cuda".
54 std::string binary_name;
55 /// Vendor BLAS name for the validation log line, e.g. "cuBLAS". Optional.
56 std::string blas_library_name;
57 /// Vendor eigensolver name for the validation log line, e.g. "cuSOLVER". Optional.
59 /// Human-readable description of the mixed-precision mode, e.g. "mixed (FP32 FFT via cuFFT)".
60 std::string mixed_precision_description = "mixed (FP32 FFT)";
61
62 using DeviceBackendPointer = std::shared_ptr<execution::gpu::device::GPUDeviceBackend>;
63
64 std::function<std::unique_ptr<execution::gpu::blas::GPUBLAS>(DeviceBackendPointer)> create_blas;
65 std::function<std::unique_ptr<execution::gpu::lapack::GPUEigensolver>(DeviceBackendPointer)>
67 /**
68 * Optional: backends without a native GPU FFT (Metal) leave this empty and
69 * the FP64 path falls back to the PlanewaveBasis FFTW grid.
70 */
71 std::function<std::unique_ptr<gpu::GPUFFTGrid>(DeviceBackendPointer)> create_fft_grid;
72};
73
74// ─── Backend validation ───────────────────────────────────────────────────────
75
77 log::info("GPU Device: {}", device_backend.device_name());
78 log::info("GPU Memory: {:.1f} GB",
79 static_cast<double>(device_backend.total_memory_bytes()) /
80 (1024.0 * 1024.0 * 1024.0));
81 log::info("GPU FP64 support: {}", device_backend.supports_double_precision() ? "yes" : "no");
82
83 // Validate allocate/deallocate cycle
84 void* test_pointer = device_backend.allocate_unified(1024);
85 device_backend.deallocate(test_pointer);
86 log::info("GPU memory allocate/deallocate: OK");
87}
88
90 const std::string& blas_library_name) {
91 /**
92 * Quick BLAS sanity: compute dot product of [1+0i, 0+1i] with itself
93 * Expected: conj([1,i]) · [1,i] = 1*1 + (-i)*i = 1 + 1 = 2
94 */
95 std::vector<Complex> test_vector = {Complex(1.0, 0.0), Complex(0.0, 1.0)};
96 Complex dot_result = gpu_blas.dot_product(2, test_vector.data(), test_vector.data());
97 double error = std::abs(dot_result.real() - 2.0) + std::abs(dot_result.imag());
98 if (error > 1.0e-14) {
99 throw std::runtime_error("GPU BLAS dot product validation failed");
100 }
101 if (blas_library_name.empty()) {
102 log::info("GPU BLAS validation: OK");
103 } else {
104 log::info("GPU BLAS ({}) validation: OK", blas_library_name);
105 }
106}
107
109 const std::string& eigensolver_library_name) {
110 // Solve 2x2 Hermitian eigenvalue problem: A = [2,1; 1,2] -> eigenvalues = [1,3]
111 std::vector<Complex> matrix = {Complex(2, 0), Complex(1, 0), Complex(1, 0), Complex(2, 0)};
112 std::vector<Real> eigenvalues(2);
113 gpu_eigensolver.hermitian_eigenvalue_solve(2, matrix.data(), 2, eigenvalues.data());
114 double eigen_error = std::abs(eigenvalues[0] - 1.0) + std::abs(eigenvalues[1] - 3.0);
115 if (eigen_error > 1.0e-12) {
116 throw std::runtime_error("GPU eigensolver validation failed");
117 }
118 if (eigensolver_library_name.empty()) {
119 log::info("GPU eigensolver validation: OK");
120 } else {
121 log::info("GPU eigensolver ({}) validation: OK", eigensolver_library_name);
122 }
123}
124
125// ─── Shared entry point ───────────────────────────────────────────────────────
126
127/**
128 * Run the full GPU entry-point flow for the backend described by
129 * 'descriptor'. Returns the process exit code.
130 */
131inline int run_gpu_entry_point(int argc, char* argv[], const GPUEntryPointDescriptor& descriptor) {
133 auto options = parse_options(argc, argv, descriptor.binary_name);
134
135 q3::log::init(options.log_file);
136
137 /**
138 * Answered before the input is resolved, so `--version` works in a
139 * directory holding no input.
140 */
141 if (options.print_version) {
143 return 0;
144 }
145
146 /**
147 * One process, or none. There is no multi-process mode here: the GPU path
148 * builds a serial iterator, so every task of a multi-task step would set up
149 * the whole problem on its own device, converge it, and print the same
150 * answer -- which is what `srun -n 2 q3_gpu_cuda` did until 2026-09-11,
151 * silently, for twice the allocation and 2% more wall time (camsai/q3#15).
152 *
153 * Refused after --version so `srun q3_gpu_cuda --version` still answers,
154 * and before anything touches a device.
155 */
156 const auto launcher_tasks = detect_launcher_tasks();
157 if (launcher_tasks.count > 1) {
158 /**
159 * The one case where several tasks running the same binary is what the
160 * user meant: packing independent calculations into one step. Nothing
161 * here can tell that apart from the mistake, so it is opt-in.
162 */
163 const bool packing_independent_runs = std::getenv("Q3_ALLOW_MULTI_TASK_GPU") != nullptr;
164 constexpr const char* message =
165 "{} was launched as {} tasks ({}={}), and it has no multi-process mode: each task "
166 "would set up the whole problem on its own device and solve all of it, giving {} "
167 "identical answers for {}x the allocation. Run it as one task, or use q3_mpi to "
168 "spread k-points across ranks on CPU, or q3_mpi_gpu_cuda to spend one device "
169 "per rank on a single calculation. Set "
170 "Q3_ALLOW_MULTI_TASK_GPU=1 to proceed anyway, which is right only when the tasks "
171 "are packing independent calculations into one step.";
172 if (!packing_independent_runs) {
173 log::error(message,
174 descriptor.binary_name,
175 launcher_tasks.count,
176 launcher_tasks.variable,
177 launcher_tasks.count,
178 launcher_tasks.count,
179 launcher_tasks.count);
180 return 1;
181 }
182 log::warn(message,
183 descriptor.binary_name,
184 launcher_tasks.count,
185 launcher_tasks.variable,
186 launcher_tasks.count,
187 launcher_tasks.count,
188 launcher_tasks.count);
189 }
190
191 if (!options.log_level.empty()) {
192 log::set_level(options.log_level);
193 }
194
195 bool use_mixed_precision = false;
196 if (options.precision == "mixed") {
197 use_mixed_precision = true;
198 } else if (!options.precision.empty() && options.precision != "double") {
199 log::error("Unknown precision: {}. Use 'double' or 'mixed'.", options.precision);
200 return 1;
201 }
202
203 print_timestamped_banner("BEGIN (" + descriptor.backend_label + "): ");
204
205 int exit_code = 0;
206 try {
207 auto input = read_input(options, descriptor.binary_name);
208 if (!input.hubbard.empty()) {
209 log::error("DFT+U (hubbard) is not yet supported on GPU. Run on CPU instead.");
210 return 1;
211 }
212
213 // ── Step 1: Initialize and validate the GPU backends ──────────────
214 log::info("Initializing {} backends...", descriptor.backend_label);
216 validate_gpu_device_backend(*device_backend);
217
218 auto gpu_blas = descriptor.create_blas(device_backend);
219 validate_gpu_blas(*gpu_blas, descriptor.blas_library_name);
220
221 auto gpu_eigensolver = descriptor.create_eigensolver(device_backend);
222 validate_gpu_eigensolver(*gpu_eigensolver, descriptor.eigensolver_library_name);
223
224 /**
225 * Backends without a native GPU FFT run the FP64 path through the
226 * PlanewaveBasis FFTW grid (gpu_fft_grid stays null).
227 */
228 std::unique_ptr<gpu::GPUFFTGrid> gpu_fft_grid;
229 if (descriptor.create_fft_grid) {
230 gpu_fft_grid = descriptor.create_fft_grid(device_backend);
231 }
232
233 log::info("All {} backend validations passed.", descriptor.backend_label);
234 log::info("Precision mode: {}",
235 use_mixed_precision ? descriptor.mixed_precision_description : "double (FP64)");
236 log::info("");
237
238 /**
239 * ── Step 2: Every flavor's iterator: serial policy, GPU Davidson ─────
240 * Handed to the flavor rather than built here, so whichever flavor the
241 * input names -- the SCF, or the non-self-consistent pass of a band
242 * structure or a DOS -- diagonalizes on this device in this precision.
243 */
244 IteratorFactory iterator_factory = [&](const InputData& run_input) {
245 auto execution_policy = std::make_unique<execution::PlanewaveSerialExecutionPolicy>();
246 const execution::PlanewaveSerialExecutionPolicy& policy_ref = *execution_policy;
247 auto iterator = create_kohn_sham_iterator(run_input, policy_ref);
248 iterator->set_owned_execution_policy(std::move(execution_policy));
249
250 auto gpu_davidson = std::make_unique<PlanewaveDavidsonDiagonalizer>(
252 *gpu_blas,
253 gpu_fft_grid.get(),
254 *gpu_eigensolver,
255 device_backend);
256 if (use_mixed_precision) {
257 gpu_davidson->set_use_mixed_precision(true);
258 }
259 iterator->set_diagonalizer(std::move(gpu_davidson));
260 return iterator;
261 };
262
263 // ── Step 3: Run the input's flavor ────────────────────────────────
264 log::info(" Calculation type: {}", input.calculation);
265 auto flavor = create_flavor(input.calculation, std::move(iterator_factory));
266 generated::Results results = flavor->run(input);
267 write_results_json(results, input.output_directory + "/" + input.prefix + "_results.json");
268 write_companion_outputs(input, results);
269
270 } catch (const std::exception& exception) {
271 log::error("Error: {}", exception.what());
272 exit_code = 1;
273 }
274
275 auto& profiling = q3::ProfilingCounters::instance();
276 q3::log::info(" Total FFT calls: {}", profiling.fft_call_count.load());
277 q3::log::info(" Total Davidson iterations: {}",
278 profiling.davidson_iteration_count.load());
279 q3::log::info(" Total Hamiltonian applications: {}",
280 profiling.hamiltonian_application_count.load());
281 profiling.print_memory_summary();
282 profiling.davidson_timing.print_summary();
283 print_timestamped_banner("END (" + descriptor.backend_label + "): ");
284 return exit_code;
285}
286
287} // namespace cli
288} // namespace q3
static const execution::SerialExecutionPolicy default_serial_execution_policy
Definition davidson.hpp:354
virtual Complex dot_product(int number_of_elements, const Complex *vector_x, const Complex *vector_y) const =0
virtual bool supports_double_precision() const =0
virtual size_t total_memory_bytes() const =0
virtual void deallocate(void *pointer)=0
virtual void * allocate_unified(size_t size_bytes)=0
virtual std::string device_name() const =0
virtual void hermitian_eigenvalue_solve(int matrix_dimension, Complex *matrix_data, int leading_dimension, Real *eigenvalues)=0
void register_all()
No add-on is compiled in, so there is nothing to register.
void validate_gpu_eigensolver(execution::gpu::lapack::GPUEigensolver &gpu_eigensolver, const std::string &eigensolver_library_name)
int run_gpu_entry_point(int argc, char *argv[], const GPUEntryPointDescriptor &descriptor)
void validate_gpu_device_backend(execution::gpu::device::GPUDeviceBackend &device_backend)
void validate_gpu_blas(execution::gpu::blas::GPUBLAS &gpu_blas, const std::string &blas_library_name)
Options parse_options(int argc, char *argv[], const std::string &binary_name)
Definition cli.hpp:175
void print_version(const std::string &binary_name)
Definition cli.hpp:45
LauncherTasks detect_launcher_tasks()
The same, against the real environment.
Definition launcher.hpp:72
InputData read_input(Options &options, const std::string &binary_name)
Definition cli.hpp:239
void print_timestamped_banner(const std::string &label_prefix)
Definition cli.hpp:261
std::shared_ptr< GPUDeviceBackend > create_gpu_device_backend()
Definition cuda.cpp:103
void init(const std::string &log_file="")
Definition logger.hpp:59
void warn(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:187
void error(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:192
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
void set_level(const std::string &level)
Set log level: "trace", "debug", "info", "warn", "error", "off".
Definition logger.hpp:94
void write_results_json(const generated::Results &results, const std::string &output_path)
Write the results to output_path as results.json.
Definition results.hpp:191
std::unique_ptr< Flavor > create_flavor(const std::string &calculation_type, IteratorFactory iterator_factory={})
Definition factory.hpp:52
std::function< std::unique_ptr< PlanewaveKohnShamSCFIterator >(const InputData &)> IteratorFactory
Definition factory.hpp:59
std::complex< double > Complex
void write_companion_outputs(const InputData &input, const generated::Results &results)
Write the companion outputs of the format input was read from, if it has any.
std::unique_ptr< PlanewaveKohnShamSCFIterator > create_kohn_sham_iterator(const InputData &input, const execution::PlanewaveExecutionPolicy &execution_policy)
Definition factory.hpp:64
Parsed input data — holds raw values read from input files.
Definition data.hpp:27
static ProfilingCounters & instance()
Definition counters.hpp:220
std::string backend_label
Banner label, e.g. "CUDA GPU" → "BEGIN (CUDA GPU): <timestamp>".
std::function< std::unique_ptr< execution::gpu::blas::GPUBLAS >(DeviceBackendPointer)> create_blas
std::function< std::unique_ptr< execution::gpu::lapack::GPUEigensolver >(DeviceBackendPointer)> create_eigensolver
std::string binary_name
Binary name for the usage line, e.g. "q3_gpu_cuda".
std::shared_ptr< execution::gpu::device::GPUDeviceBackend > DeviceBackendPointer
std::string eigensolver_library_name
Vendor eigensolver name for the validation log line, e.g. "cuSOLVER". Optional.
std::string blas_library_name
Vendor BLAS name for the validation log line, e.g. "cuBLAS". Optional.
std::function< std::unique_ptr< gpu::GPUFFTGrid >(DeviceBackendPointer)> create_fft_grid
std::string mixed_precision_description
Human-readable description of the mixed-precision mode, e.g. "mixed (FP32 FFT via cuFFT)".