← Documentation
ADS-IDAC Engine 5.0.0
Server.cpp
Go to the documentation of this file.
1//
2// Created by atezbas on 10/21/24.
3//
4
5#include "Server.h"
6#include "../Global/SharedConfig.h"
8#include "../Scheduler/Scheduler.h"
9#include "../Global/DETCheckpoint.h"
10#include "../Global/PEOutput.h"
11#include "../Global/OutputMerge.h"
12#include <sstream>
13#include <iomanip>
14#include <map>
15#include <set>
16#include <unordered_set>
17#include <csignal>
18#include <unistd.h>
19#include "../Global/SimulationContext.h"
20using namespace std;
21
22// SIGUSR1 checkpoint request (for HPC wall-time limits)
23static volatile sig_atomic_t mpi_checkpoint_requested = 0;
25
26// SIGINT (Ctrl+C) interactive interrupt
27static volatile sig_atomic_t mpi_sigint_received = 0;
28static void mpi_sigint_handler(int) { mpi_sigint_received = 1; }
29
30// Show interactive interrupt menu on rank 0, return user choice (1/2/3)
31static int show_mpi_interrupt_menu(int seq_completed, int queue_depth, int busy_clients) {
32 std::cout << "\n\n=== Simulation Interrupted (Ctrl+C) ===" << std::endl;
33 std::cout << " Sequences completed: " << seq_completed << std::endl;
34 std::cout << " Queue depth: " << queue_depth << std::endl;
35 std::cout << " Busy clients: " << busy_clients << std::endl;
36 std::cout << std::endl;
37 std::cout << " 1. Continue simulation" << std::endl;
38 std::cout << " 2. Save checkpoint and exit" << std::endl;
39 std::cout << " 3. Exit without saving" << std::endl;
40 std::cout << std::endl;
41 std::cout << "Choice [1]: " << std::flush;
42
43 std::string line;
44 if (!std::getline(std::cin, line) || line.empty()) return 1;
45 if (line[0] == '2') return 2;
46 if (line[0] == '3') return 3;
47 return 1;
48}
49
50Server::Server(const int rank, int viz_port) {
51 this->rank = rank;
52 viz_port_ = viz_port;
53 clients_busy = std::make_unique<int[]>(PG::size);
54 for (int i = 0; i < PG::size; i++) clients_busy[i] = 0;
57
58 // Start visualization server if requested
59 if (viz_port_ > 0) {
60 viz_state_ = std::make_shared<TreeState>();
61 viz_state_->total_clients = PG::size - 1;
62 viz_state_->start_time = MPI_Wtime();
63 viz_server_ = std::make_unique<VisualizationServer>(viz_state_, viz_port_);
64 viz_server_->start();
65 PU::print_message(rank, "Visualization server at http://localhost:" + std::to_string(viz_port_), PG::log);
66 // Also print to original stdout (rank 0 doesn't redirect)
67 std::cout << "DET visualization: http://localhost:" << viz_port_ << std::endl;
68 }
69
71 PU::print_message(rank, "Server initiated..", PG::log);
72 PU::print_message(rank, "Size of the MPI world: " + std::to_string(PG::size), PG::log);
73}
74
75Server::~Server() = default;
76
78 PU::print_message(0, "Sending shutdown signal to clients..", PG::log);
79 for (int who = 1; who < PG::size; who++)
80 MPI_Send (nullptr, 0, MPI_INT, who, PG::client_shutdown, MPI_COMM_WORLD);
81 PU::print_message(0, "Shutdown signal sent to all clients..", PG::log);
82 if (viz_server_) viz_server_->stop();
83}
84
85
86void Server::Simulation() // PG::rank 0 executes this
87{
88 PU::print_message(rank, "Started the simulation", PG::log);
89
90 // The server deserializes crew snapshots (checkpoint gather, resume load)
91 // but never runs RELAP5, so onFirstInit never registers the procedures
92 // master here. Register it explicitly (idempotent).
95 g_sim.Indicator.readInfoFromFile(); // procedures parser needs the panel maps
96 // Procedures before the crew, same reason as Client.cpp: the reason
97 // machine resolves procedure-step pointers during crew init, and rank 0
98 // deserializes crew snapshots, so the master must already be registered.
108
109 // Register signal handlers
110 signal(SIGUSR1, mpi_sigusr1_handler);
111 signal(SIGINT, mpi_sigint_handler);
112
113 // Occupancy trace: open once, header first. Off unless --occupancy-log given.
114 if (!g_sim.config.occupancy_log.empty()) {
115 occupancy_log_.open(g_sim.config.occupancy_log, std::ios::out | std::ios::trunc);
116 if (occupancy_log_) {
117 occupancy_t0_ = MPI_Wtime();
118 occupancy_log_ << "elapsed_s,queue_depth,busy_workers,total_workers,sequences_completed\n";
119 } else {
120 PU::print_message(rank, "WARNING: cannot open occupancy log "
121 + g_sim.config.occupancy_log + " -- continuing without it", PG::log);
122 }
123 }
124
125 // Helper: push viz stats under lock (no-op if viz disabled), and record the
126 // occupancy step function (no-op if --occupancy-log not given).
127 auto update_viz_stats = [this]() {
128 const int queue = static_cast<int>(tasks_queue.size());
129 const int workers = PG::size - 1;
130 const int busy = workers - num_free_clients;
131
132 if (occupancy_log_ && (queue != last_queue_ || busy != last_busy_)) {
133 occupancy_log_ << (MPI_Wtime() - occupancy_t0_) << ',' << queue << ','
134 << busy << ',' << workers << ',' << sequences_completed_ << '\n';
135 last_queue_ = queue;
136 last_busy_ = busy;
137 }
138
139 if (!viz_state_) return;
140 std::lock_guard<std::mutex> lock(viz_state_->mu);
141 viz_state_->sequences_completed = sequences_completed_;
142 viz_state_->queue_depth = queue;
143 viz_state_->num_free_clients = num_free_clients;
144 viz_state_->memory_mb = get_rss_mb();
145 };
146
147 // Resume from checkpoint or start fresh
148 if (!g_sim.config.resume_file.empty()) {
150 } else {
151 // Sequence 0 -> first free client
152 int free_rank = find_next_free_client_round_robin();
153 rank_current_sequence_[free_rank] = 0;
154 PU::print_message(rank, "Number of free clients = " + std::to_string(num_free_clients), PG::log);
155 PU::print_message(rank, "Sending the tag for initial simulation to Client " + std::to_string(free_rank), PG::log);
156 MPI_Send (nullptr, 0, MPI_INT, free_rank, PG::client_simulate_this, MPI_COMM_WORLD);
157 clients_busy[free_rank] = 1;
158 }
159 update_viz_stats();
160
161 bool user_exit = false;
162 while (true) {
163 // Check for user interrupt (Ctrl+C)
166 int busy = PG::size - 1 - num_free_clients;
168 static_cast<int>(tasks_queue.size()), busy);
169 if (choice == 2) {
170 if (g_sim.config.checkpoint_dir.empty()) {
172 fs::create_directories(g_sim.config.checkpoint_dir);
173 }
174 checkpoint_pending_ = true;
176 PU::print_message(rank, "Checkpoint+exit requested, waiting for "
177 + std::to_string(busy) + " clients to finish", PG::log);
178 } else if (choice == 3) {
179 PU::print_message(rank, "Exit requested by user", PG::log);
180 // If an interval checkpoint was written this run, the outputs
181 // are an intermediate state: keep natural PE names so that
182 // checkpoint stays resumable.
184 user_exit = true;
185 break;
186 }
187 // choice 1: re-register handler and continue
188 signal(SIGINT, mpi_sigint_handler);
189 }
190
191 // Non-blocking probe — allows interrupt checks between messages
192 int has_msg = 0;
193 MPI_Iprobe(MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &has_msg, &status_server);
194 if (!has_msg) {
195 // Check termination conditions even during idle polling
198 checkpoint_pending_ = false;
199 if (g_sim.config.checkpoint_stop) { stopped_at_checkpoint_ = true; break; }
200 }
201 if (tasks_queue.empty() && resume_pending_.empty() &&
203 PU::print_message(0, "Finished the parallel simulation", PG::log);
204 update_viz_stats();
205 break;
206 }
207 usleep(1000); // 1ms poll interval — negligible overhead
208 continue;
209 }
210
211 if (status_server.MPI_TAG == PG::server_output_data) {
212 // Receive output data (PE events, end states) — does NOT mark client free
214 PU::receive_object(data, status_server.MPI_SOURCE,
215 PG::server_output_data, MPI_COMM_WORLD);
216 if (viz_state_) {
217 std::lock_guard<std::mutex> lock(viz_state_->mu);
218 viz_state_->output_data[data.sequence_id] = std::move(data);
219 }
220
221 } else if (status_server.MPI_TAG == PG::server_done_with_report) {
222 // The client sends a SequenceReport payload (sent as MPI_BYTE)
223 SequenceReport report{};
224 MPI_Recv(&report, sizeof(SequenceReport), MPI_BYTE,
226 MPI_COMM_WORLD, MPI_STATUS_IGNORE);
228 clients_busy[status_server.MPI_SOURCE] = 0;
230 PU::print_message(rank, "Sequences completed: " + std::to_string(sequences_completed_)
231 + " (S" + std::to_string(report.sequence_id) + " from C["
232 + std::to_string(status_server.MPI_SOURCE) + "])"
233 + ", free_clients=" + std::to_string(num_free_clients)
234 + ", queue=" + std::to_string(tasks_queue.size()), PG::log);
235 if (viz_state_) {
236 std::lock_guard<std::mutex> lock(viz_state_->mu);
237 viz_state_->completed.push_back(report);
238 }
239
240 } else if (status_server.MPI_TAG == PG::server_done_simulating) {
241 // Empty completion signal without a SequenceReport (no tree data).
242 // Client.cpp reports completion with server_done_with_report instead.
243 MPI_Recv(nullptr, 0, MPI_INT, status_server.MPI_SOURCE, PG::server_done_simulating,
244 MPI_COMM_WORLD, MPI_STATUS_IGNORE);
246 clients_busy[status_server.MPI_SOURCE] = 0;
248 PU::print_message(rank, "Sequences completed: " + std::to_string(sequences_completed_)
249 + " (legacy, no report, from C[" + std::to_string(status_server.MPI_SOURCE) + "])"
250 + ", free_clients=" + std::to_string(num_free_clients)
251 + ", queue=" + std::to_string(tasks_queue.size()), PG::log);
252
253 } else if (status_server.MPI_TAG == PG::server_send_complete) {
254 MPI_Recv(nullptr, 0, MPI_INT, status_server.MPI_SOURCE, PG::server_send_complete,
255 MPI_COMM_WORLD, MPI_STATUS_IGNORE);
257 clients_busy[status_server.MPI_SOURCE] = 0;
258 PU::print_message(rank, "[PEER] Send complete from C[" + std::to_string(status_server.MPI_SOURCE)
259 + "], free=" + std::to_string(num_free_clients), PG::log);
260
261 } else if (status_server.MPI_TAG == PG::server_branch_meta) {
262 int source = status_server.MPI_SOURCE;
263 int local_id, num_branches;
264 MPI_Recv(&local_id, 1, MPI_INT, source, PG::server_branch_meta,
265 MPI_COMM_WORLD, MPI_STATUS_IGNORE);
266 MPI_Recv(&num_branches, 1, MPI_INT, source, PG::server_branch_meta,
267 MPI_COMM_WORLD, MPI_STATUS_IGNORE);
268
269 // Parent for all children is whatever sequence the source is currently running
270 int parent_seq = rank_current_sequence_.count(source) ? rank_current_sequence_[source] : -1;
271
272 int needed = num_branches - 1;
273 // A pending checkpoint must not hand work back out. This handler
274 // assigns branches to free clients directly, outside the
275 // `if (!checkpoint_pending_)` dispatch gate further down, so while
276 // a checkpoint is pending it places nothing (direct = 0) and
277 // queues every sibling branch. Otherwise retiring sequences would
278 // re-arm the workers they free and `num_free_clients == size-1`
279 // would never be reached. The donor still runs branch 0 itself,
280 // so the drain converges once the running sequences finish.
281 int direct = checkpoint_pending_ ? 0
282 : std::min(needed, num_free_clients);
283 int queued = needed - direct;
284
285 PU::print_message(rank, "[PEER] C[" + std::to_string(source)
286 + "] lid=" + std::to_string(local_id) + " branches=" + std::to_string(num_branches)
287 + " parent_seq=" + std::to_string(parent_seq)
288 + " -> direct=" + std::to_string(direct)
289 + " queued=" + std::to_string(queued), PG::log);
290
291 MPI_Send(&direct, 1, MPI_INT, source, PG::client_peer_assign, MPI_COMM_WORLD);
292
293 for (int i = 0; i < direct; i++) {
294 int free_rank = find_next_free_client_round_robin();
295 int branch_index = i + 1;
296 int seq_id = sequence_counter_++;
297
298 MPI_Send(&free_rank, 1, MPI_INT, source, PG::client_peer_assign, MPI_COMM_WORLD);
299 MPI_Send(&branch_index, 1, MPI_INT, source, PG::client_peer_assign, MPI_COMM_WORLD);
300 MPI_Send(&seq_id, 1, MPI_INT, source, PG::client_peer_assign, MPI_COMM_WORLD);
301 MPI_Send(&parent_seq, 1, MPI_INT, source, PG::client_peer_assign, MPI_COMM_WORLD);
302
303 rank_current_sequence_[free_rank] = seq_id;
304
305 PU::print_message(rank, "[PEER] Direct: C[" + std::to_string(source)
306 + "] -> C[" + std::to_string(free_rank) + "] bi="
307 + std::to_string(branch_index) + " seq=" + std::to_string(seq_id), PG::log);
308 }
309
310 MPI_Send(&queued, 1, MPI_INT, source, PG::client_peer_assign, MPI_COMM_WORLD);
311
312 for (int bi = direct + 1; bi < num_branches; ++bi) {
313 RestartTask task{};
314 task.source_rank = source;
315 task.local_id = local_id;
316 task.sequence_id = sequence_counter_++;
317 task.branch_index = bi;
318 task.parent_sequence_id = parent_seq;
319 tasks_queue.push(task);
320 }
321
322 PU::print_message(rank, "[PEER] Done: free=" + std::to_string(num_free_clients)
323 + " queue=" + std::to_string(tasks_queue.size()), PG::log);
324 }
325
326 // Check checkpoint conditions. last_checkpoint_seq_ advances at every
327 // write, so the interval condition arms once per interval and not
328 // once per server message at the same sequence count.
330 // Threshold, not modulo. This condition is evaluated inside a
331 // message handler, so a `% interval == 0` test fires only if the
332 // counter lands exactly on a multiple at the instant that handler
333 // runs, which many workers reporting concurrently do not
334 // guarantee. A >= comparison against the last checkpoint cannot
335 // be skipped.
339 checkpoint_pending_ = true;
341 PU::print_message(rank, "[CHECKPOINT] Pending — waiting for "
342 + std::to_string(PG::size - 1 - num_free_clients) + " clients to drain", PG::log);
343 }
344 }
345
349 checkpoint_pending_ = false;
351 PU::print_message(rank, "[CHECKPOINT] --checkpoint-stop: exiting after "
352 + std::to_string(sequences_completed_) + " sequences", PG::log);
354 break;
355 }
356 }
357
358 if (!checkpoint_pending_) {
359 // Resume tasks take priority (server holds data from checkpoint)
360 while (!resume_pending_.empty() && num_free_clients > 0)
362 // Then normal peer-based dispatch
364 }
365 update_viz_stats();
366
367 if (tasks_queue.empty() && resume_pending_.empty() && num_free_clients == PG::size - 1 && !checkpoint_pending_)
368 {
369 PU::print_message(0, "Finished the parallel simulation", PG::log);
370 update_viz_stats();
371 break;
372 }
373 }
374}
375
377{
378 for (int i = 0; i < PG::size - 1; ++i) {
379 int candidate = 1 + (next_candidate + i - 1) % (PG::size - 1);
380 if (clients_busy[candidate] == 0) {
381 clients_busy[candidate] = 1;
383 next_candidate = candidate + 1;
385 return candidate;
386 }
387 }
388 return -1;
389}
390
392{
393 for (int i = 0; i < PG::size - 1; ++i) {
394 int candidate = 1 + (next_candidate + i - 1) % (PG::size - 1);
395 if (clients_busy[candidate] == 0 && candidate != exclude_rank) {
396 clients_busy[candidate] = 1;
398 next_candidate = candidate + 1;
400 return candidate;
401 }
402 }
403 return -1;
404}
405
407{
408 // Scan queue for tasks whose source client is free.
409 // Dispatch via peer send (source sends to another free client)
410 // or self-run (source runs the task directly if no other client is free).
411 size_t initial_size = tasks_queue.size();
412 for (size_t i = 0; i < initial_size && num_free_clients > 0; i++) {
413 RestartTask task = tasks_queue.front();
414 tasks_queue.pop();
415
416 if (clients_busy[task.source_rank] != 0) {
417 tasks_queue.push(task);
418 continue;
419 }
420
421 int receiver = find_free_client_except(task.source_rank);
422 if (receiver != -1) {
423 clients_busy[task.source_rank] = 1;
425
426 MPI_Send(&task.local_id, 1, MPI_INT, task.source_rank, PG::client_send_to_peer, MPI_COMM_WORLD);
427 MPI_Send(&receiver, 1, MPI_INT, task.source_rank, PG::client_send_to_peer, MPI_COMM_WORLD);
428 MPI_Send(&task.branch_index, 1, MPI_INT, task.source_rank, PG::client_send_to_peer, MPI_COMM_WORLD);
429 MPI_Send(&task.sequence_id, 1, MPI_INT, task.source_rank, PG::client_send_to_peer, MPI_COMM_WORLD);
430 MPI_Send(&task.parent_sequence_id, 1, MPI_INT, task.source_rank, PG::client_send_to_peer, MPI_COMM_WORLD);
431
432 rank_current_sequence_[receiver] = task.sequence_id;
433
434 PU::print_message(rank, "[PEER] Deferred: C[" + std::to_string(task.source_rank)
435 + "] -> C[" + std::to_string(receiver) + "] lid=" + std::to_string(task.local_id)
436 + " bi=" + std::to_string(task.branch_index)
437 + " seq=" + std::to_string(task.sequence_id), PG::log);
438 } else {
439 clients_busy[task.source_rank] = 1;
441
442 MPI_Send(&task.local_id, 1, MPI_INT, task.source_rank, PG::client_run_held, MPI_COMM_WORLD);
443 MPI_Send(&task.branch_index, 1, MPI_INT, task.source_rank, PG::client_run_held, MPI_COMM_WORLD);
444 MPI_Send(&task.sequence_id, 1, MPI_INT, task.source_rank, PG::client_run_held, MPI_COMM_WORLD);
445 MPI_Send(&task.parent_sequence_id, 1, MPI_INT, task.source_rank, PG::client_run_held, MPI_COMM_WORLD);
446
448
449 PU::print_message(rank, "[PEER] Self-run: C[" + std::to_string(task.source_rank)
450 + "] lid=" + std::to_string(task.local_id)
451 + " bi=" + std::to_string(task.branch_index)
452 + " seq=" + std::to_string(task.sequence_id), PG::log);
453 }
454 }
455 PU::print_message(rank, "[MEM] RSS=" + std::to_string(get_rss_mb())
456 + "MB, queue=" + std::to_string(tasks_queue.size())
457 + ", free=" + std::to_string(num_free_clients), PG::log);
458}
459
461{
462 const double t_merge = MPI_Wtime();
463 // --merge=serial selects the rank-0 copy loop and the in-RAM PE rewrite
464 // (escape hatch and equivalence oracle for the rename-based path).
465 const bool legacy = (g_sim.config.merge_mode == 1);
466 std::string dest_es = g_sim.SScheduler.getESDirectory();
467 std::string dest_pe = g_sim.SScheduler.getPEDirectory();
468 std::string dest_kps = g_sim.SScheduler.getKPSDirectory();
469
470 // Create destination directories
471 fs::create_directories(dest_es);
472 fs::create_directories(dest_pe);
473 fs::create_directories(dest_kps);
474
475 // Under --merge=now the clients move their own ES/KPS files and stage the
476 // append sources as parts inside their shutdown handler; wait for them.
477 // Every rank parses the same argv, so the barrier count is consistent.
478 // (merge_mode 2 = defer reaches this function only when a checkpoint-stop
479 // forced an inline merge; the clients then staged nothing and entered no
480 // barrier, so this must not either.)
481 if (g_sim.config.merge_mode == 0) MPI_Barrier(MPI_COMM_WORLD);
482
483 // Merge per-rank outputs into shared directory
484 for (int r = 1; r < PG::size; ++r) {
485 std::string rank_out = "../rank_" + std::to_string(r) + "/Output/";
486 if (!fs::exists(rank_out)) continue;
487
488 // Copy individual files — unique-by-ID files get copied, text logs get concatenated
489 // The PE aggregate is append-only and self-delimiting, so
490 // concatenating each rank's copy is exactly the right merge — without
491 // this it would land on one shared path and ranks would clobber it.
492 static const std::vector<std::string> append_files = {
493 "Scenario info.txt", "Procedure data.txt", "Scan data.txt",
495 };
496 std::unordered_set<std::string> made_dirs;
497 for (auto& entry : fs::recursive_directory_iterator(rank_out)) {
498 if (!entry.is_regular_file()) continue;
499 fs::path rel = fs::relative(entry.path(), rank_out);
500 fs::path dest = fs::path(g_sim.SScheduler.getOutputDirectory()) / rel;
501 if (made_dirs.insert(dest.parent_path().string()).second)
502 fs::create_directories(dest.parent_path());
503
504 std::string fname = entry.path().filename().string();
505 bool should_append = false;
506 for (auto& af : append_files)
507 if (fname == af) { should_append = true; break; }
508
509 if (should_append) {
510 std::ifstream in(entry.path());
511 std::ofstream out(dest, std::ios::app);
512 out << in.rdbuf();
513 } else if (legacy) {
514 fs::copy(entry.path(), dest, fs::copy_options::overwrite_existing);
515 } else {
516 // Same filesystem by construction (rank_N and Output/ both live
517 // under the project dir): a rename moves metadata, not bytes.
518 // POSIX rename-over-existing gives colliding PE filenames the
519 // same highest-rank-wins result as the --merge=serial copy loop.
520 std::error_code mec;
521 fs::rename(entry.path(), dest, mec);
522 if (mec) {
523 // EXDEV or exotic filesystems: fall back to copy + remove.
524 mec.clear();
525 fs::copy(entry.path(), dest, fs::copy_options::overwrite_existing, mec);
526 if (mec) {
527 PU::print_message(rank, "[MERGE] failed to move " + entry.path().string()
528 + ": " + mec.message(), PG::log);
529 } else {
530 fs::remove(entry.path(), mec);
531 }
532 }
533 }
534 }
535 }
536
537 // Ordered concatenation of the parts the clients staged (--merge=now).
538 // The ES logs and err messages are truncated first — the same net
539 // semantics as the per-run reset — while the PE aggregate destination is
540 // APPEND-ONLY: it accumulates across checkpoint phases and collect()
541 // depends on the accumulation.
542 if (g_sim.config.merge_mode == 0) {
543 const std::vector<std::pair<fs::path, fs::path>> part_sets = {
544 {fs::path(dest_es) / "parts", fs::path(dest_es)},
545 {fs::path(dest_pe) / "parts", fs::path(dest_pe)}};
546 for (auto& [parts_dir, out_dir] : part_sets) {
547 std::error_code pec;
548 if (!fs::exists(parts_dir, pec)) continue;
549 std::set<std::string> bases;
550 for (auto& e : fs::directory_iterator(parts_dir, pec)) {
551 const std::string f = e.path().filename().string();
552 const auto pos = f.rfind(".rank_");
553 if (pos != std::string::npos) bases.insert(f.substr(0, pos));
554 }
555 for (const auto& base : bases) {
556 const bool is_agg = (base == adsidac::pe::aggregate_basename());
557 std::ofstream out(out_dir / base,
558 std::ios::binary | (is_agg ? std::ios::app : std::ios::trunc));
559 for (int r = 1; r < PG::size; ++r) {
560 std::ifstream in(parts_dir / (base + ".rank_" + std::to_string(r)),
561 std::ios::binary);
562 if (in) out << in.rdbuf();
563 }
564 }
565 fs::remove_all(parts_dir, pec);
566 }
567 // The server's own err messages, if any (rank_0 cwd).
568 std::error_code sec;
569 if (fs::exists("err messages.txt", sec)) {
570 std::ifstream in("err messages.txt", std::ios::binary);
571 std::ofstream out(fs::path(dest_es) / "err messages.txt",
572 std::ios::binary | std::ios::app);
573 out << in.rdbuf();
574 }
575 }
576
577 // Build ES_summary.txt from all ZESNode files in the destination tree
578 // (shared with --merge-only via Global/OutputMerge.h so the two paths
579 // cannot diverge on parse or format).
580 const size_t merged_seq_count = adsidac::merge::build_es_summary(dest_es);
581
582 // Renumber PE files sequentially (clients use rank-offset IDs, need sequential for output).
583 // ONLY at a true end of run. A --checkpoint-stop merge is an intermediate
584 // state: files must keep their natural branch-ID names so a resumed run's
585 // re-saves land on the same paths (overwrite = refresh, not
586 // duplicate-or-clobber).
587 size_t pe_count = 0;
589 // Collect from whichever layout the clients wrote, then rewrite with
590 // sequential IDs in the layout this run is configured for. Both halves
591 // live in Global/PEOutput.h so the serial merge in ADSIDAC.cpp cannot
592 // drift from this one.
593 pe_count = legacy
597 } else {
598 PU::print_message(rank, "[CHECKPOINT] Intermediate stop — PE files keep natural IDs for resume", PG::log);
599 }
600
601 {
602 std::ostringstream oss;
603 oss << "Merged " << merged_seq_count << " sequences, " << pe_count
604 << " PE nodes into " << g_sim.SScheduler.getOutputDirectory()
605 << " in " << std::fixed << std::setprecision(1)
606 << (MPI_Wtime() - t_merge) << " s ("
607 << (legacy ? "serial copy" : "rename") << ")";
608 PU::print_message(rank, oss.str(), PG::log);
609 }
610}
611
613{
614 PU::print_message(rank, "[CHECKPOINT] Gathering held_data from " + std::to_string(PG::size - 1) + " clients", PG::log);
615
616 // Gather held_data from all clients (serialize BranchRecord to blob immediately)
617 struct GatheredEntry {
618 int source_rank;
619 int local_id;
620 int cid;
621 std::shared_ptr<RestartNodeInfo> rinfo;
622 std::string branch_record_blob;
623 };
624 std::vector<GatheredEntry> all_held;
625
626 for (int c = 1; c < PG::size; c++) {
627 MPI_Send(nullptr, 0, MPI_INT, c, PG::client_checkpoint, MPI_COMM_WORLD);
628 int count;
629 MPI_Recv(&count, 1, MPI_INT, c, PG::client_checkpoint, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
630 for (int i = 0; i < count; i++) {
631 GatheredEntry entry{};
632 entry.source_rank = c;
633 MPI_Recv(&entry.local_id, 1, MPI_INT, c, PG::client_checkpoint, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
634 entry.rinfo = std::make_shared<RestartNodeInfo>();
635 PU::receive_object(*entry.rinfo, c, PG::client_checkpoint, MPI_COMM_WORLD);
636 BranchRecord br;
637 PU::receive_object(br, c, PG::client_checkpoint, MPI_COMM_WORLD);
638 // Serialize BranchRecord to blob immediately (BranchRecord is not movable)
639 std::ostringstream oss(std::ios::binary);
640 boost::archive::binary_oarchive oa(oss);
641 oa << br;
642 entry.branch_record_blob = oss.str();
643 entry.cid = entry.rinfo->getCorrespondingBranchClusterID();
644 all_held.push_back(std::move(entry));
645 }
646 PU::print_message(rank, "[CHECKPOINT] Client " + std::to_string(c) + ": " + std::to_string(count) + " entries", PG::log);
647 }
648
649 // Fix CID collisions: two workers may independently generate the same CID.
650 // Renumber colliding CIDs so each held entry has a globally unique CID.
651 {
652 std::set<int> seen_cids;
653 int next_unique_cid = 0;
654 // Find max CID across all entries to start renumbering above it
655 for (auto& entry : all_held)
656 if (entry.cid > next_unique_cid) next_unique_cid = entry.cid;
657 next_unique_cid += 1000; // start well above any existing CID
658
659 for (auto& entry : all_held) {
660 if (seen_cids.count(entry.cid)) {
661 int old_cid = entry.cid;
662 int new_cid = next_unique_cid++;
663 PU::print_message(rank, "[CHECKPOINT] CID collision: renumbering CID="
664 + std::to_string(old_cid) + " -> " + std::to_string(new_cid), PG::log);
665
666 // Update the RestartNodeInfo's CID reference
667 entry.rinfo->setCorrespondingBranchClusterID(new_cid);
668
669 // Update the BranchCluster CID inside the serialized BranchRecord blob
670 BranchRecord br;
671 { std::istringstream iss(entry.branch_record_blob, std::ios::binary);
672 boost::archive::binary_iarchive ia(iss); ia >> br; }
673 for (auto& bc : br.branch_cluster) {
674 if (bc->getIDOfThisBranchCluster() == old_cid)
675 bc->setIDOfThisBranchCluster(new_cid);
676 }
677 { std::ostringstream oss(std::ios::binary);
678 boost::archive::binary_oarchive oa(oss); oa << br;
679 entry.branch_record_blob = oss.str(); }
680
681 entry.cid = new_cid;
682 }
683 seen_cids.insert(entry.cid);
684 }
685 }
686
687 // Build DETCheckpoint
688 DETCheckpoint ckpt;
691
692 // Store restart states and branch record blobs per CID (collision-free after the renumbering above)
693 for (auto& entry : all_held) {
694 if (ckpt.restart_states.find(entry.cid) == ckpt.restart_states.end()) {
695 ckpt.restart_states[entry.cid] = entry.rinfo;
696 ckpt.branch_record_blobs[entry.cid] = entry.branch_record_blob;
697 }
698 }
699
700 // Move each restart blob out of its RestartNodeInfo into source_blobs so
701 // the archive stores it exactly once (MPI held entries are 1:1 with their
702 // sources).
703 for (auto& [cid, ri] : ckpt.restart_states) {
704 if (ri && !ri->relap_restart_buf.empty()) {
705 ckpt.source_blobs[cid] = std::move(ri->relap_restart_buf);
706 ri->relap_restart_buf.clear();
707 ri->relap_restart_buf.shrink_to_fit();
708 ckpt.cid_source[cid] = cid;
709 }
710 }
711
712 // Convert tasks_queue to pending_tasks with per-task data
713 std::queue<RestartTask> temp = tasks_queue;
714 while (!temp.empty()) {
715 auto& task = temp.front();
716 PendingBranch pb;
717 pb.cid = 0;
718 pb.branch_index = task.branch_index;
719 pb.sequence_id = task.sequence_id;
720 pb.parent_sequence_id = task.parent_sequence_id;
721
722 std::shared_ptr<RestartNodeInfo> task_rinfo;
723 for (auto& entry : all_held) {
724 if (entry.source_rank == task.source_rank && entry.local_id == task.local_id) {
725 pb.cid = entry.cid;
726 task_rinfo = entry.rinfo;
727 break;
728 }
729 }
730 ckpt.pending_tasks.push_back(pb);
731 ckpt.task_restart_states.push_back(task_rinfo);
732 // task_branch_blobs stays empty: resume always uses the merged
733 // branch_center_blob and nothing reads per-task copies.
734 temp.pop();
735 }
736
737 // Chained resume: tasks inherited from a prior checkpoint that were never
738 // dispatched this run must carry over, or their entire subtree is lost
739 // from the DET. Their restart states/blobs still live in resume_ckpt_
740 // (freed only at dispatch). Carried source blobs get negative keys so
741 // they can never collide with this run's CID-keyed sources.
742 if (resume_ckpt_ && !resume_pending_.empty()) {
743 PU::print_message(rank, "[CHECKPOINT] Carrying over " + std::to_string(resume_pending_.size())
744 + " undispatched inherited tasks", PG::log);
745 for (size_t i = 0; i < resume_pending_.size(); ++i) {
746 const auto& pb = resume_pending_[i];
747 ckpt.pending_tasks.push_back(pb);
748 std::shared_ptr<RestartNodeInfo> ri =
749 (i < resume_ckpt_->task_restart_states.size()) ? resume_ckpt_->task_restart_states[i] : nullptr;
750 ckpt.task_restart_states.push_back(ri);
751 // Serial resume reads restart_states (CID-keyed), not the
752 // per-task vector — register the carried rinfo there too.
753 if (ri && !ckpt.restart_states.count(pb.cid))
754 ckpt.restart_states[pb.cid] = ri;
755 auto cs = resume_ckpt_->cid_source.find(pb.cid);
756 if (cs != resume_ckpt_->cid_source.end()) {
757 int carried_key = -(cs->second) - 1;
758 ckpt.cid_source[pb.cid] = carried_key;
759 auto sb = resume_ckpt_->source_blobs.find(cs->second);
760 if (sb != resume_ckpt_->source_blobs.end() && !ckpt.source_blobs.count(carried_key))
761 ckpt.source_blobs[carried_key] = sb->second; // copy: this run continues
762 }
763 }
764 }
765
766 // Merge all client BranchRecords into one combined branch_center_blob.
767 //
768 // CIDs are per-lineage counters: every daughter lineage continues its
769 // donor's counter, so two DIFFERENT physical clusters routinely share a
770 // CID across gathered records. The merge therefore keys clusters by their
771 // true identity: the branch-0 ID, minted exactly once at creation and
772 // immutable. Same-CID clusters with different identities both survive;
773 // pending-task targets keep their CID (task/rinfo alignment), displaced
774 // non-targets are renumbered, and mother links are repaired via the
775 // globally unique branch IDs. A CID alone does not identify a cluster: a
776 // fully explored ancestor sharing a pending target's CID would displace
777 // the target, and the resumed task would re-run an explored branch while
778 // its real branch never ran.
779 {
780 std::map<int, int> pending_min_bi;
781 for (auto& pt : ckpt.pending_tasks) {
782 auto it = pending_min_bi.find(pt.cid);
783 if (it == pending_min_bi.end() || pt.branch_index < it->second)
784 pending_min_bi[pt.cid] = pt.branch_index;
785 }
786
787 BranchRecord merged;
788 std::map<int, std::unique_ptr<BranchCluster>> best; // identity -> best version
789 std::map<int, int> reserved_cid; // identity -> pending target CID
790
791 auto identity_of = [](const BranchCluster& bc) {
792 return bc.all_branch.empty() ? -1 : bc.all_branch.front()->getIDOfThisBranch();
793 };
794 // Versions of one physical cluster differ in how many sibling branch
795 // IDs they know (the explorer's copy has the reassigned ID, the
796 // donor's still has the -999 placeholder). Keep the most complete.
797 auto known_ids = [](const BranchCluster& bc) {
798 int n = 0;
799 for (auto& bp : bc.all_branch)
800 if (bp && bp->getIDOfThisBranch() > 0) ++n;
801 return n;
802 };
803 // Versions of one physical cluster carry complementary knowledge —
804 // each explorer's copy knows its own branch's reassigned ID and child
805 // links. Union them so the kept version is the most complete.
806 auto merge_versions = [](BranchCluster& w, BranchCluster& l) {
807 size_t n = std::min(w.all_branch.size(), l.all_branch.size());
808 for (size_t i = 0; i < n; ++i) {
809 auto* wb = w.all_branch[i].get();
810 auto* lb = l.all_branch[i].get();
811 if (!wb || !lb) continue;
812 if (wb->getIDOfThisBranch() <= 0 && lb->getIDOfThisBranch() > 0)
813 wb->setIDOfThisBranch(lb->getIDOfThisBranch());
814 for (int c : lb->child_IDs) {
815 bool have = false;
816 for (int wc : wb->child_IDs)
817 if (wc == c) { have = true; break; }
818 if (!have) wb->child_IDs.push_back(c);
819 }
820 }
821 };
822 auto absorb = [&](BranchRecord& br, int own_target_cid) {
823 for (auto& bc : br.branch_cluster) {
824 if (!bc) continue;
825 int ident = identity_of(*bc);
826 if (own_target_cid != -1 && bc->getIDOfThisBranchCluster() == own_target_cid
827 && !reserved_cid.count(ident))
828 reserved_cid[ident] = own_target_cid;
829 auto it = best.find(ident);
830 if (it == best.end()) {
831 best[ident] = std::move(bc);
832 } else if (known_ids(*bc) > known_ids(*it->second)) {
833 merge_versions(*bc, *it->second);
834 it->second = std::move(bc);
835 } else {
836 merge_versions(*it->second, *bc);
837 }
838 }
841 };
842
843 for (auto& [cid, blob] : ckpt.branch_record_blobs) {
844 BranchRecord br;
845 std::istringstream iss(blob, std::ios::binary);
846 boost::archive::binary_iarchive ia(iss);
847 ia >> br;
848 // Only the blob that OWNS target CID `cid` may reserve it — a
849 // foreign record's same-CID cluster is a different physical node.
850 absorb(br, cid);
851 }
852 // Chained resume: fold the prior checkpoint's merged record in too.
853 // Carried-over tasks' clusters (and closed ancestors) may not appear
854 // in any held record gathered this run. Carried tasks' targets live
855 // in the prior merged record, so they reserve their CIDs here.
856 if (resume_ckpt_ && !resume_ckpt_->branch_center_blob.empty()) {
857 std::set<int> carried_targets;
858 for (auto& pt : ckpt.pending_tasks)
859 if (!ckpt.branch_record_blobs.count(pt.cid))
860 carried_targets.insert(pt.cid);
861 BranchRecord br;
862 std::istringstream iss(resume_ckpt_->branch_center_blob, std::ios::binary);
863 boost::archive::binary_iarchive ia(iss);
864 ia >> br;
865 for (auto& bc : br.branch_cluster) {
866 if (!bc) continue;
867 int ident = identity_of(*bc);
868 int c = bc->getIDOfThisBranchCluster();
869 if (carried_targets.count(c) && !reserved_cid.count(ident))
870 reserved_cid[ident] = c;
871 auto it = best.find(ident);
872 if (it == best.end()) {
873 best[ident] = std::move(bc);
874 } else if (known_ids(*bc) > known_ids(*it->second)) {
875 merge_versions(*bc, *it->second);
876 it->second = std::move(bc);
877 } else {
878 merge_versions(*it->second, *bc);
879 }
880 }
883 }
884
885 // Assign final CIDs: reserved identities take their pending target CID
886 // (the collision renumbering of held entries above may have changed it
887 // inside the owning blob while foreign copies of the same cluster kept
888 // the unrenumbered CID); others keep their CID unless a
889 // reserved/earlier cluster holds it.
890 std::set<int> used_cids;
891 int next_cid = 0;
892 for (auto& [ident, bc] : best)
893 next_cid = std::max(next_cid, bc->getIDOfThisBranchCluster());
894 for (auto& [ident, cid] : reserved_cid) {
895 used_cids.insert(cid);
896 next_cid = std::max(next_cid, cid);
897 }
898 next_cid += 1000;
899
900 std::map<int, std::pair<int, BranchCluster*>> branch_to_cluster; // branch id -> (final cid, cluster)
901 for (auto& [ident, bc] : best) {
902 int final_cid;
903 auto rit = reserved_cid.find(ident);
904 if (rit != reserved_cid.end()) {
905 final_cid = rit->second;
906 } else {
907 final_cid = bc->getIDOfThisBranchCluster();
908 if (used_cids.count(final_cid)) {
909 int old = final_cid;
910 final_cid = next_cid++;
911 PU::print_message(rank, "[CHECKPOINT] CID " + std::to_string(old)
912 + " names two physical clusters — renumbering non-target copy to "
913 + std::to_string(final_cid), PG::log);
914 }
915 used_cids.insert(final_cid);
916 }
917 bc->setIDOfThisBranchCluster(final_cid);
918 for (auto& bp : bc->all_branch)
919 if (bp && bp->getIDOfThisBranch() > 0)
920 branch_to_cluster[bp->getIDOfThisBranch()] = {final_cid, bc.get()};
921 }
922
923 // Repair mother links via the globally unique mother BRANCH id —
924 // initiationForRestart appends new child branch IDs through this link,
925 // so a stale CID would silently drop tree linkage in resumed output.
926 for (auto& [ident, bc] : best) {
927 int mb = bc->getIDOfTheMotherBranch();
928 auto mit = branch_to_cluster.find(mb);
929 if (mb > 0 && mit != branch_to_cluster.end()) {
930 bc->setIDOfMotherCluster(mit->second.first);
931 for (auto& bp : mit->second.second->all_branch)
932 if (bp && bp->getIDOfThisBranch() == mb) {
933 bp->setIDOfTheChildBranchCluster(bc->getIDOfThisBranchCluster());
934 break;
935 }
936 }
937 }
938
939 for (auto& [ident, bc] : best) {
940 auto pit = pending_min_bi.find(bc->getIDOfThisBranchCluster());
941 if (pit != pending_min_bi.end()) {
942 bc->setCurrentBranchNum(pit->second - 1);
943 } else {
944 bc->setCurrentBranchNum(static_cast<int>(bc->all_branch.size()) - 1);
945 }
946 merged.branch_cluster.push_back(std::move(bc));
947 }
948 // Set max CID so resumed workers assign new cluster CIDs above existing ones.
949 // Without this, new clusters start at CID=0 which triggers the "first cluster"
950 // check in checkAndAddBranchCluster(), resetting probability to 1.0.
951 {
952 int max_cid = -1;
953 for (auto& bc : merged.branch_cluster)
954 max_cid = std::max(max_cid, bc->getIDOfThisBranchCluster());
956 }
957 // Branch IDs are IMMUTABLE for the life of a node: the checkpoint
958 // stores them exactly as issued. PE files are keyed by these IDs, so
959 // a resumed client re-saves an ancestor node under the same name
960 // phase 1 used. Both resume paths are ID-agnostic: serial continues
961 // above the recorded max (set below), MPI windows seed in a fresh
962 // epoch above max_branch_id.
963 {
964 int orig_max_branch_id = -1;
965 for (auto& bc : merged.branch_cluster)
966 for (auto& bp : bc->all_branch)
967 if (bp->getIDOfThisBranch() > orig_max_branch_id)
968 orig_max_branch_id = bp->getIDOfThisBranch();
969 merged.setIDOfRecentGeneratedBranch(orig_max_branch_id);
970 ckpt.max_branch_id = orig_max_branch_id;
971 }
972
973 std::ostringstream oss(std::ios::binary);
974 boost::archive::binary_oarchive oa(oss);
975 oa << merged;
976 ckpt.branch_center_blob = oss.str();
977 }
978
979 // Store completed reports from viz state
980 if (viz_state_) {
981 std::lock_guard<std::mutex> lock(viz_state_->mu);
982 ckpt.completed_reports = viz_state_->completed;
983 }
984
987 std::string ckpt_path = g_sim.config.checkpoint_dir + "/checkpoint_seq_" + std::to_string(sequences_completed_) + ".dat";
988 ckpt.save(ckpt_path);
989
990 for (auto& pt : ckpt.pending_tasks) {
991 PU::print_message(rank, "[CHECKPOINT] pending: CID=" + std::to_string(pt.cid)
992 + " bi=" + std::to_string(pt.branch_index)
993 + " seq=" + std::to_string(pt.sequence_id), PG::log);
994 }
995 PU::print_message(rank, "[CHECKPOINT] Saved: " + ckpt_path
996 + " (" + std::to_string(sequences_completed_) + " sequences, "
997 + std::to_string(ckpt.pending_tasks.size()) + " pending, "
998 + std::to_string(ckpt.restart_states.size()) + " restart states)", PG::log);
999 std::cout << "Resume with: --resume=" << ckpt_path << std::endl;
1000}
1001
1003{
1005 // Refuse to resume against changed configuration inputs.
1006 if (!ckpt.config_matches(g_sim.config_fingerprint)) {
1007 std::ostringstream msg;
1008 msg << "FATAL: checkpoint configuration mismatch (checkpoint fingerprint 0x"
1009 << std::hex << ckpt.config_fingerprint << ", current inputs 0x"
1010 << g_sim.config_fingerprint << std::dec
1011 << "). The input deck, Input/ tree, or ZCtrlPar.txt changed since this "
1012 << "checkpoint was written. Restore the original inputs or start a fresh run.";
1013 PU::print_message(rank, msg.str(), PG::log);
1014 std::cerr << msg.str() << std::endl;
1015 MPI_Abort(MPI_COMM_WORLD, 1);
1016 }
1017 if (ckpt.version >= 3 && ckpt.compress_queue != g_sim.config.compress_queue) {
1018 PU::print_message(rank, "[RESUME] Overriding --compress-queue to match the checkpoint ("
1019 + std::string(ckpt.compress_queue ? "on" : "off") + ")", PG::log);
1020 g_sim.config.compress_queue = ckpt.compress_queue;
1021 }
1022 sequences_completed_ = ckpt.sequences_completed;
1023 sequence_counter_ = ckpt.sequence_counter;
1024
1025 // Restore viz state
1026 if (viz_state_) {
1027 std::lock_guard<std::mutex> lock(viz_state_->mu);
1028 viz_state_->completed = ckpt.completed_reports;
1029 }
1030
1031 PU::print_message(rank, "[RESUME] Loaded: " + std::to_string(ckpt.sequences_completed)
1032 + " sequences done, " + std::to_string(ckpt.pending_tasks.size()) + " pending tasks"
1033 + " seq_counter=" + std::to_string(ckpt.sequence_counter), PG::log);
1034
1035 // Derive this resumed run's branch-ID epoch. Phase-1 identities
1036 // (rank-offset IDs preserved in merged PE outputs and recorded as
1037 // max_branch_id) must never be re-issued; every phase-2 rank window
1038 // starts in a fresh epoch above them. The epoch base is the first
1039 // multiple of the stride (128 * 100000 IDs, i.e. 128 rank windows of
1040 // 100000 IDs) above the highest ID issued so far; a rank's window then
1041 // starts at epoch_base + rank * 100000.
1042 // The record only spans pending lineages, so also scan the merged PE
1043 // filenames, the ground truth of every ID any prior phase issued.
1044 {
1045 int max_issued_bid = ckpt.max_branch_id;
1046 int max_file_bid = -1;
1047 bool has_zero = false, has_negative = false;
1048 long nonneg_count = 0;
1049 std::string pe_dir = g_sim.SScheduler.getPEDirectory();
1050 if (fs::is_directory(pe_dir))
1051 for (auto& e : fs::recursive_directory_iterator(pe_dir)) {
1052 std::string fname = e.path().filename().string();
1053 if (e.is_regular_file() && fname.size() > 7 && fname.substr(0, 7) == "ZPENode"
1054 && fname.back() == 't') {
1055 // ZPENodes_agg.txt matches this filter; stoi("agg") must not throw.
1056 int fid = 0;
1057 try { fid = std::stoi(fname.substr(fname.find('_') + 1, fname.find('.') - fname.find('_') - 1)); }
1058 catch (...) { continue; }
1059 if (fid > max_issued_bid) max_issued_bid = fid;
1060 if (fid > max_file_bid) max_file_bid = fid;
1061 if (fid == 0) has_zero = true;
1062 if (fid < 0) has_negative = true;
1063 if (fid >= 0) ++nonneg_count;
1064 }
1065 }
1066 // A finalized Output (end-of-run sequential rename) is dense 0..N-1
1067 // with the -999 donor sentinels stripped; a checkpoint-stop state is
1068 // sparse (rank-window IDs) and carries the sentinel while work is
1069 // pending. Resuming onto a finalized tree would corrupt it
1070 // (natural-ID re-saves land next to renamed files), so refuse.
1071 if (has_zero && !has_negative
1072 && nonneg_count == static_cast<long>(max_file_bid) + 1) {
1073 std::string msg = "FATAL: the project Output/ already contains a finalized run "
1074 "(sequentially renamed PE files). This checkpoint's run has completed; "
1075 "resuming would corrupt the merged outputs. Move the existing Output/ "
1076 "aside to re-run from this checkpoint.";
1077 PU::print_message(rank, msg, PG::log);
1078 std::cerr << msg << std::endl;
1079 MPI_Abort(MPI_COMM_WORLD, 1);
1080 }
1081 constexpr int kEpochStride = 128 * 100000;
1082 int epoch_base = (std::max(max_issued_bid, 0) / kEpochStride + 1) * kEpochStride;
1083 for (auto& ri : ckpt.task_restart_states)
1084 if (ri) ri->id_epoch_base = epoch_base;
1085 PU::print_message(rank, "[RESUME] Branch-ID epoch base = " + std::to_string(epoch_base)
1086 + " (max issued so far: " + std::to_string(max_issued_bid) + ")", PG::log);
1087 }
1088
1089 // Count tasks per source so blobs can be freed at last dispatch.
1090 resume_source_refs_.clear();
1091 for (auto& pt : ckpt.pending_tasks) {
1092 auto cs = ckpt.cid_source.find(pt.cid);
1093 if (cs != ckpt.cid_source.end())
1094 ++resume_source_refs_[cs->second];
1095 }
1096
1097 // Store checkpoint for dispatch_one_resume_task (uses per-task indexing)
1098 resume_pending_ = ckpt.pending_tasks;
1099 resume_ckpt_ = std::make_unique<DETCheckpoint>(std::move(ckpt));
1100
1101 // Dispatch as many as we can now (one per free client)
1102 while (!resume_pending_.empty() && num_free_clients > 0)
1104}
1105
1107{
1108 if (resume_pending_.empty() || !resume_ckpt_) return false;
1109 int free_rank = find_next_free_client_round_robin();
1110 if (free_rank < 0) return false;
1111
1112 // Pop from back — index into per-task vectors using current size
1113 int ti = static_cast<int>(resume_pending_.size()) - 1;
1114 auto task = resume_pending_.back();
1115 resume_pending_.pop_back();
1116
1117 // Per-task data is indexed the same as pending_tasks in the checkpoint.
1118 // resume_pending_ starts as a copy of ckpt.pending_tasks and only ever
1119 // shrinks from the back, so ti (its size before the pop, minus 1) is also
1120 // the task's index into the checkpoint's per-task vectors.
1121
1122 if (ti < 0 || ti >= static_cast<int>(resume_ckpt_->task_restart_states.size()) ||
1123 !resume_ckpt_->task_restart_states[ti]) {
1124 PU::print_message(rank, "[RESUME] ERROR: no data for task index=" + std::to_string(ti), PG::log);
1125 clients_busy[free_rank] = 0;
1127 return false;
1128 }
1129
1130 auto& rinfo = resume_ckpt_->task_restart_states[ti];
1131
1132 int seq_id = task.sequence_id > 0 ? task.sequence_id : sequence_counter_++;
1133
1134 // The restart blob lives once in source_blobs: attach it to the
1135 // RestartNodeInfo transiently for the wire and detach afterwards so the
1136 // server never accumulates per-task copies.
1137 {
1138 auto cs = resume_ckpt_->cid_source.find(task.cid);
1139 if (cs != resume_ckpt_->cid_source.end()) {
1140 auto sb = resume_ckpt_->source_blobs.find(cs->second);
1141 if (sb != resume_ckpt_->source_blobs.end())
1142 rinfo->relap_restart_buf = sb->second;
1143 }
1144 }
1145 PU::send_object(*rinfo, free_rank, PG::client_restart_run, MPI_COMM_WORLD);
1146 rinfo->relap_restart_buf.clear();
1147 rinfo->relap_restart_buf.shrink_to_fit();
1148 // Free the source blob once its last referencing task is dispatched, so
1149 // the server's hold on checkpoint blobs shrinks as the resume proceeds.
1150 {
1151 auto cs = resume_ckpt_->cid_source.find(task.cid);
1152 if (cs != resume_ckpt_->cid_source.end()) {
1153 auto rc = resume_source_refs_.find(cs->second);
1154 if (rc != resume_source_refs_.end() && --rc->second <= 0) {
1155 resume_ckpt_->source_blobs.erase(cs->second);
1156 resume_source_refs_.erase(rc);
1157 }
1158 }
1159 resume_ckpt_->task_restart_states[ti].reset();
1160 }
1161 // Every resumed task is dispatched with the MERGED BranchRecord
1162 // (branch_center_blob: one version of each cluster, branch IDs exactly as
1163 // issued). write_checkpoint stores no per-task records.
1164 BranchRecord br;
1165 {
1166 std::istringstream iss(resume_ckpt_->branch_center_blob, std::ios::binary);
1167 boost::archive::binary_iarchive ia(iss);
1168 ia >> br;
1169 }
1170 PU::send_object(br, free_rank, PG::client_restart_run, MPI_COMM_WORLD);
1171 int path_len = 0;
1172 MPI_Send(&path_len, 1, MPI_INT, free_rank, PG::client_restart_run, MPI_COMM_WORLD);
1173 char empty = '\0';
1174 MPI_Send(&empty, 0, MPI_CHAR, free_rank, PG::client_restart_run, MPI_COMM_WORLD);
1175 MPI_Send(&seq_id, 1, MPI_INT, free_rank, PG::client_restart_run, MPI_COMM_WORLD);
1176 MPI_Send(&task.branch_index, 1, MPI_INT, free_rank, PG::client_restart_run, MPI_COMM_WORLD);
1177
1178 rank_current_sequence_[free_rank] = seq_id;
1179
1180 PU::print_message(rank, "[RESUME] Dispatched CID=" + std::to_string(task.cid)
1181 + " bi=" + std::to_string(task.branch_index) + " seq=" + std::to_string(seq_id)
1182 + " -> C[" + std::to_string(free_rank) + "]", PG::log);
1183 return true;
1184}
long get_rss_mb()
Get current process RSS in MB (Linux + macOS, returns 0 elsewhere).
static void mpi_sigint_handler(int)
Definition: Server.cpp:28
static void mpi_sigusr1_handler(int)
Definition: Server.cpp:24
static volatile sig_atomic_t mpi_sigint_received
Definition: Server.cpp:27
static int show_mpi_interrupt_menu(int seq_completed, int queue_depth, int busy_clients)
Definition: Server.cpp:31
static volatile sig_atomic_t mpi_checkpoint_requested
Definition: Server.cpp:23
MPI server (rank 0): dispatches DET sequences to worker clients.
SimulationContext g_sim
Global simulation context: the single instance, defined in SimulationContext.cpp.
document e
HTTP server for real-time DET visualization on MPI rank 0.
std::vector< std::unique_ptr< BranchProbability > > all_branch
The alternatives of this branching point; index 0 is the one the generating sequence continues on.
void setIDOfRecentGeneratedES(int)
int getIDOfRecentGeneratedES()
void setIDOfRecentGeneratedBranchCluster(int)
void setIDOfRecentGeneratedBranch(int)
std::vector< std::unique_ptr< BranchCluster > > branch_cluster
All branching points generated so far, in creation order.
Definition: BranchRecord.h:284
void readInfoFromFile()
void initiationForSimulation()
KB knowledge_base
Definition: Operator.h:127
EventSymptomMatrix diagnosis_matrix
Definition: Operator.h:129
void initiationForSimulation()
Definition: Operator.cpp:79
void initiationForSimulation()
Definition: Procedures.cpp:42
std::string getProjectDirectory()
Definition: Scheduler.cpp:595
std::string getESDirectory()
Definition: Scheduler.cpp:601
std::string getPEDirectory()
Definition: Scheduler.cpp:604
void initiationForSimulation(ConsoleMode iMode=ConsoleMode::Console)
Prepares the RELAP5 bridge and the working directory for a run. Call it after parseOptions().
Definition: Scheduler.cpp:364
std::string getOutputDirectory()
Definition: Scheduler.cpp:598
std::string getKPSDirectory()
Definition: Scheduler.cpp:607
void merge_outputs()
Merge per-rank Output/ directories into the shared project Output/.
Definition: Server.cpp:460
bool dispatch_one_resume_task()
Dispatch the last task of resume_pending_ to a free client.
Definition: Server.cpp:1106
std::shared_ptr< TreeState > viz_state_
Definition: Server.h:196
std::unordered_map< int, int > rank_current_sequence_
rank -> sequence_id being run
Definition: Server.h:199
int next_candidate
Round-robin pointer (starts at rank 1)
Definition: Server.h:167
void Shutdown()
Send client_shutdown to every client rank and stop the visualization server. Under –merge=now each cl...
Definition: Server.cpp:77
std::queue< RestartTask > tasks_queue
Pending metadata-only restart tasks.
Definition: Server.h:178
Server(int rank, int viz_port=0)
Set up the client table and, when viz_port is nonzero, start the visualization server; also runs Sche...
Definition: Server.cpp:50
int rank
This process rank (always 0)
Definition: Server.h:162
std::unique_ptr< DETCheckpoint > resume_ckpt_
Loaded –resume checkpoint (null in a fresh run)
Definition: Server.h:202
int find_next_free_client_round_robin()
Claim the next free client, round-robin from next_candidate. The returned client is marked busy and n...
Definition: Server.cpp:376
~Server()
Defined in Server.cpp (VisualizationServer complete type required)
int sequence_counter_
Next sequence ID to assign (0 is the initial sequence)
Definition: Server.h:163
int find_free_client_except(int exclude_rank)
Claim the next free client via round-robin, skipping exclude_rank.
Definition: Server.cpp:391
void Simulation()
Main server loop: poll for messages, dispatch tasks, until the tree is complete.
Definition: Server.cpp:86
std::map< int, int > resume_source_refs_
Undispatched resume tasks per checkpoint source blob.
Definition: Server.h:166
void dispatch_available_tasks()
Make one pass over the task queue.
Definition: Server.cpp:406
int viz_port_
Definition: Server.h:198
int num_free_clients
Number of currently idle clients.
Definition: Server.h:160
int last_busy_
last logged busy-worker count
Definition: Server.h:193
std::ofstream occupancy_log_
Definition: Server.h:190
double occupancy_t0_
MPI_Wtime() when the log was opened [s].
Definition: Server.h:191
int last_queue_
last logged depth, to suppress no-op rows
Definition: Server.h:192
bool stopped_at_checkpoint_
Run ended at a checkpoint: outputs are intermediate.
Definition: Server.h:159
int last_checkpoint_seq_
Sequence count at the last checkpoint written from the message path (-1 = none)
Definition: Server.h:165
void write_checkpoint()
Gather held branch data from every client and write <checkpoint_dir>/checkpoint_seq_<sequences_comple...
Definition: Server.cpp:612
MPI_Status status_server
Definition: Server.h:180
int sequences_completed_
Total sequences finished so far.
Definition: Server.h:164
bool checkpoint_pending_
Checkpoint requested, waiting for clients to drain.
Definition: Server.h:158
std::vector< PendingBranch > resume_pending_
Undispatched tasks; consumed from the back, index-aligned with resume_ckpt_->task_restart_states.
Definition: Server.h:203
void dispatch_resume_tasks()
Load the –resume checkpoint and dispatch its pending tasks.
Definition: Server.cpp:1002
std::unique_ptr< int[]> clients_busy
Per-client busy flag (0=free, 1=busy)
Definition: Server.h:179
std::unique_ptr< VisualizationServer > viz_server_
Definition: Server.h:197
void initiationForSimulation()
bool log
Enable/disable rank-specific logging.
@ client_send_to_peer
Server -> Client A: send held data to another client.
@ server_branch_meta
Client A -> Server: branch metadata only (rank-local branch id + num_branches)
@ client_run_held
Server -> Client A: run task using own held data.
@ client_peer_assign
Server -> Client A: peer assignment list for direct sends.
@ client_shutdown
Server -> Client: no more work, shut down.
@ server_done_simulating
Client -> Server: empty completion signal, client is free. The server handles it; Client....
@ server_done_with_report
Client -> Server: sequence completed with SequenceReport payload.
@ server_send_complete
Client -> Server: data send finished, client is free again.
@ client_simulate_this
Server -> Client: run the initial (first) sequence.
@ client_restart_run
Server -> Client: run a pending task of a resumed checkpoint with server-provided data.
@ client_checkpoint
Server -> Client: send held_data for checkpoint gather.
@ server_output_data
Client -> Server: SequenceOutputData (before report)
int size
Total number of MPI processes.
int rank
MPI rank of this process.
size_t build_es_summary(const std::string &dest_es)
Rebuild <dest_es>ES_summary.txt by scanning the destination ES tree.
Definition: OutputMerge.h:265
std::vector< std::pair< int, std::string > > collect(const std::string &pe_dir)
Collect every PE node under pe_dir (recursively), from either layout, reading all node content into m...
Definition: PEOutput.h:156
size_t rewrite_sequential(const std::string &pe_dir, std::vector< std::pair< int, std::string > > entries, bool aggregate)
Replace the contents of pe_dir with entries renumbered 0..N-1 in ascending id order (the content path...
Definition: PEOutput.h:199
const char * aggregate_basename()
Basename of the aggregate file ("ZPENodes_agg.txt"). It is in the MPI merge's append-files list,...
Definition: PEOutput.h:59
size_t finalize_sequential(const std::string &pe_dir, bool aggregate)
End-of-run finalize: renumber every PE node 0..N-1 in id order, ending in the same layout rewrite_seq...
Definition: PEOutput.h:254
void set_shared_procedures(const Procedures *master)
void set_shared_kb(int operator_type, const KB *master)
void set_shared_control_panel(const ControlPanel *master)
void set_shared_diagnosis_matrix(int operator_type, const EventSymptomMatrix *master)
constexpr int ODM
Definition: SimConstants.h:145
constexpr int OAT
Definition: SimConstants.h:144
Complete state of a dynamic event tree run at a sequence boundary.
Definition: DETCheckpoint.h:45
std::map< int, std::string > branch_record_blobs
cid -> serialized BranchRecord of the holding client; filled by the MPI writer only and used there to...
Definition: DETCheckpoint.h:94
std::map< int, std::vector< char > > source_blobs
source key -> restart blob
Definition: DETCheckpoint.h:71
std::vector< SequenceReport > completed_reports
Reports of finished sequences (filled only when the visualization server is on)
int sequences_completed
Sequences finished when the checkpoint was taken.
Definition: DETCheckpoint.h:50
std::string branch_center_blob
Definition: DETCheckpoint.h:80
uint64_t config_fingerprint
Definition: DETCheckpoint.h:57
void save(const std::string &path)
Write the checkpoint to path.
std::vector< PendingBranch > pending_tasks
Unexplored branches.
Definition: DETCheckpoint.h:97
std::map< int, std::shared_ptr< RestartNodeInfo > > restart_states
cid -> snapshot; read by serial resume
Definition: DETCheckpoint.h:93
static DETCheckpoint load(const std::string &path)
Read a checkpoint file.
std::map< int, int > cid_source
Definition: DETCheckpoint.h:72
std::vector< std::shared_ptr< RestartNodeInfo > > task_restart_states
Snapshot per pending task (null = none); read by MPI resume.
Definition: DETCheckpoint.h:98
One unexplored branch recorded in a checkpoint.
Definition: DETCheckpoint.h:25
int cid
Branch cluster ID the branch belongs to.
Definition: DETCheckpoint.h:26
int sequence_id
Global sequence ID already assigned by the MPI server; 0 = assign at dispatch (serial checkpoints)
Definition: DETCheckpoint.h:28
int parent_sequence_id
Sequence that generated the branch point (-1 = unknown)
Definition: DETCheckpoint.h:29
int branch_index
Index of the alternative within the cluster (1..N-1)
Definition: DETCheckpoint.h:27
Per-sequence output payload sent from client to server via MPI.
Report sent by client when a sequence completes (for DET visualization).
A metadata-only restart task: the data stays on the source client.
Definition: Server.h:170
int sequence_id
Assigned sequence number.
Definition: Server.h:173
int local_id
Rank-local branch-point ID, the key of the data held on the source client.
Definition: Server.h:172
int parent_sequence_id
Sequence that branched to create this task.
Definition: Server.h:175
int source_rank
Client that holds the branch data.
Definition: Server.h:171
int branch_index
Which alternative branch to run (1..N-1)
Definition: Server.h:174
std::string resume_file
–resume checkpoint file as an absolute path (empty = fresh start)
Definition: SimConfig.h:31
std::string checkpoint_dir
–checkpoint-dir as an absolute path, created at startup (empty = disabled)
Definition: SimConfig.h:28
bool checkpoint_stop
–checkpoint-stop: exit after the first checkpoint written; requires checkpoint_interval > 0
Definition: SimConfig.h:30
int checkpoint_interval
–checkpoint-interval: checkpoint every N completed sequences (0 = disabled); requires checkpoint_dir
Definition: SimConfig.h:29
int merge_mode
Definition: SimConfig.h:38
bool compress_queue
–compress-queue (default off): LZ4-compress RELAP restart buffers held and sent between MPI ranks
Definition: SimConfig.h:22
bool aggregate_pe_output
Definition: SimConfig.h:23
std::string occupancy_log
–occupancy-log path: CSV of server queue/worker occupancy (empty = disabled, MPI only)....
Definition: SimConfig.h:37
Scheduler SScheduler
Simulation scheduling, parses CLI options.
Procedures All_procedure
Emergency operating procedures.
OperatorAT Action_Taker
Crew action execution model.
ControlPanel Indicator
Control room panel state.
OperatorDM Decision_Maker
Crew decision-making model.
SystemModule Plant
Maps RELAP5 card numbers to C++ objects.
uint64_t config_fingerprint
FNV-1a of immutable config inputs (see ConfigFingerprint.h); 0 = not computed.
ConvertToString Translator
Parameter name/value conversion.