Loading...
Searching...
No Matches
ClientEON.cpp File Reference
#include "eon/BaseStructures.h"
#include "eon/Bundling.h"
#include "eon/CommandLine.h"
#include "eon/EpiCenters.h"
#include "eon/HelperFunctions.h"
#include "eon/Job.h"
#include "eon/Parameters.h"
#include "eon/Potential.h"
#include "eon/Runtime.h"
#include "version.h"
#include <cstdlib>
#include <exception>
#include <format>
#include <fstream>
#include <iostream>
#include <string_view>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <ctime>
#include <filesystem>
#include "eon/fpe_handler.h"
#include <sys/resource.h>
#include <sys/time.h>
#include <sys/utsname.h>
#include <unistd.h>

Go to the source code of this file.

Functions

void printSystemInfo ()
static int eonClientMain (int argc, char **argv)
int main (int argc, char **argv)

Function Documentation

◆ eonClientMain()

int eonClientMain ( int argc,
char ** argv )
static

Definition at line 129 of file ClientEON.cpp.

129 {
130 // --- Start Logging setup
131 // Configure backend for optimal performance (see BackendOptions.h)
132 quill::BackendOptions backend_options;
133 // Use 10us sleep for balanced performance (10x faster than 100us default)
134 backend_options.sleep_duration = std::chrono::microseconds{10};
135 // Larger initial buffer avoids reallocs in NEB (43 LOG calls/iter)
136 backend_options.transit_event_buffer_initial_capacity = 2048;
137 // eOn is single-threaded; SPSC queue guarantees ordering, no grace needed
138 backend_options.log_timestamp_ordering_grace_period =
139 std::chrono::microseconds{0};
140 // Flush more frequently for better responsiveness
141 backend_options.sink_min_flush_interval = std::chrono::milliseconds{100};
142 // Disable per-string printable char scan (eOn logs numeric data only)
143 backend_options.check_printable_char = {};
144 quill::Backend::start(backend_options);
145 auto console_sink =
146 quill::Frontend::create_or_get_sink<quill::ConsoleSink>("console");
147 auto file_sink = quill::Frontend::create_or_get_sink<quill::FileSink>(
148 "client_quill.log",
149 []() {
150 quill::FileSinkConfig cfg;
151 cfg.set_open_mode('w');
152 return cfg;
153 }(),
154 quill::FileEventNotifier{});
155 auto *logger = quill::Frontend::create_or_get_logger(
156 "combi", {std::move(console_sink), std::move(file_sink)},
157 quill::PatternFormatterOptions{"%(message)"},
158 quill::ClockSourceType::System);
159 logger->set_log_level(quill::LogLevel::TraceL3);
160 // Traceback logger
161 auto trace_csink =
162 quill::Frontend::create_or_get_sink<quill::ConsoleSink>("trace_console");
163 auto trace_fsink = quill::Frontend::create_or_get_sink<quill::FileSink>(
164 "client_traceback.log",
165 []() {
166 quill::FileSinkConfig cfg;
167 cfg.set_open_mode('w');
168 return cfg;
169 }(),
170 quill::FileEventNotifier{});
171 quill::Frontend::create_or_get_logger(
172 "_traceback", {std::move(trace_csink), std::move(trace_fsink)},
173 quill::PatternFormatterOptions{
174 " [%(log_level)] [%(source_location)] [%(caller_function)] \n "
175 "%(message)\n[end %(log_level)]"},
176 quill::ClockSourceType::System);
177 //--- End logging setup
178 // File sinks above open relative to this directory. MPI jobs chdir later.
179 const auto logHome = std::filesystem::current_path();
180 eonc::Parameters parameters;
181
182#if defined WITH_ASE_ORCA || EMBED_PYTHON || WITH_ASE_NWCHEM
184#endif
185
186#ifdef EONMPI
187 bool client_standalone = false;
188 if (getenv("EON_CLIENT_STANDALONE") != nullptr) {
189 client_standalone = true;
190 }
191 int number_of_clients;
192 if (!client_standalone) {
193 if (getenv("EON_SERVER_PATH") == nullptr) {
194 QUILL_LOG_ERROR(logger, "error: must set the env var EON_SERVER_PATH");
195 logger->flush_log();
196 return 1;
197 }
198 if (getenv("EON_NUMBER_OF_CLIENTS") == nullptr) {
199 QUILL_LOG_ERROR(logger,
200 "error: must set the env var EON_NUMBER_OF_CLIENTS");
201 logger->flush_log();
202 return 1;
203 }
204 number_of_clients = atoi(getenv("EON_NUMBER_OF_CLIENTS"));
205 } else {
206 number_of_clients = 1;
207 }
208
209 int eon_mpi_inited = 0;
210 MPI_Initialized(&eon_mpi_inited);
211 if (!eon_mpi_inited) {
212 int eon_mpi_provided = 0;
213 MPI_Init_thread(&argc, &argv, MPI_THREAD_MULTIPLE, &eon_mpi_provided);
214 }
215
216 int error;
217 std::string config_file = "config.ini";
218 if (client_standalone) {
219 if (eonc::helpers::existsFile("config_0.ini")) {
220 config_file = "config_0.ini";
221 }
222 QUILL_LOG_INFO(logger, "Loading parameter file {}", config_file);
223 error = parameters.load(config_file);
224 } else {
225 QUILL_LOG_INFO(logger, "Loading parameter file {}",
226 parameters.main_options().iniFilename);
227 error = parameters.load(parameters.main_options().iniFilename);
228 }
229 if (error) {
230 QUILL_LOG_ERROR(logger, "problem loading parameter file");
231 logger->flush_log();
232 MPI_Abort(MPI_COMM_WORLD, 1);
233 }
234
235 // All ranks must have loaded Parameters before the process-type Allgather.
236 MPI_Barrier(MPI_COMM_WORLD);
237
238 int irank;
239 MPI_Comm_rank(MPI_COMM_WORLD, &irank);
240 int isize;
241 MPI_Comm_size(MPI_COMM_WORLD, &isize);
242
243 std::vector<int> process_types(isize);
244 int process_type;
245
246 process_type = 1;
247
248 MPI_Allgather(&process_type, 1, MPI_INT, &process_types[0], 1, MPI_INT,
249 MPI_COMM_WORLD);
250
251 int i, servers = 0, clients = 0, potentials = 0;
252 int server_rank = -1;
253 int my_client_number = -1;
254 std::vector<int> client_ranks;
255 for (i = 0; i < isize; i++) {
256 switch (process_types[i]) {
257 case 0:
258 servers++;
259 break;
260 case 1:
261 if (i == irank) {
262 my_client_number = clients;
263 }
264 clients++;
265 client_ranks.push_back(i);
266 break;
267 case 2:
268 potentials++;
269 break;
270 }
271 }
272
273 if (clients < number_of_clients) {
274 QUILL_LOG_ERROR(logger,
275 "didn't launch as many mpi client ranks as specified in "
276 "EON_NUMBER_OF_CLIENTS");
277 logger->flush_log();
278 MPI_Abort(MPI_COMM_WORLD, 1);
279 }
280 clients = number_of_clients;
281
282 if (parameters.potential_options().potential == eonc::PotType::MPI) {
283 if (potentials == 0 || potentials % clients != 0) {
284 QUILL_LOG_ERROR(logger,
285 "the MPI potential needs a nonzero number of potential "
286 "ranks divisible by EON_NUMBER_OF_CLIENTS ({} potential "
287 "ranks, {} clients)",
288 potentials, clients);
289 logger->flush_log();
290 MPI_Abort(MPI_COMM_WORLD, 1);
291 }
292 std::vector<int> potential_ranks(potentials);
293 int j;
294 for (i = 0, j = 0; i < isize; i++) {
295 if (process_types[i] == 2) {
296 potential_ranks[j] = i;
297 j++;
298 }
299 }
300 int potential_group_size = potentials / clients;
301
302 for (i = 0; i < clients; i++) {
303 MPI_Group orig_group, new_group;
304 MPI_Comm_group(MPI_COMM_WORLD, &orig_group);
305 int offset = i * potential_group_size;
306 MPI_Group_incl(orig_group, potential_group_size, &potential_ranks[offset],
307 &new_group);
308 MPI_Comm pot_comm;
309 MPI_Comm_create(MPI_COMM_WORLD, new_group, &pot_comm);
310 }
311
312 if (my_client_number < number_of_clients) {
313 parameters.set_mpi_potential_rank(
314 potential_ranks[my_client_number * potential_group_size]);
315 }
316 }
317
318 // LAMMPS MPI communicator setup (runtime check, not compile-time)
320 for (i = 0; i < static_cast<int>(client_ranks.size()); i++) {
321 MPI_Group world_group, new_group;
322 MPI_Comm_group(MPI_COMM_WORLD, &world_group);
323 int r = client_ranks[i];
324 MPI_Group_incl(world_group, 1, &r, &new_group);
325 MPI_Comm new_comm;
326 MPI_Comm_create(MPI_COMM_WORLD, new_group, &new_comm);
327 if (new_comm != MPI_COMM_NULL) {
328 eonc::setMpiClientComm(parameters, new_comm);
329 }
330 QUILL_LOG_INFO(logger, "creating group with ranks: {}", r);
331 }
332 }
333
334 if (!client_standalone) {
335 server_rank = client_ranks[number_of_clients];
336 if (my_client_number == number_of_clients) {
337 std::ostringstream oss;
338 oss << client_ranks.at(0);
339 for (i = 1; i < number_of_clients; i++) {
340 oss << ":" << client_ranks.at(i);
341 }
342 setenv("EON_CLIENT_RANKS", oss.str().c_str(), 1);
343
344 wchar_t **py_argv =
345 static_cast<wchar_t **>(malloc(sizeof(wchar_t *) * 2));
346 py_argv[0] = Py_DecodeLocale(argv[0], nullptr);
347 char *program = getenv("EON_SERVER_PATH");
348 py_argv[1] = Py_DecodeLocale(program, nullptr);
349 QUILL_LOG_INFO(logger, "rank: {} becoming {}", irank, program);
350 Py_Initialize();
351 Py_Main(2, py_argv);
352 Py_FinalizeEx();
353 // GH
354 MPI_Finalize();
355 return 0;
356 } else if (my_client_number > number_of_clients) {
357 MPI_Finalize();
358 return 0;
359 }
360 }
361#endif
362
363#ifndef EONMPI
364 if (argc > 1) {
365 eonc::commandLine(argc, argv);
366 return 0;
367 }
368#endif
369
370 eonc::enableFPE(); // from ExceptionsEON.h
371
372#ifdef EONMPI
373 // Server sends a path starting with STOPCAR to end this loop.
374 char logfilename[1024];
375 snprintf(logfilename, 1024, "eonclient_%i.log", my_client_number);
376
377 auto orig_path = std::filesystem::current_path();
378 // In client/server mode a failed job is logged and its directory handed
379 // back without results.dat; the server skips it and this rank stays in
380 // the pool. A standalone rank still exits with the error.
381 const bool keepServing = !client_standalone;
382 while (true) {
383 std::filesystem::current_path(orig_path);
384 std::string path(1024, '\0');
385 int ready = 1;
386 if (!client_standalone) {
387 QUILL_LOG_INFO(
388 logger, "client: rank {} is ready, posting send to server rank: {}!",
389 irank, server_rank);
390 // Tag "1" is to interrupt the main loop and tell the communicator that a
391 // client is ready
392 {
393 MPI_Request eon_rq;
394 MPI_Isend(&ready, 1, MPI_INT, server_rank, 1, MPI_COMM_WORLD, &eon_rq);
395 MPI_Request_free(&eon_rq);
396 }
397
398 // Get the path we should run in from the server
399 MPI_Recv(&path[0], 1024, MPI_CHAR, server_rank, 0, MPI_COMM_WORLD,
400 MPI_STATUS_IGNORE);
401 if (path.starts_with("STOPCAR")) {
402 QUILL_LOG_INFO(logger, "rank {} got STOPCAR", irank);
403 MPI_Finalize();
404 return 0;
405 }
406 QUILL_LOG_INFO(logger, "client: rank: {} chdir to {}", irank, path);
407
408 if (const auto chdirError =
409 eonc::helpers::enterJobDirectory(path.c_str())) {
410 QUILL_LOG_ERROR(logger, "error: chdir: {}", *chdirError);
411 logger->flush_log();
412 MPI_Send(&path[0], 1024, MPI_CHAR, server_rank, 0, MPI_COMM_WORLD);
413 continue;
414 }
415 }
416#else
417 constexpr bool keepServing = false;
418#endif
419 // Flushes the logs into the job directory so a failed job still shows
420 // its error to whoever reads the returned directory.
421 auto stageFailedJobLogs = [&] {
422 logger->flush_log();
423 if (auto *trace =
424 quill::Frontend::get_logger(std::string{"_traceback"})) {
425 trace->flush_log();
426 }
427 for (const std::string_view logName :
428 {std::string_view{"client_quill.log"},
429 std::string_view{"client_traceback.log"}}) {
430 eonc::helpers::stageReturnLog(logHome.string(), logName);
431 }
432 };
433
435
436 bool bundlingEnabled = false;
437 int bundleSize = eonc::getBundleSize();
438 if (bundleSize <= 0) {
439 bundleSize = 1;
440 bundlingEnabled = false;
441 } else {
442 bundlingEnabled = true;
443 }
444
445 std::vector<std::string> bundledFilenames;
446 for (int i = 0; i < bundleSize; i++) {
447 // This job only. A clock above the bundle or MPI loop also counts
448 // earlier jobs and the idle wait between them.
449 const auto start_time = std::chrono::steady_clock::now();
450 if (bundleSize > 1)
451 QUILL_LOG_INFO(logger, "Beginning Job {} of {}", i + 1, bundleSize);
452 std::vector<std::string> unbundledFilenames;
453 if (bundlingEnabled) {
454 unbundledFilenames = eonc::unbundle(i);
455 }
456
457 // check to see if parameters file exists before loading
458 int error = 0;
459 std::string config_file =
461 QUILL_LOG_INFO(logger, "Loading parameter file {}", config_file);
462 error = parameters.load(config_file);
463
464 if (error) {
465 QUILL_LOG_ERROR(logger, "problem loading parameter file, stopping");
466 logger->flush_log();
467 if (keepServing) {
468 stageFailedJobLogs();
469 continue;
470 }
471 return 1;
472 }
473
474 // Determine what type of job we are running according to the parameters
475 // file.
476 eonc::Runtime rt;
477 auto job = eonc::helpers::makeJob(
478 std::make_unique<eonc::Parameters>(parameters), rt);
479 if (job == nullptr) {
480 QUILL_LOG_ERROR(logger, "error: Unknown job: {}",
481 std::string{magic_enum::enum_name<eonc::JobType>(
482 parameters.main_options().job)});
483 logger->flush_log();
484 if (keepServing) {
485 stageFailedJobLogs();
486 continue;
487 }
488 return 1;
489 }
490
491 std::vector<std::string> filenames;
492 try {
493 filenames = job->run();
494 } catch (int e) {
495 QUILL_LOG_CRITICAL(logger, "[ERROR] job exited on error {}", e);
496 logger->flush_log();
497 if (keepServing) {
498 stageFailedJobLogs();
499 continue;
500 }
501 return EXIT_FAILURE;
502 } catch (const std::exception &e) {
503 QUILL_LOG_CRITICAL(logger, "[ERROR] unhandled exception: {}", e.what());
504 logger->flush_log();
505 std::cerr << "[ERROR] unhandled exception: " << e.what() << "\n";
506 if (keepServing) {
507 stageFailedJobLogs();
508 continue;
509 }
510 return EXIT_FAILURE;
511 }
512
513 job->releasePotential();
514 rt.pots().write_summary();
515 job.reset();
516 filenames.push_back(std::string("_potcalls.json"));
517
518 // Finalize Timing Information
519 auto end_time = std::chrono::steady_clock::now();
520 std::chrono::duration<double> elapsed = end_time - start_time;
521
522 double utime = 0, stime = 0, rtime = 0;
523 eonc::helpers::getTime(&rtime, &utime, &stime);
524
525 QUILL_LOG_INFO(logger, "Timing Information:");
526 QUILL_LOG_INFO(logger, " Real time: {:.3f} seconds", elapsed.count());
527 QUILL_LOG_INFO(logger, " User time: {:.3f} seconds", utime);
528 QUILL_LOG_INFO(logger, " System time: {:.3f} seconds", stime);
529
530 // results.dat contract is "<value> <key>" (same as all job writers and
531 // eon.fileio.parse_results / eon_schema.jobs adapters).
532 std::ofstream result_file("results.dat", std::ios::app);
533 if (result_file.is_open()) {
534 result_file << std::format("{:.12e} time_seconds\n", elapsed.count());
535#ifndef _WIN32
536 result_file << std::format("{:.12e} user_time\n", utime);
537 result_file << std::format("{:.12e} system_time\n", stime);
538#endif
539 } else {
540 QUILL_LOG_ERROR(logger, "Failed to write timing to results.dat");
541 }
542 result_file.close();
543
544 logger->flush_log();
545 if (auto *trace =
546 quill::Frontend::get_logger(std::string{"_traceback"})) {
547 trace->flush_log();
548 }
549 // Quill opens these before an MPI job chdir. Copy them into the job
550 // directory so return_files.dat does not name a missing log.
551 constexpr std::string_view jobLogs[] = {"client_quill.log",
552 "client_traceback.log"};
553 for (const std::string_view logName : jobLogs) {
554 if (eonc::helpers::stageReturnLog(logHome.string(), logName)) {
555 filenames.push_back(std::string{logName});
556 }
557 }
558
559 {
560 std::ofstream manifest("return_files.dat");
561 if (manifest) {
562 for (const auto &fn : filenames) {
563 manifest << fn << "\n";
564 }
565 }
566 filenames.push_back(std::string("return_files.dat"));
567 }
568
569 if (bundlingEnabled) {
570 eonc::bundle(i, filenames, &bundledFilenames);
571 eonc::deleteUnbundledFiles(unbundledFilenames);
572 } else {
573 bundledFilenames = filenames;
574 }
575 }
576
577#ifdef EONMPI
578 if (client_standalone) {
579 break;
580 }
581 {
582 MPI_Request eon_rq;
583 MPI_Isend(&path[0], 1024, MPI_CHAR, server_rank, 0, MPI_COMM_WORLD,
584 &eon_rq);
585 // path is destroyed at the end of this iteration. Request_free does
586 // not complete the send, so the buffer stays live until Wait returns.
587 MPI_Wait(&eon_rq, MPI_STATUS_IGNORE);
588 }
589
590 // End of MPI while loop
591 }
592#endif
593
594#ifdef OSX
595#ifndef __aarch64__
596 print_memory_usage();
597#endif
598#endif
599
600#ifdef EONMPI
601 // Clean shutdown for both the standalone single-rank case and the
602 // client/server case; the standalone path previously aborted MPI_COMM_WORLD.
603 MPI_Finalize();
604#endif
605
606 // Ensure all queued log messages are flushed before exiting
607 quill::Backend::stop();
608 return EXIT_SUCCESS;
609}
void printSystemInfo()
Definition ClientEON.cpp:96
void set_mpi_potential_rank(int rank)
const potential_options_t & potential_options() const
const main_options_t & main_options() const
int load(std::string_view filename)
void write_summary(const std::string &path="_potcalls.json") const
Move-only composition root for process resources (dlopen loaders and the potential registry).
Definition Runtime.h:27
PotRegistry & pots() noexcept
Definition Runtime.cpp:33
std::string error()
Definition DynLib.h:82
std::string getRelevantFile(std::string filename)
bool stageReturnLog(std::string_view logHome, std::string_view name)
Copy name from logHome into the current directory when that file is not already this directory's copy...
std::optional< std::string > enterJobDirectory(std::string_view jobPath)
Enter jobPath as the working directory.
std::unique_ptr< Job > makeJob(std::unique_ptr< Parameters > params, Runtime &runtime)
Borrow: caller keeps Runtime alive.
void getTime(double *real, double *user, double *sys)
bool existsFile(std::string filename)
void bundle(int number, const std::vector< std::string > &filenames, std::vector< std::string > *bundledFilenames)
Definition Bundling.cpp:180
void deleteUnbundledFiles(const std::vector< std::string > &unbundledFilenames)
Definition Bundling.cpp:145
std::vector< std::string > unbundle(int number)
Definition Bundling.cpp:71
int getBundleSize()
Definition Bundling.cpp:27
void ensure_interpreter()
Definition NbGuard.h:21
void commandLine(int argc, char **argv)
void enableFPE()

