← Documentation
ADS-IDAC Engine 5.0.0
OutputMerge.h
Go to the documentation of this file.
1
8//
9// PURPOSE. Tree compute and output consolidation need not share one wall
10// clock. Inside an allocation the merge runs in parallel with the workers'
11// self-merge (--merge=now). With these helpers it can also run as a separate
12// `--merge-only` process against a project whose run ended with
13// `--merge=defer`, or whose merge was interrupted.
14//
15// Everything is MPI-free by design: `--merge-only` runs serially (with an
16// optional thread pool over rank directories), and the serial-only build
17// (ENABLE_MPI=OFF) compiles this header without the parallelexec target.
18// All functions report filesystem errors on stderr or ignore them; none throws
19// on a failed move. They assume no run and no other merge is writing the
20// project at the same time.
21//
22// Layout contracts preserved (see docs/developer/output-layouts.md):
23// ES/KPS keep their `<Kind>Folder_<id/1000>/` buckets and exact file names;
24// the finalized PE layout is a single `PEFolder_0/` with dense sequential ids
25// and no -999 sentinels; the three analysis logs are per-sequence blocks in
26// any rank order; the PE aggregate destination is append-only because it
27// accumulates across checkpoint phases.
28//
29#ifndef ADSIDAC_OUTPUTMERGE_H
30#define ADSIDAC_OUTPUTMERGE_H
31
32#include <algorithm>
33#include <atomic>
34#include <filesystem>
35#include <fstream>
36#include <iostream>
37#include <limits>
38#include <set>
39#include <sstream>
40#include <string>
41#include <thread>
42#include <unordered_set>
43#include <utility>
44#include <vector>
45
46#include "PEOutput.h"
47
48namespace adsidac::merge {
49
50namespace fs = std::filesystem;
51
55inline const char* manifest_basename() { return "MERGE_PENDING.txt"; }
56
60inline std::vector<std::string> append_basenames() {
61 return {"Scenario info.txt", "Procedure data.txt", "Scan data.txt",
63}
64
66inline bool is_append_basename(const std::string& fname) {
67 for (const auto& af : append_basenames())
68 if (fname == af) return true;
69 return false;
70}
71
75inline std::vector<int> discover_ranks(const std::string& project_dir) {
76 std::vector<int> ranks;
77 std::error_code ec;
78 for (auto& e : fs::directory_iterator(project_dir, ec)) {
79 if (!e.is_directory()) continue;
80 const std::string n = e.path().filename().string();
81 if (n.rfind("rank_", 0) != 0) continue;
82 try {
83 const int r = std::stoi(n.substr(5));
84 if (r > 0) ranks.push_back(r);
85 } catch (...) {}
86 }
87 std::sort(ranks.begin(), ranks.end());
88 return ranks;
89}
90
100inline void move_file(const fs::path& src, const fs::path& dest,
101 std::unordered_set<std::string>& made_dirs) {
102 std::error_code ec;
103 if (made_dirs.insert(dest.parent_path().string()).second)
104 fs::create_directories(dest.parent_path(), ec);
105 std::error_code mec;
106 fs::rename(src, dest, mec);
107 if (mec) {
108 mec.clear();
109 fs::copy(src, dest, fs::copy_options::overwrite_existing, mec);
110 if (mec)
111 std::cerr << "[MERGE] failed to move " << src << ": " << mec.message() << std::endl;
112 else
113 fs::remove(src, mec);
114 }
115}
116
125inline void move_rank_es_kps(const std::string& project_dir, int r) {
126 std::error_code ec;
127 const fs::path rank_out = fs::path(project_dir) / ("rank_" + std::to_string(r)) / "Output";
128 const fs::path shared_out = fs::path(project_dir) / "Output";
129 if (!fs::exists(rank_out, ec)) return;
130 std::unordered_set<std::string> made_dirs;
131 for (const char* kind : {"ES", "KPS"}) {
132 const fs::path src_root = rank_out / kind;
133 if (!fs::exists(src_root, ec)) continue;
134 for (auto& entry : fs::recursive_directory_iterator(src_root, ec)) {
135 if (ec) break;
136 if (!entry.is_regular_file()) continue;
137 const std::string fname = entry.path().filename().string();
138 if (is_append_basename(fname)) continue;
139 move_file(entry.path(), shared_out / kind / fs::relative(entry.path(), src_root),
140 made_dirs);
141 }
142 }
143}
144
152inline void move_rank_pe_files(const std::string& project_dir, const std::vector<int>& ranks) {
153 std::error_code ec;
154 const fs::path shared_pe = fs::path(project_dir) / "Output" / "PE";
155 std::unordered_set<std::string> made_dirs;
156 for (int r : ranks) {
157 const fs::path src_root = fs::path(project_dir) / ("rank_" + std::to_string(r))
158 / "Output" / "PE";
159 if (!fs::exists(src_root, ec)) continue;
160 for (auto& entry : fs::recursive_directory_iterator(src_root, ec)) {
161 if (ec) break;
162 if (!entry.is_regular_file()) continue;
163 const std::string fname = entry.path().filename().string();
164 if (is_append_basename(fname)) continue;
165 move_file(entry.path(), shared_pe / fs::relative(entry.path(), src_root), made_dirs);
166 }
167 }
168}
169
184inline void concat_appends_from_ranks(const std::string& project_dir,
185 const std::vector<int>& ranks) {
186 std::error_code ec;
187 const fs::path shared_out = fs::path(project_dir) / "Output";
188 for (const auto& base : append_basenames()) {
189 const bool is_agg = (base == adsidac::pe::aggregate_basename());
190 const char* kind = is_agg ? "PE" : "ES";
191 auto src_of = [&](int r) -> fs::path {
192 const fs::path in_rank = fs::path(project_dir) / ("rank_" + std::to_string(r))
193 / "Output" / kind / base;
194 std::error_code e2;
195 if (fs::exists(in_rank, e2)) return in_rank;
196 const fs::path staged = shared_out / kind / "parts" / (base + ".rank_" + std::to_string(r));
197 if (fs::exists(staged, e2)) return staged;
198 return {};
199 };
200 bool any = false;
201 for (int r : ranks)
202 if (!src_of(r).empty()) { any = true; break; }
203 if (!any) continue;
204 fs::create_directories(shared_out / kind, ec);
205 std::ofstream out(shared_out / kind / base,
206 std::ios::binary | (is_agg ? std::ios::app : std::ios::trunc));
207 size_t agg_raw = 0, agg_unique = 0;
208 for (int r : ranks) {
209 const fs::path src = src_of(r);
210 if (src.empty()) continue;
211 if (is_agg) {
212 // Rank-local compaction before streaming: last-wins over this
213 // rank's own re-saves, so the finalize reads fewer records with
214 // identical resolution (see PEOutput.h). A part staged by a
215 // --merge=now worker is already compacted; re-compacting is a
216 // cheap no-op.
217 const auto [raw, uniq] = adsidac::pe::compact_aggregate(src);
218 agg_raw += raw; agg_unique += uniq;
219 }
220 std::ifstream in(src, std::ios::binary);
221 if (in) { out << in.rdbuf(); in.close(); fs::remove(src, ec); }
222 }
223 if (is_agg && agg_raw > agg_unique)
224 std::cout << "[MERGE] compacted PE aggregates: " << agg_raw << " -> "
225 << agg_unique << " records across " << ranks.size()
226 << " rank dirs" << std::endl;
227 }
228 for (const char* kind : {"ES", "PE"}) {
229 const fs::path parts = shared_out / kind / "parts";
230 std::error_code e2;
231 if (fs::exists(parts, e2) && fs::is_empty(parts, e2))
232 fs::remove(parts, e2);
233 }
234 // Per-rank "err messages.txt" lives in the rank cwd (including the
235 // server's), outside Output/; fold it in. Like the logs above, a rank's
236 // copy may instead sit staged under parts/ if a --merge=now run ended
237 // between staging and concat. Both locations are read for every rank.
238 {
239 std::ofstream out(shared_out / "ES" / "err messages.txt",
240 std::ios::binary | std::ios::app);
241 std::vector<int> with_zero = ranks;
242 with_zero.insert(with_zero.begin(), 0);
243 for (int r : with_zero) {
244 for (const fs::path src : {
245 fs::path(project_dir) / ("rank_" + std::to_string(r)) / "err messages.txt",
246 shared_out / "ES" / "parts" / ("err messages.txt.rank_" + std::to_string(r))}) {
247 std::ifstream in(src, std::ios::binary);
248 if (in) { out << in.rdbuf(); in.close(); fs::remove(src, ec); }
249 }
250 }
251 }
252}
253
265inline size_t build_es_summary(const std::string& dest_es) {
266 std::string summary_path = dest_es + "ES_summary.txt";
267 std::ofstream summary(summary_path, std::ios::out);
268 summary << "ES#\tProbability\tEnd_time\tConsequence#\tDescription\n";
269
270 std::vector<std::pair<int, fs::path>> es_files;
271 std::error_code ec;
272 if (fs::exists(dest_es, ec))
273 for (auto& entry : fs::recursive_directory_iterator(dest_es, ec)) {
274 std::string fname = entry.path().filename().string();
275 if (fname.substr(0, 7) == "ZESNode" && fname.back() == 't') {
276 auto start = fname.find('_') + 1;
277 auto end = fname.find('.');
278 int id = 0;
279 try { id = std::stoi(fname.substr(start, end - start)); }
280 catch (...) { continue; }
281 es_files.push_back({id, entry.path()});
282 }
283 }
284 std::sort(es_files.begin(), es_files.end());
285
286 for (auto& [id, path] : es_files) {
287 std::ifstream fin(path);
288 std::string line;
289 if (std::getline(fin, line)) {
290 // ZESNode format: "ES_N time N mother_id 0 3096 0 probability"
291 std::istringstream iss(line);
292 std::string es_label;
293 double time_val, probability;
294 int es_id, mother_id, z1, z2, z3;
295 iss >> es_label >> time_val >> es_id >> mother_id >> z1 >> z2 >> z3 >> probability;
296 std::string consequence_line, description;
297 if (std::getline(fin, consequence_line)) {
298 std::istringstream css(consequence_line);
299 std::string tag;
300 int cons_type;
301 css >> tag >> cons_type;
302 if (std::getline(fin, description)) {
303 if (!description.empty() && description.front() == '"') description.erase(0, 1);
304 if (!description.empty() && description.back() == '"') description.pop_back();
305 }
306 summary << id << '\t' << probability << '\t' << time_val << '\t'
307 << cons_type << '\t' << description << '\n';
308 }
309 }
310 }
311 return es_files.size();
312}
313
327inline void write_manifest(const std::string& project_dir, int ranks, bool aggregate,
328 long sequences, unsigned long long fingerprint) {
329 std::error_code ec;
330 const fs::path out = fs::path(project_dir) / "Output";
331 fs::create_directories(out, ec);
332 std::ofstream m(out / manifest_basename(), std::ios::trunc);
333 m << "# adsidac deferred merge v1\n"
334 << "ranks " << ranks << "\n"
335 << "aggregate_pe " << (aggregate ? 1 : 0) << "\n"
336 << "sequences " << sequences << "\n"
337 << "config_fingerprint 0x" << std::hex << fingerprint << std::dec << "\n";
338}
339
341inline bool manifest_exists(const std::string& project_dir) {
342 std::error_code ec;
343 return fs::exists(fs::path(project_dir) / "Output" / manifest_basename(), ec);
344}
345
350inline bool manifest_aggregate(const std::string& project_dir, bool fallback) {
351 std::ifstream m(fs::path(project_dir) / "Output" / manifest_basename());
352 std::string key;
353 int val = 0;
354 while (m >> key) {
355 if (key == "aggregate_pe" && (m >> val)) return val != 0;
356 m.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
357 }
358 return fallback;
359}
360
368inline bool looks_finalized(const std::string& pe_dir) {
369 std::error_code ec;
370 if (!fs::exists(pe_dir, ec)) return false;
371 long nonneg = 0;
372 int max_id = -1;
373 bool has_zero = false, has_negative = false, any = false;
374 for (auto& entry : fs::recursive_directory_iterator(pe_dir, ec)) {
375 if (!entry.is_regular_file()) continue;
376 const std::string fname = entry.path().filename().string();
377 std::vector<int> ids;
378 if (fname == adsidac::pe::aggregate_basename()) {
379 std::vector<std::pair<int, std::string>> recs;
380 adsidac::pe::parse_aggregate(entry.path(), recs);
381 for (auto& [id, c] : recs) ids.push_back(id);
382 } else if (fname.size() > 7 && fname.compare(0, 7, "ZPENode") == 0
383 && fname.back() == 't') {
384 try {
385 ids.push_back(std::stoi(fname.substr(fname.find('_') + 1,
386 fname.find('.') - fname.find('_') - 1)));
387 } catch (...) { continue; }
388 }
389 for (int id : ids) {
390 any = true;
391 if (id > max_id) max_id = id;
392 if (id == 0) has_zero = true;
393 if (id < 0) has_negative = true;
394 if (id >= 0) ++nonneg;
395 }
396 }
397 return any && has_zero && !has_negative && nonneg == static_cast<long>(max_id) + 1;
398}
399
404inline bool ranks_hold_outputs(const std::string& project_dir, const std::vector<int>& ranks) {
405 std::error_code ec;
406 for (int r : ranks) {
407 const fs::path out = fs::path(project_dir) / ("rank_" + std::to_string(r)) / "Output";
408 if (!fs::exists(out, ec)) continue;
409 for (auto& entry : fs::recursive_directory_iterator(out, ec)) {
410 if (ec) break;
411 if (!entry.is_regular_file()) continue;
412 const std::string f = entry.path().filename().string();
413 if (f.rfind("ZESNode", 0) == 0 || f.rfind("ZKPS", 0) == 0
414 || f.rfind("ZDIA", 0) == 0 || f.rfind("ZPIF", 0) == 0
415 || f.rfind("ZPENode", 0) == 0 || is_append_basename(f))
416 return true;
417 }
418 }
419 return false;
420}
421
440inline std::pair<size_t, size_t> merge_project(const std::string& project_dir,
441 bool aggregate, int threads) {
442 const std::vector<int> ranks = discover_ranks(project_dir);
443 const std::string dest_es = (fs::path(project_dir) / "Output" / "ES").string() + "/";
444 const std::string dest_pe = (fs::path(project_dir) / "Output" / "PE").string() + "/";
445
446 if (threads < 1) threads = 1;
447 std::atomic<size_t> next{0};
448 auto worker = [&] {
449 for (size_t i = next.fetch_add(1); i < ranks.size(); i = next.fetch_add(1))
450 move_rank_es_kps(project_dir, ranks[i]);
451 };
452 if (threads == 1 || ranks.size() <= 1) {
453 worker();
454 } else {
455 std::vector<std::thread> pool;
456 const int n = std::min<int>(threads, static_cast<int>(ranks.size()));
457 pool.reserve(n);
458 for (int t = 0; t < n; ++t) pool.emplace_back(worker);
459 for (auto& th : pool) th.join();
460 }
461 move_rank_pe_files(project_dir, ranks);
462 concat_appends_from_ranks(project_dir, ranks);
463 const size_t seqs = build_es_summary(dest_es);
464 const size_t pe_count = adsidac::pe::finalize_sequential(dest_pe, aggregate);
465 std::error_code ec;
466 fs::remove(fs::path(project_dir) / "Output" / manifest_basename(), ec);
467 return {seqs, pe_count};
468}
469
470} // namespace adsidac::merge
471
472#endif // ADSIDAC_OUTPUTMERGE_H
Pivotal-event output layouts (one file per node, or self-delimiting records in one aggregate per rank...
document e
void move_rank_es_kps(const std::string &project_dir, int r)
Move one rank's ES and KPS files into the shared tree, keeping their paths relative to Output/ES and ...
Definition: OutputMerge.h:125
std::vector< std::string > append_basenames()
Base names of the outputs that are merged by concatenation rather than by moving: the three analysis ...
Definition: OutputMerge.h:60
void move_file(const fs::path &src, const fs::path &dest, std::unordered_set< std::string > &made_dirs)
Move one file: rename (replacing an existing dest), with copy+remove fallback for EXDEV or exotic fil...
Definition: OutputMerge.h:100
const char * manifest_basename()
File name of the sentinel written to Output/ by –merge=defer. Its presence means the rank directories...
Definition: OutputMerge.h:55
void concat_appends_from_ranks(const std::string &project_dir, const std::vector< int > &ranks)
Concatenate every rank's append files into the shared Output/.
Definition: OutputMerge.h:184
std::pair< size_t, size_t > merge_project(const std::string &project_dir, bool aggregate, int threads)
The full standalone merge, in this order: ES/KPS moves (threaded over rank dirs), rank-ordered PE mov...
Definition: OutputMerge.h:440
bool looks_finalized(const std::string &pe_dir)
True when pe_dir already carries the finalized signature: at least one node, ids dense 0....
Definition: OutputMerge.h:368
bool manifest_exists(const std::string &project_dir)
True if <project_dir>/Output/MERGE_PENDING.txt exists.
Definition: OutputMerge.h:341
void write_manifest(const std::string &project_dir, int ranks, bool aggregate, long sequences, unsigned long long fingerprint)
Write the –merge=defer manifest <project_dir>/Output/MERGE_PENDING.txt (Output/ is created if needed;...
Definition: OutputMerge.h:327
bool manifest_aggregate(const std::string &project_dir, bool fallback)
The aggregate_pe flag the deferred run recorded in its manifest.
Definition: OutputMerge.h:350
bool is_append_basename(const std::string &fname)
True if fname (a base name, no directory) is one of append_basenames().
Definition: OutputMerge.h:66
bool ranks_hold_outputs(const std::string &project_dir, const std::vector< int > &ranks)
True when any rank_N/Output tree still holds mergeable output files (names starting with ZESNode,...
Definition: OutputMerge.h:404
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< int > discover_ranks(const std::string &project_dir)
Worker rank numbers N for which <project_dir>/rank_N is a directory.
Definition: OutputMerge.h:75
void move_rank_pe_files(const std::string &project_dir, const std::vector< int > &ranks)
Rank-ordered move of per-file PE nodes into Output/PE.
Definition: OutputMerge.h:152
void parse_aggregate(const fs::path &path, std::vector< std::pair< int, std::string > > &out)
Parse one aggregate file, appending (id, content) to out in file order, duplicates and negative ids i...
Definition: PEOutput.h:88
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
std::pair< size_t, size_t > compact_aggregate(const fs::path &agg_path)
Rewrite one rank's aggregate file last-wins per id, sorted by id.
Definition: PEOutput.h:123
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