◆ main()

int main ( int argc,
char ** argv )

Definition at line 630 of file ClientEON.cpp.

630 {
631 try {
632 return eonClientMain(argc, argv);
633 } catch (const std::exception &e) {
634 return reportFatal(e.what());
635 } catch (...) {
636 return reportFatal("exception of unknown type");
637 }
638}
static int eonClientMain(int argc, char **argv)

◆ printSystemInfo()

void printSystemInfo ( )

Definition at line 96 of file ClientEON.cpp.

96 {
97 auto *log = eonc::log::get();
98 QUILL_LOG_INFO(log, "eOn Client");
99 QUILL_LOG_INFO(log, "{}", VERSION_STRING);
100#ifndef __aarch64__
101 QUILL_LOG_INFO(log, "OS: {}", OS_INFO);
102 QUILL_LOG_INFO(log, "Arch: {}", ARCH);
103#endif
104
105#ifdef _WIN32
106 TCHAR hostname[MAX_COMPUTERNAME_LENGTH + 1];
107 DWORD size = sizeof(hostname) / sizeof(hostname[0]);
108 if (GetComputerName(hostname, &size)) {
109 QUILL_LOG_INFO(log, "Hostname: {}", hostname);
110 } else {
111 QUILL_LOG_ERROR(log, "Failed to get hostname");
112 }
113 QUILL_LOG_INFO(log, "PID: {}", GetCurrentProcessId());
114#else
115 struct utsname systemInfo;
116 int status = uname(&systemInfo);
117 if (status == 0) {
118 QUILL_LOG_INFO(log, "Hostname: {}", systemInfo.nodename);
119 QUILL_LOG_INFO(log, "PID: {}", getpid());
120 } else {
121 QUILL_LOG_ERROR(log, "Failed to get system information");
122 }
123#endif
124
125 std::filesystem::path cwd = std::filesystem::current_path();
126 QUILL_LOG_INFO(log, "DIR: {}", cwd.string());
127}
quill::Logger * get() noexcept
Get or create the default "combi" logger.
Definition EonLogger.h:44