arXiv:2412.10665
ho22joshua commited on
Commit
9e4bc69
·
1 Parent(s): a3c512e

making job submission and scripts more general, removing hard coded paths

Browse files
root_gnn_dgl/jobs/cpu.sh CHANGED
@@ -1 +1,5 @@
1
- salloc --nodes=1 --ntasks=64 --cpus-per-task=1 --qos=interactive --time=04:00:00 --constraint=cpu --account=atlas
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ ACCOUNT="${SLURM_ACCOUNT:-atlas}"
4
+
5
+ salloc --nodes=1 --ntasks=64 --cpus-per-task=1 --qos=interactive --time=04:00:00 --constraint=cpu --account="$ACCOUNT"
root_gnn_dgl/jobs/inference/run_inference.py CHANGED
@@ -1,214 +1,277 @@
 
 
 
 
 
 
1
  import os
 
2
  import subprocess
3
- from mpi4py import MPI
 
 
 
 
 
 
 
 
 
4
 
5
- # Define the number of GPUs per node
 
6
  GPUS_PER_NODE = 4
7
 
8
- # Define your files, configs, and branch namesfiles = [
9
- files = [
10
- "merged_aMCPy8_tHjb125_CP_0_AF3_had_scaled.root",
11
- "merged_aMCPy8_tHjb125_CP_0_AF3_lep_scaled.root",
12
- "merged_aMCPy8_tHjb125_CP_0_FullSim_had_scaled.root",
13
- "merged_aMCPy8_tHjb125_CP_0_FullSim_lep_scaled.root",
14
- "merged_aMCPy8_tHjb125_CP_15_AF3_had_scaled.root",
15
- "merged_aMCPy8_tHjb125_CP_15_AF3_lep_scaled.root",
16
- "merged_aMCPy8_tHjb125_CP_30_AF3_had_scaled.root",
17
- "merged_aMCPy8_tHjb125_CP_30_AF3_lep_scaled.root",
18
- "merged_aMCPy8_tHjb125_CP_45_AF3_had_scaled.root",
19
- "merged_aMCPy8_tHjb125_CP_45_AF3_lep_scaled.root",
20
- "merged_aMCPy8_tHjb125_CP_45_FullSim_had_scaled.root",
21
- "merged_aMCPy8_tHjb125_CP_45_FullSim_lep_scaled.root",
22
- "merged_aMCPy8_tHjb125_CP_60_AF3_had_scaled.root",
23
- "merged_aMCPy8_tHjb125_CP_60_AF3_lep_scaled.root",
24
- "merged_aMCPy8_tHjb125_CP_75_AF3_had_scaled.root",
25
- "merged_aMCPy8_tHjb125_CP_75_AF3_lep_scaled.root",
26
- "merged_aMCPy8_tHjb125_CP_90_AF3_had_scaled.root",
27
- "merged_aMCPy8_tHjb125_CP_90_AF3_lep_scaled.root",
28
- "merged_aMCPy8_ttH125_CP_0_AF3_had_scaled.root",
29
- "merged_aMCPy8_ttH125_CP_0_AF3_lep_scaled.root",
30
- "merged_aMCPy8_ttH125_CP_0_FullSim_had_scaled.root",
31
- "merged_aMCPy8_ttH125_CP_0_FullSim_lep_scaled.root",
32
- "merged_aMCPy8_ttH125_CP_15_AF3_had_scaled.root",
33
- "merged_aMCPy8_ttH125_CP_15_AF3_lep_scaled.root",
34
- "merged_aMCPy8_ttH125_CP_30_AF3_had_scaled.root",
35
- "merged_aMCPy8_ttH125_CP_30_AF3_lep_scaled.root",
36
- "merged_aMCPy8_ttH125_CP_45_AF3_had_scaled.root",
37
- "merged_aMCPy8_ttH125_CP_45_AF3_lep_scaled.root",
38
- "merged_aMCPy8_ttH125_CP_45_FullSim_had_scaled.root",
39
- "merged_aMCPy8_ttH125_CP_45_FullSim_lep_scaled.root",
40
- "merged_aMCPy8_ttH125_CP_60_AF3_had_scaled.root",
41
- "merged_aMCPy8_ttH125_CP_60_AF3_lep_scaled.root",
42
- "merged_aMCPy8_ttH125_CP_75_AF3_had_scaled.root",
43
- "merged_aMCPy8_ttH125_CP_75_AF3_lep_scaled.root",
44
- "merged_aMCPy8_ttH125_CP_90_AF3_had_scaled.root",
45
- "merged_aMCPy8_ttH125_CP_90_AF3_lep_scaled.root",
46
- "merged_aMCPy8_tWH125_CP_0_AF3_had_scaled.root",
47
- "merged_aMCPy8_tWH125_CP_0_AF3_lep_scaled.root",
48
- "merged_aMCPy8_tWH125_CP_0_FullSim_had_scaled.root",
49
- "merged_aMCPy8_tWH125_CP_0_FullSim_lep_scaled.root",
50
- "merged_aMCPy8_tWH125_CP_15_AF3_had_scaled.root",
51
- "merged_aMCPy8_tWH125_CP_15_AF3_lep_scaled.root",
52
- "merged_aMCPy8_tWH125_CP_30_AF3_had_scaled.root",
53
- "merged_aMCPy8_tWH125_CP_30_AF3_lep_scaled.root",
54
- "merged_aMCPy8_tWH125_CP_45_AF3_had_scaled.root",
55
- "merged_aMCPy8_tWH125_CP_45_AF3_lep_scaled.root",
56
- "merged_aMCPy8_tWH125_CP_45_FullSim_had_scaled.root",
57
- "merged_aMCPy8_tWH125_CP_45_FullSim_lep_scaled.root",
58
- "merged_aMCPy8_tWH125_CP_60_AF3_had_scaled.root",
59
- "merged_aMCPy8_tWH125_CP_60_AF3_lep_scaled.root",
60
- "merged_aMCPy8_tWH125_CP_75_AF3_had_scaled.root",
61
- "merged_aMCPy8_tWH125_CP_75_AF3_lep_scaled.root",
62
- "merged_aMCPy8_tWH125_CP_90_AF3_had_scaled.root",
63
- "merged_aMCPy8_tWH125_CP_90_AF3_lep_scaled.root",
64
- "merged_hadnti_data.root",
65
- "merged_hadti_data.root",
66
- "merged_lepnti_data.root",
67
- "merged_lepti_data.root",
68
- "merged_MGPy8_ttyy_combined_FullSim_had_scaled.root",
69
- "merged_MGPy8_ttyy_combined_FullSim_lep_scaled.root",
70
- "merged_PhPy8_4FS_A14NNPDF_bbH125_FullSim_had_scaled.root",
71
- "merged_PhPy8_4FS_A14NNPDF_bbH125_FullSim_lep_scaled.root",
72
- "merged_PhPy8_ggH125_NNLOPS_FullSim_had_scaled.root",
73
- "merged_PhPy8_ggH125_NNLOPS_FullSim_lep_scaled.root",
74
- "merged_PhPy8_ggZH125_FullSim_had_scaled.root",
75
- "merged_PhPy8_ggZH125_FullSim_lep_scaled.root",
76
- "merged_PhPy8_VBFH125_FullSim_had_scaled.root",
77
- "merged_PhPy8_VBFH125_FullSim_lep_scaled.root",
78
- "merged_PhPy8_WmH125J_MINLO_FullSim_had_scaled.root",
79
- "merged_PhPy8_WmH125J_MINLO_FullSim_lep_scaled.root",
80
- "merged_PhPy8_WpH125J_MINLO_FullSim_had_scaled.root",
81
- "merged_PhPy8_WpH125J_MINLO_FullSim_lep_scaled.root",
82
- "merged_PhPy8_ZH125J_MINLO_FullSim_had_scaled.root",
83
- "merged_PhPy8_ZH125J_MINLO_FullSim_lep_scaled.root",
84
- ]
85
- # Configurations for each type of file
86
- config = [
87
- "configs/model_configs/run_3_ttH/v06/sb_yukawa_no_weights.yaml",
88
- "configs/model_configs/run_3_ttH/v06/sb_yukawa_weights.yaml",
89
- "configs/model_configs/run_3_ttH/v06/tHjb_CP_no_weights.yaml",
90
- "configs/model_configs/run_3_ttH/v06/tHjb_CP_weights.yaml",
91
- "configs/model_configs/run_3_ttH/v06/ttH_CP_no_weights.yaml",
92
- "configs/model_configs/run_3_ttH/v06/ttH_CP_weights.yaml",
93
- "configs/model_configs/run_3_ttH/v06/ttH_tHjb_tWH_CP_no_weights.yaml",
94
- "configs/model_configs/run_3_ttH/v06/ttH_tHjb_tWH_CP_weights.yaml",
95
- "configs/model_configs/run_3_ttH/v06/tWH_CP_no_weights.yaml",
96
- "configs/model_configs/run_3_ttH/v06/tWH_CP_weights.yaml",
97
-
98
- ]
99
-
100
- # Branch names for each type of file
101
- branch_name = [
102
- "sb_yukawa_no_weights_score",
103
- "sb_yukawa_weights_score",
104
- "tHjb_CP_no_weights_score",
105
- "tHjb_CP_weights_score",
106
- "ttH_CP_no_weights_score",
107
- "ttH_CP_weights_score",
108
- "ttH_tHjb_tWH_CP_no_weights_score",
109
- "ttH_tHjb_tWH_CP_weights_score",
110
- "tWH_CP_no_weights_score",
111
- "tWH_CP_weights_score",
112
- ]
113
-
114
- # MPI Initialization
115
- comm = MPI.COMM_WORLD
116
- rank = comm.Get_rank() # Process rank (unique ID for each process)
117
- size = comm.Get_size() # Total number of processes across all nodes
118
-
119
- # Master process manages the task queue
120
- def master():
121
- num_workers = size - 1 # Exclude the master process
122
- task_queue = files[:] # Copy the list of files
123
- workers_busy = [False] * num_workers # Track if workers are busy
124
-
125
- print(f"Master process starting with {num_workers} workers.")
126
-
127
- # Distribute initial tasks to workers
128
- for worker_rank in range(1, size):
129
- if task_queue:
130
- # Send a file to the worker
131
- file = task_queue.pop(0)
132
- comm.send(file, dest=worker_rank, tag=0)
133
- workers_busy[worker_rank - 1] = True
134
- else:
135
- # No tasks left, send termination signal
136
- comm.send(None, dest=worker_rank, tag=1)
137
-
138
- # Process results and assign new tasks dynamically
139
- while any(workers_busy):
140
- # Receive results from workers
141
- status = MPI.Status()
142
- result = comm.recv(source=MPI.ANY_SOURCE, tag=MPI.ANY_TAG, status=status)
143
- worker_rank = status.Get_source()
144
- workers_busy[worker_rank - 1] = False # Worker is now free
145
-
146
- # Assign a new task if available
147
- if task_queue:
148
- file = task_queue.pop(0)
149
- comm.send(file, dest=worker_rank, tag=0)
150
- workers_busy[worker_rank - 1] = True
151
  else:
152
- # No tasks left, send termination signal
153
- comm.send(None, dest=worker_rank, tag=1)
154
-
155
- print("Master process: All tasks completed.")
156
-
157
- # Worker processes handle tasks dynamically
158
- def worker():
159
- gpu_id = (rank - 1) % GPUS_PER_NODE # Assign GPU ID based on rank
160
- print(f"Worker {rank} starting, GPU ID: {gpu_id}", flush=True)
161
-
162
- while True:
163
- # Receive a task from the master
164
- file = comm.recv(source=0, tag=MPI.ANY_TAG)
165
- if file is None:
166
- # Termination signal received
167
- print(f"Worker {rank}: Termination signal received.")
168
- break
169
-
170
- # Initialize config and branch_name for each file
171
- local_config = config[:] # Create a copy of the global config
172
- local_branch_name = branch_name[:] # Create a copy of the global branch_name
173
-
174
- # Determine file type and append appropriate configurations
175
- if "had" in file:
176
- local_config += had_config # Append had-specific configurations
177
- local_branch_name += had_branch_name # Append had-specific branch names
178
- elif "lep" in file:
179
- local_config += lep_config # Append lep-specific configurations
180
- local_branch_name += lep_branch_name # Append lep-specific branch names
181
-
182
- # Process the file
183
- print(f"Worker {rank}: Processing file {file} on GPU {gpu_id}", flush=True)
184
- print(f"branches = {local_branch_name}")
185
- target = f"/global/cfs/projectdirs/atlas/joshua/ttHCP/ntuples/v02/preselection/merged/{file}"
186
- destination = f"/global/cfs/projectdirs/atlas/joshua/ttHCP/ntuples/scores/gnn_v06/{file}"
187
-
188
- # Set the GPU for this process
189
- os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
190
-
191
- # Build the command
192
- command = [
193
- "python", "scripts/inference.py",
194
- "--target", target,
195
- "--destination", destination,
196
- "--config", *local_config,
197
- "--branch_name", *local_branch_name,
198
- "--chunks", "1",
199
- "--chunkno", "0",
200
- "--write"
201
- ]
202
-
203
- # Execute the command
204
- subprocess.run(command)
205
-
206
- # Notify the master that the task is complete
207
- comm.send(f"Worker {rank}: Finished {file}", dest=0, tag=0)
208
-
209
- # Main function
210
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  if rank == 0:
212
- master()
213
- else:
214
- worker()
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import glob
7
  import os
8
+ import re
9
  import subprocess
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ import yaml
15
+
16
+ try:
17
+ from mpi4py import MPI
18
+ except ImportError:
19
+ MPI = None
20
 
21
+
22
+ REPO_ROOT = Path(__file__).resolve().parents[2]
23
  GPUS_PER_NODE = 4
24
 
25
+
26
+ @dataclass(frozen=True)
27
+ class InferenceTask:
28
+ target: Path
29
+ destination: Path
30
+
31
+
32
+ def load_config(path: Path) -> dict:
33
+ with path.open("r", encoding="utf-8") as handle:
34
+ return yaml.safe_load(handle)
35
+
36
+
37
+ def repo_relative(path: Path) -> str:
38
+ try:
39
+ return str(path.resolve().relative_to(REPO_ROOT))
40
+ except ValueError:
41
+ return str(path)
42
+
43
+
44
+ def resolve_path(raw_path: str) -> Path:
45
+ path = Path(raw_path).expanduser()
46
+ if path.is_absolute():
47
+ return path.resolve()
48
+
49
+ repo_path = (REPO_ROOT / path).resolve()
50
+ if repo_path.exists() or str(raw_path).startswith(("configs/", "jobs/", "scripts/")):
51
+ return repo_path
52
+
53
+ return (Path.cwd() / path).resolve()
54
+
55
+
56
+ def sanitize_branch_name(name: str) -> str:
57
+ cleaned = re.sub(r"\W+", "_", name).strip("_")
58
+ if not cleaned:
59
+ cleaned = "gnn"
60
+ if cleaned[0].isdigit():
61
+ cleaned = f"gnn_{cleaned}"
62
+ return f"{cleaned}_score"
63
+
64
+
65
+ def discover_model_configs(raw_configs: list[str], raw_config_dirs: list[str]) -> list[Path]:
66
+ configs = []
67
+ for raw_config in raw_configs:
68
+ config = resolve_path(raw_config)
69
+ if not config.is_file():
70
+ raise FileNotFoundError(f"Config file does not exist: {config}")
71
+ configs.append(config)
72
+
73
+ for raw_dir in raw_config_dirs:
74
+ config_dir = resolve_path(raw_dir)
75
+ if not config_dir.is_dir():
76
+ raise NotADirectoryError(f"Config directory does not exist: {config_dir}")
77
+ configs.extend(sorted(path.resolve() for path in config_dir.glob("*.yaml")))
78
+
79
+ unique_configs = []
80
+ seen = set()
81
+ for config in configs:
82
+ if config not in seen:
83
+ unique_configs.append(config)
84
+ seen.add(config)
85
+ if not unique_configs:
86
+ raise ValueError("No model configs were provided.")
87
+ return unique_configs
88
+
89
+
90
+ def default_branch_names(config_paths: list[Path]) -> list[str]:
91
+ branch_names = []
92
+ for config_path in config_paths:
93
+ config = load_config(config_path)
94
+ training_name = config.get("Training_Name") or config_path.stem
95
+ branch_names.append(sanitize_branch_name(str(training_name)))
96
+ return branch_names
97
+
98
+
99
+ def discover_targets_from_config(sample_config_path: Path) -> list[Path]:
100
+ config = load_config(sample_config_path)
101
+ targets = []
102
+
103
+ for dataset_name, dataset_config in config.get("Datasets", {}).items():
104
+ args = dataset_config.get("args", {})
105
+ raw_dir = args.get("raw_dir")
106
+ file_names = args.get("file_names")
107
+ if not raw_dir or not file_names:
108
+ raise ValueError(f"Dataset {dataset_name} is missing raw_dir or file_names.")
109
+
110
+ patterns = file_names if isinstance(file_names, list) else [file_names]
111
+ for pattern in patterns:
112
+ matches = sorted(Path(path).resolve() for path in glob.glob(os.path.join(raw_dir, pattern)))
113
+ if not matches:
114
+ raise FileNotFoundError(f"No files matched {os.path.join(raw_dir, pattern)}")
115
+ targets.extend(matches)
116
+
117
+ unique_targets = []
118
+ seen = set()
119
+ for target in targets:
120
+ if target not in seen:
121
+ unique_targets.append(target)
122
+ seen.add(target)
123
+ return unique_targets
124
+
125
+
126
+ def discover_targets(raw_targets: list[str], sample_config: str | None) -> list[Path]:
127
+ targets = []
128
+ if sample_config:
129
+ targets.extend(discover_targets_from_config(resolve_path(sample_config)))
130
+
131
+ for raw_target in raw_targets:
132
+ matches = sorted(Path(path).resolve() for path in glob.glob(str(resolve_path(raw_target))))
133
+ if matches:
134
+ targets.extend(matches)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  else:
136
+ target = resolve_path(raw_target)
137
+ if not target.exists():
138
+ raise FileNotFoundError(f"Target does not exist: {target}")
139
+ targets.append(target)
140
+
141
+ unique_targets = []
142
+ seen = set()
143
+ for target in targets:
144
+ if target not in seen:
145
+ unique_targets.append(target)
146
+ seen.add(target)
147
+ if not unique_targets:
148
+ raise ValueError("No inference targets were provided.")
149
+ return unique_targets
150
+
151
+
152
+ def build_tasks(targets: list[Path], output_dir: Path) -> list[InferenceTask]:
153
+ output_dir.mkdir(parents=True, exist_ok=True)
154
+ return [InferenceTask(target=target, destination=(output_dir / target.name).resolve()) for target in targets]
155
+
156
+
157
+ def build_command(
158
+ task: InferenceTask,
159
+ config_paths: list[Path],
160
+ branch_names: list[str],
161
+ args: argparse.Namespace,
162
+ ) -> list[str]:
163
+ command = [
164
+ sys.executable,
165
+ "scripts/inference.py",
166
+ "--target",
167
+ str(task.target),
168
+ "--destination",
169
+ str(task.destination),
170
+ "--config",
171
+ *[str(path) for path in config_paths],
172
+ "--branch_name",
173
+ *branch_names,
174
+ "--chunks",
175
+ str(args.chunks),
176
+ "--chunkno",
177
+ str(args.chunkno),
178
+ ]
179
+
180
+ if args.write:
181
+ command.append("--write")
182
+ if args.clobber:
183
+ command.append("--clobber")
184
+ if args.ckpt is not None:
185
+ command.extend(["--ckpt", str(args.ckpt)])
186
+ if args.var:
187
+ command.extend(["--var", args.var])
188
+ if args.mode:
189
+ command.extend(["--mode", args.mode])
190
+ if args.tree:
191
+ command.extend(["--tree", args.tree])
192
+ return command
193
+
194
+
195
+ def run_task(
196
+ task: InferenceTask,
197
+ config_paths: list[Path],
198
+ branch_names: list[str],
199
+ args: argparse.Namespace,
200
+ rank: int,
201
+ ) -> int:
202
+ gpu_id = rank % GPUS_PER_NODE
203
+ env = os.environ.copy()
204
+ env["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
205
+ command = build_command(task, config_paths, branch_names, args)
206
+
207
+ print(
208
+ f"[rank {rank}] target={task.target} destination={task.destination} gpu={gpu_id}",
209
+ flush=True,
210
+ )
211
+ print(f"[rank {rank}] command={' '.join(command)}", flush=True)
212
+ if args.test:
213
+ return 0
214
+ return subprocess.run(command, cwd=REPO_ROOT, env=env).returncode
215
+
216
+
217
+ def parse_args() -> argparse.Namespace:
218
+ parser = argparse.ArgumentParser(
219
+ description="Run scripts/inference.py over many ROOT files with many model configs."
220
+ )
221
+ parser.add_argument("--sample-config", help="Config whose dataset file_names define inference targets.")
222
+ parser.add_argument("--target", nargs="*", default=[], help="Target ROOT files or glob patterns.")
223
+ parser.add_argument("--config", nargs="*", default=[], help="Model config files to score.")
224
+ parser.add_argument("--config-dir", nargs="*", default=[], help="Directories containing model YAML configs.")
225
+ parser.add_argument("--branch-name", nargs="*", default=[], help="Score branch names. Defaults to Training_Name_score.")
226
+ parser.add_argument("--output-dir", required=True, help="Directory for output ROOT/NPZ files.")
227
+ parser.add_argument("--chunks", type=int, default=1)
228
+ parser.add_argument("--chunkno", type=int, default=0)
229
+ parser.add_argument("--write", action="store_true", help="Write ROOT files with score branches.")
230
+ parser.add_argument("--clobber", action="store_true")
231
+ parser.add_argument("--ckpt", type=int, default=None, help="Checkpoint epoch. Omit to let scripts/inference.py choose best epoch.")
232
+ parser.add_argument("--var", default="Test_AUC")
233
+ parser.add_argument("--mode", default="max")
234
+ parser.add_argument("--tree", default="")
235
+ parser.add_argument("--test", action="store_true", help="Print planned commands without running inference.")
236
+ return parser.parse_args()
237
+
238
+
239
+ def main() -> int:
240
+ args = parse_args()
241
+ config_paths = discover_model_configs(args.config, args.config_dir)
242
+ branch_names = args.branch_name or default_branch_names(config_paths)
243
+ if len(branch_names) != len(config_paths):
244
+ raise ValueError("--branch-name count must match the number of model configs.")
245
+
246
+ targets = discover_targets(args.target, args.sample_config)
247
+ tasks = build_tasks(targets, resolve_path(args.output_dir))
248
+
249
+ print(f"Repo root: {REPO_ROOT}", flush=True)
250
+ print(f"Targets: {len(tasks)}", flush=True)
251
+ print(f"Model configs: {len(config_paths)}", flush=True)
252
+ print("Branches:", " ".join(branch_names), flush=True)
253
+
254
+ if MPI is None:
255
+ failures = 0
256
+ for task in tasks:
257
+ failures += int(run_task(task, config_paths, branch_names, args, rank=0) != 0)
258
+ return 1 if failures else 0
259
+
260
+ comm = MPI.COMM_WORLD
261
+ rank = comm.Get_rank()
262
+ size = comm.Get_size()
263
+
264
+ failures = 0
265
+ for index, task in enumerate(tasks):
266
+ if index % size != rank:
267
+ continue
268
+ failures += int(run_task(task, config_paths, branch_names, args, rank=rank) != 0)
269
+
270
+ total_failures = comm.allreduce(failures, op=MPI.SUM)
271
  if rank == 0:
272
+ print(f"Completed {len(tasks) - total_failures} task(s); failed {total_failures}.", flush=True)
273
+ return 1 if total_failures else 0
274
+
275
+
276
+ if __name__ == "__main__":
277
+ sys.exit(main())
root_gnn_dgl/jobs/interactive.sh CHANGED
@@ -1 +1,5 @@
1
- salloc --nodes 1 --qos shared_interactive --time 04:00:00 --constraint gpu --account=atlas --gres=gpu:1
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ ACCOUNT="${SLURM_ACCOUNT:-atlas}"
4
+
5
+ salloc --nodes 1 --qos shared_interactive --time 04:00:00 --constraint gpu --account="$ACCOUNT" --gres=gpu:1
root_gnn_dgl/jobs/prep_data/run_processing.py CHANGED
@@ -1,7 +1,9 @@
1
  from concurrent.futures import ThreadPoolExecutor, as_completed
 
2
  import yaml
3
  import os
4
  import subprocess
 
5
 
6
  def extract_dataset_keys_and_chunks(yaml_file):
7
  """
@@ -34,12 +36,11 @@ def call_bash_script(config, dataset_key, shuffle_chunks, bash_script):
34
  """
35
  try:
36
  # Construct the command to call the bash script
37
- command = f"{bash_script} {config} {dataset_key} {shuffle_chunks}"
38
 
39
- print(f"Executing command: {command}") # Debugging: Print the command being executed
40
 
41
- # Run the command
42
- result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
43
  if result.stdout:
44
  print(result.stdout, end="")
45
  if result.stderr:
@@ -55,7 +56,7 @@ def process_yaml_file(config, base_directory, bash_script):
55
  """
56
  Process a single YAML file by extracting datasets and calling the bash script in parallel.
57
  """
58
- yaml_file = os.path.join(base_directory, config) # Construct full path
59
  if os.path.exists(yaml_file):
60
  print(f"Processing file: {config}")
61
  datasets = extract_dataset_keys_and_chunks(yaml_file)
@@ -77,18 +78,19 @@ def process_yaml_file(config, base_directory, bash_script):
77
  return [f"File not found: {yaml_file}"]
78
 
79
  def main():
80
- # Base directory containing the YAML files
81
- base_directory = os.getcwd() + "/"
 
 
 
 
 
 
82
 
83
- # List of YAML configuration files (relative to the base directory)
84
- configs = [
85
- # "configs/stats_100K/pretraining_multiclass.yaml",
86
- "configs/stats_100K/ttH_CP_even_vs_odd.yaml",
87
- # "configs/stats_all/pretraining_multiclass.yaml",
88
- ]
89
-
90
- # Path to the bash script to be called
91
- bash_script = base_directory + "jobs/prep_data/prep_data.sh"
92
 
93
  # Use ThreadPoolExecutor to process YAML files concurrently
94
  max_workers = os.cpu_count() # Use all available CPU cores
 
1
  from concurrent.futures import ThreadPoolExecutor, as_completed
2
+ import argparse
3
  import yaml
4
  import os
5
  import subprocess
6
+ from pathlib import Path
7
 
8
  def extract_dataset_keys_and_chunks(yaml_file):
9
  """
 
36
  """
37
  try:
38
  # Construct the command to call the bash script
39
+ command = [bash_script, config, dataset_key, str(shuffle_chunks)]
40
 
41
+ print(f"Executing command: {' '.join(command)}")
42
 
43
+ result = subprocess.run(command, check=True, capture_output=True, text=True)
 
44
  if result.stdout:
45
  print(result.stdout, end="")
46
  if result.stderr:
 
56
  """
57
  Process a single YAML file by extracting datasets and calling the bash script in parallel.
58
  """
59
+ yaml_file = os.path.join(base_directory, config)
60
  if os.path.exists(yaml_file):
61
  print(f"Processing file: {config}")
62
  datasets = extract_dataset_keys_and_chunks(yaml_file)
 
78
  return [f"File not found: {yaml_file}"]
79
 
80
  def main():
81
+ parser = argparse.ArgumentParser(description="Run prep_data.sh for every dataset in one or more configs.")
82
+ parser.add_argument("configs", nargs="+", help="YAML config files, relative to the repo root or absolute.")
83
+ parser.add_argument(
84
+ "--bash-script",
85
+ default=None,
86
+ help="Path to prep_data.sh. Defaults to jobs/prep_data/prep_data.sh under the repo root.",
87
+ )
88
+ args = parser.parse_args()
89
 
90
+ repo_root = Path(__file__).resolve().parents[2]
91
+ base_directory = str(repo_root) + "/"
92
+ configs = [str(Path(config).resolve()) if Path(config).is_absolute() else config for config in args.configs]
93
+ bash_script = args.bash_script or str(repo_root / "jobs/prep_data/prep_data.sh")
 
 
 
 
 
94
 
95
  # Use ThreadPoolExecutor to process YAML files concurrently
96
  max_workers = os.cpu_count() # Use all available CPU cores
root_gnn_dgl/jobs/salloc.sh CHANGED
@@ -1 +1,5 @@
1
- salloc --nodes 4 --qos interactive --time 04:00:00 --constraint gpu --account=atlas --gres=gpu:4
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ ACCOUNT="${SLURM_ACCOUNT:-atlas}"
4
+
5
+ salloc --nodes 4 --qos interactive --time 04:00:00 --constraint gpu --account="$ACCOUNT" --gres=gpu:4
root_gnn_dgl/jobs/training/conda/run_job.sh CHANGED
@@ -3,26 +3,32 @@
3
  #SBATCH -C gpu
4
  #SBATCH -q shared
5
  #SBATCH -t 15:00:00
6
- #SBATCH -A atlas
7
- #SBATCH -o /global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/jobs/slurm/%j.out # STDOUT
8
 
9
  CONFIG=$1
10
  shift
11
- ARGUEMENTS="$*"
12
 
13
- DIRECTORY="/global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/"
14
- BASE_COMMAND="$DIRECTORY/training_script.py $ARGUEMENTS --preshuffle --nocompile --lazy --config $DIRECTORY"
15
 
16
- echo "launched image"
17
- cd $DIRECTORY
18
 
19
  eval "$(conda shell.bash hook)"
20
- conda init bash
21
- conda activate /opt/conda/envs/dgl
22
 
23
- COMMAND="$BASE_COMMAND$CONFIG"
 
 
 
 
 
 
 
 
24
 
25
  echo "Running my script now"
26
- echo $COMMAND
27
- python -u $COMMAND
28
- echo "Done"
 
3
  #SBATCH -C gpu
4
  #SBATCH -q shared
5
  #SBATCH -t 15:00:00
6
+ #SBATCH -o jobs/slurm/%j.out # STDOUT
 
7
 
8
  CONFIG=$1
9
  shift
10
+ ARGUMENTS=("$@")
11
 
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
14
 
15
+ cd "$REPO_ROOT"
16
+ mkdir -p jobs/slurm
17
 
18
  eval "$(conda shell.bash hook)"
19
+ conda activate "${ROOT_GNN_CONDA_ENV:-dgl}"
 
20
 
21
+ COMMAND=(
22
+ "$REPO_ROOT/scripts/training_script.py"
23
+ "${ARGUMENTS[@]}"
24
+ "--preshuffle"
25
+ "--nocompile"
26
+ "--lazy"
27
+ "--config"
28
+ "$CONFIG"
29
+ )
30
 
31
  echo "Running my script now"
32
+ echo "Executing: python -u ${COMMAND[*]}"
33
+ python -u "${COMMAND[@]}"
34
+ echo "Done"
root_gnn_dgl/jobs/training/conda/submit.sh CHANGED
@@ -1,14 +1,28 @@
 
 
1
  date
2
 
3
- DIRECTORY="/global/cfs/projectdirs/atlas/joshua/root_gnn/root_gnn_dgl/configs/model_configs/"
 
 
 
 
 
 
4
 
5
- configs=(
6
  "configs/stats_all/ttH_CP_even_vs_odd.yaml"
7
  "configs/stats_all/ttH_CP_even_vs_odd_batch_size_2048.yaml"
8
  "configs/stats_all/ttH_CP_even_vs_odd_batch_size_4096.yaml"
9
  "configs/stats_all/ttH_CP_even_vs_odd_batch_size_8192.yaml"
10
  )
11
 
 
 
 
 
 
 
12
  counter=0
13
 
14
  hours=12
@@ -16,8 +30,8 @@ time="${hours}:00:00"
16
 
17
  for job in "${configs[@]}"
18
  do
19
- sbatch --job-name="$job" --time="$time" /global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/jobs/training/conda/run_job.sh "$job"
20
  ((counter++))
21
  done
22
 
23
- echo "Total jobs submitted: $counter"
 
1
+ #!/bin/bash
2
+
3
  date
4
 
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
7
+ RUN_JOB="$REPO_ROOT/jobs/training/conda/run_job.sh"
8
+ SBATCH_ACCOUNT_ARGS=()
9
+ if [ -n "${SLURM_ACCOUNT:-}" ]; then
10
+ SBATCH_ACCOUNT_ARGS=(--account "$SLURM_ACCOUNT")
11
+ fi
12
 
13
+ default_configs=(
14
  "configs/stats_all/ttH_CP_even_vs_odd.yaml"
15
  "configs/stats_all/ttH_CP_even_vs_odd_batch_size_2048.yaml"
16
  "configs/stats_all/ttH_CP_even_vs_odd_batch_size_4096.yaml"
17
  "configs/stats_all/ttH_CP_even_vs_odd_batch_size_8192.yaml"
18
  )
19
 
20
+ if [ "$#" -gt 0 ]; then
21
+ configs=("$@")
22
+ else
23
+ configs=("${default_configs[@]}")
24
+ fi
25
+
26
  counter=0
27
 
28
  hours=12
 
30
 
31
  for job in "${configs[@]}"
32
  do
33
+ sbatch "${SBATCH_ACCOUNT_ARGS[@]}" --job-name="$job" --time="$time" "$RUN_JOB" "$job"
34
  ((counter++))
35
  done
36
 
37
+ echo "Total jobs submitted: $counter"
root_gnn_dgl/jobs/training/podman/run_job.sh CHANGED
@@ -3,12 +3,16 @@
3
  #SBATCH -C "gpu&hbm80g"
4
  #SBATCH -q shared
5
  #SBATCH -t 24:00:00
6
- #SBATCH -A atlas
7
- #SBATCH -o /global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/jobs/slurm/%j.out # STDOUT
8
 
9
- ARGUEMENTS="$*"
10
 
11
- echo "Arguements: $ARGUEMENTS"
 
12
 
13
- ## create a launch image script
14
- source "/global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/setup/launch_image.sh" "/global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/jobs/training/podman/run_job_image.sh" $ARGUEMENTS
 
 
 
 
 
3
  #SBATCH -C "gpu&hbm80g"
4
  #SBATCH -q shared
5
  #SBATCH -t 24:00:00
6
+ #SBATCH -o jobs/slurm/%j.out # STDOUT
 
7
 
8
+ ARGUMENTS=("$@")
9
 
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
12
 
13
+ cd "$REPO_ROOT"
14
+ mkdir -p jobs/slurm
15
+
16
+ echo "Arguments: ${ARGUMENTS[*]}"
17
+
18
+ source "$REPO_ROOT/setup/launch_image.sh" "$REPO_ROOT/jobs/training/podman/run_job_image.sh" "${ARGUMENTS[@]}"
root_gnn_dgl/jobs/training/podman/run_job_image.sh CHANGED
@@ -5,20 +5,21 @@ shift
5
  # Store any other potential arguments safely
6
  OTHER_ARGUEMENTS=("$@")
7
 
8
- DIRECTORY="/global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/"
 
9
 
10
- cd $DIRECTORY
11
 
12
  # Use a bash array to build the command and its arguments
13
  # Each element in the () is a separate argument.
14
  COMMAND_ARGS=(
15
- "$DIRECTORY/scripts/training_script.py"
16
  "${OTHER_ARGUEMENTS[@]}"
17
  "--preshuffle"
18
  "--nocompile"
19
  "--lazy"
20
  "--config"
21
- "$DIRECTORY$CONFIG"
22
  )
23
 
24
  echo "Running my script now"
@@ -28,4 +29,4 @@ echo "Executing: python -u ${COMMAND_ARGS[@]}"
28
  # The "${COMMAND_ARGS[@]}" syntax ensures each element is passed as a distinct argument
29
  python -u "${COMMAND_ARGS[@]}"
30
 
31
- echo "Done"
 
5
  # Store any other potential arguments safely
6
  OTHER_ARGUEMENTS=("$@")
7
 
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
10
 
11
+ cd "$REPO_ROOT"
12
 
13
  # Use a bash array to build the command and its arguments
14
  # Each element in the () is a separate argument.
15
  COMMAND_ARGS=(
16
+ "$REPO_ROOT/scripts/training_script.py"
17
  "${OTHER_ARGUEMENTS[@]}"
18
  "--preshuffle"
19
  "--nocompile"
20
  "--lazy"
21
  "--config"
22
+ "$CONFIG"
23
  )
24
 
25
  echo "Running my script now"
 
29
  # The "${COMMAND_ARGS[@]}" syntax ensures each element is passed as a distinct argument
30
  python -u "${COMMAND_ARGS[@]}"
31
 
32
+ echo "Done"
root_gnn_dgl/jobs/training/podman/submit.sh CHANGED
@@ -1,8 +1,16 @@
 
 
1
  date
2
 
3
- DIRECTORY="/global/cfs/projectdirs/atlas/joshua/root_gnn/root_gnn_dgl/configs/model_configs/"
 
 
 
 
 
 
4
 
5
- configs=(
6
  # "configs/multiclass_pretraining/baseline.yaml"
7
  # "configs/multiclass_pretraining/pretraining_batch_size/multiclass_bs_4096.yaml"
8
  # "configs/multiclass_pretraining/pretraining_hid_size/multiclass_hid_256.yaml"
@@ -47,6 +55,12 @@ configs=(
47
 
48
  )
49
 
 
 
 
 
 
 
50
  counter=0
51
 
52
  hours=24
@@ -54,7 +68,7 @@ time="${hours}:00:00"
54
 
55
  for job in "${configs[@]}"
56
  do
57
- sbatch --job-name="$job" --time="$time" /global/cfs/projectdirs/atlas/joshua/gnn/root_gnn_dgl/jobs/training/podman/run_job.sh "$job"
58
  ((counter++))
59
  done
60
 
 
1
+ #!/bin/bash
2
+
3
  date
4
 
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
7
+ RUN_JOB="$REPO_ROOT/jobs/training/podman/run_job.sh"
8
+ SBATCH_ACCOUNT_ARGS=()
9
+ if [ -n "${SLURM_ACCOUNT:-}" ]; then
10
+ SBATCH_ACCOUNT_ARGS=(--account "$SLURM_ACCOUNT")
11
+ fi
12
 
13
+ default_configs=(
14
  # "configs/multiclass_pretraining/baseline.yaml"
15
  # "configs/multiclass_pretraining/pretraining_batch_size/multiclass_bs_4096.yaml"
16
  # "configs/multiclass_pretraining/pretraining_hid_size/multiclass_hid_256.yaml"
 
55
 
56
  )
57
 
58
+ if [ "$#" -gt 0 ]; then
59
+ configs=("$@")
60
+ else
61
+ configs=("${default_configs[@]}")
62
+ fi
63
+
64
  counter=0
65
 
66
  hours=24
 
68
 
69
  for job in "${configs[@]}"
70
  do
71
+ sbatch "${SBATCH_ACCOUNT_ARGS[@]}" --job-name="$job" --time="$time" "$RUN_JOB" "$job"
72
  ((counter++))
73
  done
74
 
root_gnn_dgl/jobs/training/run_parallel_trainings.py ADDED
@@ -0,0 +1,509 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import re
8
+ import shlex
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ from collections import deque
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ import socket
16
+
17
+ import yaml
18
+
19
+
20
+ REPO_ROOT = Path(__file__).resolve().parents[2]
21
+ DEFAULT_TARGETS = (
22
+ REPO_ROOT / "configs/higgs_production_updated_leptons",
23
+ REPO_ROOT / "configs/triboson",
24
+ )
25
+ FIXED_GPU_TOKENS = ("0", "1", "2", "3")
26
+ DEFAULT_TRAINING_ARGS = ("--preshuffle", "--nocompile", "--lazy")
27
+ FORBIDDEN_TRAINING_ARGS = {"--config", "--multigpu", "--multinode"}
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class ExecutionSlot:
32
+ gpu_token: str
33
+
34
+
35
+ @dataclass
36
+ class TrainingJob:
37
+ config_path: Path
38
+ training_directory: str
39
+ pretraining_path: Path | None
40
+
41
+
42
+ @dataclass
43
+ class ActiveProcess:
44
+ job: TrainingJob
45
+ slot: ExecutionSlot
46
+ log_path: Path
47
+ log_handle: object
48
+ process: subprocess.Popen
49
+
50
+
51
+ def parse_args() -> tuple[argparse.Namespace, list[str]]:
52
+ parser = argparse.ArgumentParser(
53
+ description=(
54
+ "Queue local single-GPU trainings on the current node only. This launcher "
55
+ "is intentionally hard-coded to use GPUs 0,1,2,3 with no srun or multi-node logic."
56
+ ),
57
+ epilog=(
58
+ "Unknown arguments are forwarded to scripts/training_script.py. "
59
+ "Example: python jobs/training/run_parallel_trainings.py --split 1/2 --restart --seed 7"
60
+ ),
61
+ )
62
+ parser.add_argument(
63
+ "targets",
64
+ nargs="*",
65
+ help=(
66
+ "Config files or directories. Defaults to "
67
+ "configs/higgs_production_updated_leptons and configs/triboson."
68
+ ),
69
+ )
70
+ parser.add_argument(
71
+ "--split",
72
+ type=str,
73
+ help=(
74
+ "Run only one deterministic slice of the config queue, formatted as K/N. "
75
+ "Example: --split 1/2 for the first half and --split 2/2 for the second."
76
+ ),
77
+ )
78
+ parser.add_argument(
79
+ "--logs-root",
80
+ type=str,
81
+ default="jobs/slurm/parallel_training_logs",
82
+ help="Directory where launcher stdout/stderr logs should be written.",
83
+ )
84
+ parser.add_argument(
85
+ "--stop-on-failure",
86
+ action="store_true",
87
+ help="Stop launching new trainings after the first failure.",
88
+ )
89
+ parser.add_argument(
90
+ "--ignore-missing-pretraining",
91
+ action="store_true",
92
+ help="Launch even if a finetuning config points to a missing pretraining checkpoint.",
93
+ )
94
+ parser.add_argument(
95
+ "--test",
96
+ action="store_true",
97
+ help="Run preflight checks and print the launch plan without starting trainings.",
98
+ )
99
+ parser.add_argument(
100
+ "--poll-seconds",
101
+ type=float,
102
+ default=10.0,
103
+ help="Seconds between checks for finished trainings.",
104
+ )
105
+
106
+ # Accepted only so older commands do not fail; these are ignored in the
107
+ # single-node hard-coded launcher.
108
+ parser.add_argument("--gpus", type=str, help=argparse.SUPPRESS)
109
+ parser.add_argument("--nodelist", type=str, help=argparse.SUPPRESS)
110
+ parser.add_argument("--local-only", action="store_true", help=argparse.SUPPRESS)
111
+ parser.add_argument("--srun-binary", type=str, help=argparse.SUPPRESS)
112
+ parser.add_argument("--scontrol-binary", type=str, help=argparse.SUPPRESS)
113
+ parser.add_argument("--max-parallel", type=int, help=argparse.SUPPRESS)
114
+ parser.add_argument("--dry-run", action="store_true", help=argparse.SUPPRESS)
115
+
116
+ args, forwarded_args = parser.parse_known_args()
117
+ return args, forwarded_args
118
+
119
+
120
+ def repo_relative(path: Path) -> str:
121
+ try:
122
+ return str(path.resolve().relative_to(REPO_ROOT))
123
+ except ValueError:
124
+ return str(path.resolve())
125
+
126
+
127
+ def resolve_target(raw_target: str) -> Path:
128
+ raw_path = Path(raw_target).expanduser()
129
+ if raw_path.is_absolute():
130
+ return raw_path.resolve()
131
+
132
+ repo_candidate = (REPO_ROOT / raw_path).resolve()
133
+ if repo_candidate.exists():
134
+ return repo_candidate
135
+
136
+ return (Path.cwd() / raw_path).resolve()
137
+
138
+
139
+ def discover_configs(raw_targets: list[str]) -> list[Path]:
140
+ targets = [resolve_target(raw_target) for raw_target in raw_targets] if raw_targets else list(DEFAULT_TARGETS)
141
+ configs: list[Path] = []
142
+
143
+ for target in targets:
144
+ if target.is_file():
145
+ if target.suffix != ".yaml":
146
+ raise FileNotFoundError(f"Expected a .yaml config file, got {target}")
147
+ configs.append(target)
148
+ continue
149
+
150
+ if not target.is_dir():
151
+ raise FileNotFoundError(f"Config target does not exist: {target}")
152
+
153
+ configs.extend(sorted(path.resolve() for path in target.glob("*.yaml")))
154
+
155
+ unique_configs = []
156
+ seen = set()
157
+ for config in configs:
158
+ if config not in seen:
159
+ unique_configs.append(config)
160
+ seen.add(config)
161
+ return unique_configs
162
+
163
+
164
+ def resolve_checkpoint_path(raw_path: str) -> Path:
165
+ candidate = Path(raw_path).expanduser()
166
+ if candidate.is_absolute():
167
+ return candidate.resolve()
168
+ return (REPO_ROOT / candidate).resolve()
169
+
170
+
171
+ def load_job(config_path: Path) -> TrainingJob:
172
+ with config_path.open("r", encoding="utf-8") as handle:
173
+ config = yaml.safe_load(handle)
174
+
175
+ if not isinstance(config, dict):
176
+ raise ValueError(f"Config is not a YAML mapping: {config_path}")
177
+
178
+ training_directory = config.get("Training_Directory")
179
+ if not training_directory:
180
+ raise ValueError(f"Config is missing Training_Directory: {config_path}")
181
+
182
+ pretraining_path = None
183
+ model_args = config.get("Model", {}).get("args", {})
184
+ if isinstance(model_args, dict) and model_args.get("pretraining_path"):
185
+ pretraining_path = resolve_checkpoint_path(model_args["pretraining_path"])
186
+
187
+ return TrainingJob(
188
+ config_path=config_path.resolve(),
189
+ training_directory=str(training_directory),
190
+ pretraining_path=pretraining_path,
191
+ )
192
+
193
+
194
+ def parse_split_spec(raw_split: str | None) -> tuple[int, int] | None:
195
+ if raw_split is None:
196
+ return None
197
+
198
+ match = re.fullmatch(r"\s*(\d+)\s*/\s*(\d+)\s*", raw_split)
199
+ if not match:
200
+ raise ValueError(
201
+ f"Invalid --split value {raw_split!r}. Use the format K/N, for example 1/2."
202
+ )
203
+
204
+ split_index = int(match.group(1))
205
+ split_count = int(match.group(2))
206
+
207
+ if split_count <= 0:
208
+ raise ValueError("--split requires N > 0.")
209
+ if split_index <= 0:
210
+ raise ValueError("--split requires K >= 1.")
211
+ if split_index > split_count:
212
+ raise ValueError("--split requires K <= N.")
213
+
214
+ return split_index, split_count
215
+
216
+
217
+ def select_jobs_for_split(
218
+ jobs: list[TrainingJob],
219
+ split_spec: tuple[int, int] | None,
220
+ ) -> tuple[list[TrainingJob], str | None]:
221
+ if split_spec is None:
222
+ return jobs, None
223
+
224
+ split_index, split_count = split_spec
225
+ selected_jobs = [
226
+ job for index, job in enumerate(jobs)
227
+ if index % split_count == (split_index - 1)
228
+ ]
229
+ return selected_jobs, f"{split_index}/{split_count}"
230
+
231
+
232
+ def validate_forwarded_args(forwarded_args: list[str]) -> None:
233
+ normalized_args = [arg.split("=", 1)[0] for arg in forwarded_args]
234
+ forbidden = sorted(arg for arg in normalized_args if arg in FORBIDDEN_TRAINING_ARGS)
235
+ if forbidden:
236
+ joined = ", ".join(forbidden)
237
+ raise ValueError(
238
+ f"These training arguments are controlled by the launcher and cannot be forwarded: {joined}"
239
+ )
240
+
241
+
242
+ def warn_about_ignored_args(args: argparse.Namespace) -> None:
243
+ ignored = []
244
+
245
+ if args.gpus:
246
+ ignored.append("--gpus")
247
+ if args.nodelist:
248
+ ignored.append("--nodelist")
249
+ if args.local_only:
250
+ ignored.append("--local-only")
251
+ if args.srun_binary:
252
+ ignored.append("--srun-binary")
253
+ if args.scontrol_binary:
254
+ ignored.append("--scontrol-binary")
255
+ if args.max_parallel is not None:
256
+ ignored.append("--max-parallel")
257
+ if args.dry_run:
258
+ ignored.append("--dry-run")
259
+
260
+ if ignored:
261
+ print(
262
+ "Ignoring launcher options not used by the single-node version: "
263
+ + ", ".join(ignored),
264
+ flush=True,
265
+ )
266
+
267
+
268
+ def format_slot(slot: ExecutionSlot, hostname: str) -> str:
269
+ return f"{hostname}:gpu{slot.gpu_token}"
270
+
271
+
272
+ def sanitize_log_name(config_path: Path) -> str:
273
+ try:
274
+ relative = config_path.resolve().relative_to(REPO_ROOT)
275
+ name = "__".join(relative.parts)
276
+ except ValueError:
277
+ name = config_path.name
278
+ return f"{name}.log"
279
+
280
+
281
+ def terminate_active_processes(active_processes: list[ActiveProcess]) -> None:
282
+ for active in active_processes:
283
+ if active.process.poll() is None:
284
+ active.process.terminate()
285
+
286
+ deadline = time.time() + 10
287
+ while time.time() < deadline:
288
+ if all(active.process.poll() is not None for active in active_processes):
289
+ break
290
+ time.sleep(0.5)
291
+
292
+ for active in active_processes:
293
+ if active.process.poll() is None:
294
+ active.process.kill()
295
+
296
+ for active in active_processes:
297
+ if not active.log_handle.closed:
298
+ active.log_handle.close()
299
+
300
+
301
+ def build_training_command(job: TrainingJob, forwarded_args: list[str]) -> list[str]:
302
+ return [
303
+ sys.executable,
304
+ "scripts/training_script.py",
305
+ "--config",
306
+ str(job.config_path),
307
+ *DEFAULT_TRAINING_ARGS,
308
+ *forwarded_args,
309
+ ]
310
+
311
+
312
+ def launch_job(
313
+ job: TrainingJob,
314
+ slot: ExecutionSlot,
315
+ forwarded_args: list[str],
316
+ log_dir: Path,
317
+ hostname: str,
318
+ ) -> ActiveProcess:
319
+ log_path = log_dir / sanitize_log_name(job.config_path)
320
+ log_path.parent.mkdir(parents=True, exist_ok=True)
321
+ log_handle = log_path.open("w", encoding="utf-8")
322
+
323
+ command = build_training_command(job, forwarded_args)
324
+
325
+ env = os.environ.copy()
326
+ env["CUDA_VISIBLE_DEVICES"] = slot.gpu_token
327
+
328
+ process = subprocess.Popen(
329
+ command,
330
+ cwd=REPO_ROOT,
331
+ env=env,
332
+ stdout=log_handle,
333
+ stderr=subprocess.STDOUT,
334
+ )
335
+
336
+ print(
337
+ f"[launch] slot={format_slot(slot, hostname)} config={repo_relative(job.config_path)} "
338
+ f"log={repo_relative(log_path)}",
339
+ flush=True,
340
+ )
341
+
342
+ return ActiveProcess(
343
+ job=job,
344
+ slot=slot,
345
+ log_path=log_path,
346
+ log_handle=log_handle,
347
+ process=process,
348
+ )
349
+
350
+
351
+ def main() -> int:
352
+ args, forwarded_args = parse_args()
353
+ validate_forwarded_args(forwarded_args)
354
+ warn_about_ignored_args(args)
355
+ split_spec = parse_split_spec(args.split)
356
+
357
+ config_paths = discover_configs(args.targets)
358
+ if not config_paths:
359
+ raise RuntimeError("No YAML configs were found for the requested targets.")
360
+
361
+ all_jobs = [load_job(config_path) for config_path in config_paths]
362
+ jobs, split_label = select_jobs_for_split(all_jobs, split_spec)
363
+ if not jobs:
364
+ raise RuntimeError("No configs were selected for this split.")
365
+
366
+ duplicated_training_dirs: dict[str, Path] = {}
367
+ duplicate_messages = []
368
+ missing_pretraining = []
369
+ for job in jobs:
370
+ if job.training_directory in duplicated_training_dirs:
371
+ duplicate_messages.append(
372
+ f"{repo_relative(job.config_path)} conflicts with "
373
+ f"{repo_relative(duplicated_training_dirs[job.training_directory])} "
374
+ f"on Training_Directory={job.training_directory}"
375
+ )
376
+ else:
377
+ duplicated_training_dirs[job.training_directory] = job.config_path
378
+
379
+ if job.pretraining_path is not None and not job.pretraining_path.exists():
380
+ missing_pretraining.append(
381
+ f"{repo_relative(job.config_path)} -> missing {job.pretraining_path}"
382
+ )
383
+
384
+ if duplicate_messages:
385
+ raise RuntimeError(
386
+ "Refusing to launch configs with overlapping Training_Directory values:\n"
387
+ + "\n".join(duplicate_messages)
388
+ )
389
+
390
+ if missing_pretraining and not args.ignore_missing_pretraining:
391
+ raise RuntimeError(
392
+ "Missing pretraining checkpoints were found:\n"
393
+ + "\n".join(missing_pretraining)
394
+ + "\nRe-run with --ignore-missing-pretraining to launch anyway."
395
+ )
396
+
397
+ if missing_pretraining:
398
+ print("Pretraining checkpoint warnings:", flush=True)
399
+ for message in missing_pretraining:
400
+ print(f" {message}", flush=True)
401
+
402
+ hostname = socket.gethostname().split(".", 1)[0]
403
+ execution_slots = [ExecutionSlot(gpu_token=gpu_token) for gpu_token in FIXED_GPU_TOKENS]
404
+ max_parallel = len(execution_slots)
405
+
406
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
407
+ log_dir = (REPO_ROOT / args.logs_root / timestamp).resolve()
408
+
409
+ print(f"Repo root: {REPO_ROOT}", flush=True)
410
+ print(f"Configs discovered: {len(all_jobs)}", flush=True)
411
+ if split_label:
412
+ print(f"Configs selected for split {split_label}: {len(jobs)}", flush=True)
413
+ else:
414
+ print(f"Configs selected: {len(jobs)}", flush=True)
415
+ print(f"Node selected: {hostname}", flush=True)
416
+ print(f"GPUs selected: {', '.join(FIXED_GPU_TOKENS)}", flush=True)
417
+ print(f"Max parallel trainings: {max_parallel}", flush=True)
418
+ print(f"Training args: {' '.join(DEFAULT_TRAINING_ARGS + tuple(forwarded_args))}", flush=True)
419
+ print(f"Launcher logs: {repo_relative(log_dir)}", flush=True)
420
+ print("Launch mode: local queued trainings only", flush=True)
421
+
422
+ if args.test or args.dry_run:
423
+ print("Running launcher self-test; no trainings will be started.", flush=True)
424
+ for index, job in enumerate(jobs, start=1):
425
+ slot = execution_slots[(index - 1) % len(execution_slots)]
426
+ command = build_training_command(job, forwarded_args)
427
+ print(
428
+ f"[dry-run {index:02d}] slot={format_slot(slot, hostname)} "
429
+ f"dir={job.training_directory} cmd={shlex.join(command)}",
430
+ flush=True,
431
+ )
432
+ return 0
433
+
434
+ log_dir.mkdir(parents=True, exist_ok=True)
435
+
436
+ pending_jobs = deque(jobs)
437
+ free_slots = deque(execution_slots)
438
+ active_processes: list[ActiveProcess] = []
439
+ failed_jobs: list[tuple[TrainingJob, int, Path]] = []
440
+ completed_jobs = 0
441
+
442
+ try:
443
+ while pending_jobs or active_processes:
444
+ while pending_jobs and free_slots:
445
+ job = pending_jobs.popleft()
446
+ slot = free_slots.popleft()
447
+ active_processes.append(launch_job(job, slot, forwarded_args, log_dir, hostname))
448
+
449
+ if not active_processes:
450
+ break
451
+
452
+ time.sleep(args.poll_seconds)
453
+
454
+ still_active: list[ActiveProcess] = []
455
+ for active in active_processes:
456
+ return_code = active.process.poll()
457
+ if return_code is None:
458
+ still_active.append(active)
459
+ continue
460
+
461
+ active.log_handle.close()
462
+ free_slots.append(active.slot)
463
+
464
+ if return_code == 0:
465
+ completed_jobs += 1
466
+ print(
467
+ f"[done] slot={format_slot(active.slot, hostname)} "
468
+ f"config={repo_relative(active.job.config_path)} "
469
+ f"completed={completed_jobs}/{len(jobs)}",
470
+ flush=True,
471
+ )
472
+ else:
473
+ failed_jobs.append((active.job, return_code, active.log_path))
474
+ print(
475
+ f"[fail] slot={format_slot(active.slot, hostname)} "
476
+ f"config={repo_relative(active.job.config_path)} "
477
+ f"exit={return_code} log={repo_relative(active.log_path)}",
478
+ flush=True,
479
+ )
480
+ if args.stop_on_failure:
481
+ pending_jobs.clear()
482
+
483
+ active_processes = still_active
484
+
485
+ if failed_jobs and args.stop_on_failure:
486
+ terminate_active_processes(active_processes)
487
+ active_processes = []
488
+ break
489
+
490
+ except KeyboardInterrupt:
491
+ print("\nInterrupted. Terminating active trainings...", flush=True)
492
+ terminate_active_processes(active_processes)
493
+ return 130
494
+
495
+ print(f"Completed {completed_jobs} training(s); failed {len(failed_jobs)}.", flush=True)
496
+ if failed_jobs:
497
+ for job, return_code, log_path in failed_jobs:
498
+ print(
499
+ f" {repo_relative(job.config_path)} exit={return_code} "
500
+ f"log={repo_relative(log_path)}",
501
+ flush=True,
502
+ )
503
+ return 1
504
+
505
+ return 0
506
+
507
+
508
+ if __name__ == "__main__":
509
+ sys.exit(main())
root_gnn_dgl/scripts/check_dataset_files.py CHANGED
@@ -2,6 +2,9 @@ import yaml
2
  import os
3
  import subprocess
4
  import argparse
 
 
 
5
 
6
  def check_dataset_files(yaml_file, rerun=False):
7
  """
@@ -69,9 +72,9 @@ def check_dataset_files(yaml_file, rerun=False):
69
  # Optionally rerun data prep
70
  if rerun:
71
  print(f" Reprocessing dataset '{dataset_name}' ...")
72
- prep_command = f"jobs/prep_data/prep_data.sh {yaml_file} {dataset_name} {chunks}"
73
  try:
74
- subprocess.run(prep_command, shell=True, check=True)
75
  except subprocess.CalledProcessError as e:
76
  print(f" Could NOT reprocess '{dataset_name}': {e}")
77
  else:
@@ -87,8 +90,7 @@ def check_dataset_files(yaml_file, rerun=False):
87
  print(f"Error processing {yaml_file}: {e}")
88
 
89
  def main(pargs):
90
- # Base directory containing the YAML files
91
- base_directory = os.getcwd() + "/configs/"
92
 
93
  if pargs.configs:
94
  configs = [p.strip() for p in pargs.configs.split(',')]
@@ -101,10 +103,11 @@ def main(pargs):
101
  ]
102
 
103
  for config in configs:
104
- yaml_file = os.path.join(base_directory, config)
105
- if os.path.exists(yaml_file):
 
106
  print(f"\nProcessing file: {config}")
107
- check_dataset_files(yaml_file, pargs.rerun)
108
  else:
109
  print(f"File not found: {yaml_file}")
110
 
@@ -114,7 +117,7 @@ if __name__ == "__main__":
114
  "--configs", "-c",
115
  type=str,
116
  required=False,
117
- help="Comma-separated list of YAML config paths relative to base directory"
118
  )
119
  parser.add_argument(
120
  "--rerun", "-r",
@@ -122,4 +125,4 @@ if __name__ == "__main__":
122
  help="Automatically re-run data processing to fix missing files"
123
  )
124
  args = parser.parse_args()
125
- main(args)
 
2
  import os
3
  import subprocess
4
  import argparse
5
+ from pathlib import Path
6
+
7
+ REPO_ROOT = Path(__file__).resolve().parents[1]
8
 
9
  def check_dataset_files(yaml_file, rerun=False):
10
  """
 
72
  # Optionally rerun data prep
73
  if rerun:
74
  print(f" Reprocessing dataset '{dataset_name}' ...")
75
+ prep_command = [str(REPO_ROOT / "jobs/prep_data/prep_data.sh"), yaml_file, dataset_name, str(chunks)]
76
  try:
77
+ subprocess.run(prep_command, check=True)
78
  except subprocess.CalledProcessError as e:
79
  print(f" Could NOT reprocess '{dataset_name}': {e}")
80
  else:
 
90
  print(f"Error processing {yaml_file}: {e}")
91
 
92
  def main(pargs):
93
+ base_directory = REPO_ROOT / "configs"
 
94
 
95
  if pargs.configs:
96
  configs = [p.strip() for p in pargs.configs.split(',')]
 
103
  ]
104
 
105
  for config in configs:
106
+ config_path = Path(config).expanduser()
107
+ yaml_file = config_path if config_path.is_absolute() else base_directory / config_path
108
+ if yaml_file.exists():
109
  print(f"\nProcessing file: {config}")
110
+ check_dataset_files(str(yaml_file), pargs.rerun)
111
  else:
112
  print(f"File not found: {yaml_file}")
113
 
 
117
  "--configs", "-c",
118
  type=str,
119
  required=False,
120
+ help="Comma-separated list of YAML config paths relative to configs/ or absolute paths"
121
  )
122
  parser.add_argument(
123
  "--rerun", "-r",
 
125
  help="Automatically re-run data processing to fix missing files"
126
  )
127
  args = parser.parse_args()
128
+ main(args)
root_gnn_dgl/scripts/export_onnx.py CHANGED
@@ -23,8 +23,8 @@ from pathlib import Path
23
  from types import MethodType, SimpleNamespace
24
  from typing import Any, Dict, Iterator, Optional, Tuple
25
 
26
- file_path = os.getcwd()
27
- sys.path.append(file_path)
28
 
29
  import dgl
30
  import matplotlib.pyplot as plt
 
23
  from types import MethodType, SimpleNamespace
24
  from typing import Any, Dict, Iterator, Optional, Tuple
25
 
26
+ REPO_ROOT = Path(__file__).resolve().parents[1]
27
+ sys.path.insert(0, str(REPO_ROOT))
28
 
29
  import dgl
30
  import matplotlib.pyplot as plt
root_gnn_dgl/scripts/inference.py CHANGED
@@ -1,6 +1,4 @@
1
  import sys
2
- file_path = "/global/cfs/projectdirs/atlas/joshua/root_gnn/root_gnn_dgl"
3
- sys.path.append(file_path)
4
  import os
5
  import argparse
6
  import yaml
@@ -8,6 +6,10 @@ import gc
8
  from array import array
9
  import fnmatch
10
  import re
 
 
 
 
11
 
12
  import torch
13
  import dgl
 
1
  import sys
 
 
2
  import os
3
  import argparse
4
  import yaml
 
6
  from array import array
7
  import fnmatch
8
  import re
9
+ from pathlib import Path
10
+
11
+ REPO_ROOT = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(REPO_ROOT))
13
 
14
  import torch
15
  import dgl
root_gnn_dgl/scripts/plot_config_distributions.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import glob
5
+ import math
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+
10
+ import awkward as ak
11
+ import matplotlib
12
+ matplotlib.use("Agg")
13
+ import matplotlib.pyplot as plt
14
+ from matplotlib.backends.backend_pdf import PdfPages
15
+ import numpy as np
16
+ import uproot
17
+ import yaml
18
+
19
+
20
+ OPERATORS = {
21
+ ">": lambda values, cut: values > cut,
22
+ ">=": lambda values, cut: values >= cut,
23
+ "<": lambda values, cut: values < cut,
24
+ "<=": lambda values, cut: values <= cut,
25
+ "==": lambda values, cut: values == cut,
26
+ "!=": lambda values, cut: values != cut,
27
+ }
28
+
29
+
30
+ def load_config(path):
31
+ with open(path, "r", encoding="utf-8") as handle:
32
+ return yaml.safe_load(handle)
33
+
34
+
35
+ def selection_branches(selection):
36
+ if isinstance(selection, str):
37
+ tokens = re.findall(r"\b[A-Za-z_][A-Za-z0-9_]*\b", selection)
38
+ keywords = {"and", "or", "not", "True", "False"}
39
+ return [token for token in tokens if token not in keywords]
40
+ if isinstance(selection, (list, tuple)) and len(selection) > 0:
41
+ return [selection[0]]
42
+ return []
43
+
44
+
45
+ def feature_branches(node_branch_names):
46
+ branches = []
47
+ for feature in node_branch_names:
48
+ if not isinstance(feature, list):
49
+ continue
50
+ for branch in feature:
51
+ if isinstance(branch, str) and branch != "CALC_E":
52
+ branches.append(branch)
53
+ return branches
54
+
55
+
56
+ def branches_for_dataset(dataset_config):
57
+ args = dataset_config["args"]
58
+ branches = feature_branches(args.get("node_branch_names", []))
59
+ for selection in dataset_config.get("selections", []):
60
+ branches.extend(selection_branches(selection))
61
+ return sorted(set(branches))
62
+
63
+
64
+ def resolve_files(args):
65
+ raw_dir = args["raw_dir"]
66
+ file_names = args["file_names"]
67
+ files = []
68
+ if isinstance(file_names, str):
69
+ files.extend(glob.glob(os.path.join(raw_dir, file_names)))
70
+ else:
71
+ for file_name in file_names:
72
+ files.extend(glob.glob(os.path.join(raw_dir, file_name)))
73
+ return sorted(files)
74
+
75
+
76
+ def load_arrays(dataset_config, max_events=None):
77
+ args = dataset_config["args"]
78
+ tree_name = args.get("tree_name", "nominal_Loose")
79
+ branches = branches_for_dataset(dataset_config)
80
+ arrays = []
81
+ events_read = 0
82
+
83
+ for file_name in resolve_files(args):
84
+ with uproot.open(file_name) as root_file:
85
+ tree = root_file[tree_name]
86
+ entry_stop = None
87
+ if max_events is not None:
88
+ remaining = max_events - events_read
89
+ if remaining <= 0:
90
+ break
91
+ entry_stop = remaining
92
+ array = tree.arrays(branches, library="ak", entry_stop=entry_stop)
93
+ arrays.append(array)
94
+ if branches:
95
+ events_read += len(array[branches[0]])
96
+ if max_events is not None and events_read >= max_events:
97
+ break
98
+
99
+ if not arrays:
100
+ pattern = os.path.join(args["raw_dir"], str(args["file_names"]))
101
+ raise FileNotFoundError(f"No files found for pattern {pattern}")
102
+ return ak.concatenate(arrays, axis=0)
103
+
104
+
105
+ def selection_mask(data, selections):
106
+ first_field = data.fields[0] if len(data.fields) > 0 else None
107
+ if first_field is None:
108
+ return None
109
+
110
+ mask = np.ones(len(data[first_field]), dtype=bool)
111
+ for selection in selections:
112
+ if isinstance(selection, str):
113
+ current_mask = eval(selection, {"__builtins__": {}}, data)
114
+ else:
115
+ branch, cut, op = selection
116
+ if op not in OPERATORS:
117
+ raise ValueError(f"Unknown selection operator: {op}")
118
+ current_mask = OPERATORS[op](data[branch], cut)
119
+ mask = mask & ak.to_numpy(current_mask)
120
+ return mask
121
+
122
+
123
+ def ensure_node_array(value, reference):
124
+ if isinstance(value, (int, float, complex)):
125
+ return ak.full_like(reference, value)
126
+ return value
127
+
128
+
129
+ def branch_array(data, branch, node_type, reference):
130
+ value = ensure_node_array(branch, reference)
131
+ if not isinstance(branch, str):
132
+ return value
133
+ value = data[branch]
134
+ if node_type == "single":
135
+ return ak.singletons(value)
136
+ return value
137
+
138
+
139
+ def clean_label(value):
140
+ return str(value).replace("/", "_")
141
+
142
+
143
+ def per_type_feature_label(feature_spec, type_index):
144
+ return clean_label(feature_spec[type_index])
145
+
146
+
147
+ def flatten_values(values):
148
+ flat_values = np.asarray(ak.to_numpy(ak.ravel(values)), dtype=float)
149
+ return flat_values[np.isfinite(flat_values)]
150
+
151
+
152
+ def build_feature_values(data, dataset_config):
153
+ args = dataset_config["args"]
154
+ node_branch_names = args["node_branch_names"]
155
+ node_branch_types = args["node_branch_types"]
156
+ node_feature_scales = [float(scale) for scale in args["node_feature_scales"]]
157
+ n_types = len(node_branch_names[0])
158
+
159
+ references = []
160
+ for type_index in range(n_types):
161
+ branch = node_branch_names[0][type_index]
162
+ node_type = node_branch_types[type_index]
163
+ if isinstance(branch, str):
164
+ reference = data[branch]
165
+ if node_type == "single":
166
+ reference = ak.singletons(reference)
167
+ else:
168
+ raise ValueError("The first node feature must use real branches to define node counts.")
169
+ references.append(reference)
170
+
171
+ features = {}
172
+ pt_parts = []
173
+ eta_parts = []
174
+
175
+ for type_index in range(n_types):
176
+ pt_parts.append(branch_array(data, node_branch_names[0][type_index], node_branch_types[type_index], references[type_index]))
177
+ eta_parts.append(branch_array(data, node_branch_names[1][type_index], node_branch_types[type_index], references[type_index]))
178
+
179
+ for feature_index, feature_spec in enumerate(node_branch_names):
180
+ if not isinstance(feature_spec, list):
181
+ continue
182
+
183
+ per_type_parts = []
184
+ for type_index in range(n_types):
185
+ branch = feature_spec[type_index]
186
+ if not isinstance(branch, str) or branch == "CALC_E":
187
+ per_type_parts.append(None)
188
+ continue
189
+
190
+ reference = references[type_index]
191
+ node_type = node_branch_types[type_index]
192
+ per_type_parts.append(branch_array(data, branch, node_type, reference))
193
+
194
+ for type_index, part in enumerate(per_type_parts):
195
+ if part is None:
196
+ continue
197
+ scaled_part = part * node_feature_scales[feature_index]
198
+ scaled_pt = pt_parts[type_index] * node_feature_scales[0]
199
+ scaled_part = scaled_part[scaled_pt != 0]
200
+ features[per_type_feature_label(feature_spec, type_index)] = flatten_values(scaled_part)
201
+
202
+ return features
203
+
204
+
205
+ def common_bins(datasets, feature_name, bins):
206
+ values = np.concatenate([features[feature_name] for features in datasets.values() if len(features[feature_name]) > 0])
207
+ if len(values) == 0:
208
+ return np.linspace(0, 1, bins + 1)
209
+
210
+ unique_values = np.unique(values)
211
+ if len(unique_values) <= 20 and np.allclose(unique_values, np.round(unique_values)):
212
+ low = math.floor(values.min())
213
+ high = math.ceil(values.max())
214
+ return np.arange(low - 0.5, high + 1.5, 1)
215
+
216
+ low, high = np.percentile(values, [0.5, 99.5])
217
+ if not np.isfinite(low) or not np.isfinite(high) or low == high:
218
+ low, high = values.min(), values.max()
219
+ if low == high:
220
+ low -= 0.5
221
+ high += 0.5
222
+ return np.linspace(low, high, bins + 1)
223
+
224
+
225
+ def plot_feature(feature_name, datasets, bins):
226
+ fig, ax = plt.subplots(figsize=(8, 6))
227
+ hist_bins = common_bins(datasets, feature_name, bins)
228
+
229
+ for dataset_name, features in datasets.items():
230
+ values = features[feature_name]
231
+ if len(values) == 0:
232
+ continue
233
+ ax.hist(
234
+ values,
235
+ bins=hist_bins,
236
+ histtype="step",
237
+ density=True,
238
+ linewidth=1.8,
239
+ label=f"{dataset_name} (n={len(values)})",
240
+ )
241
+
242
+ ax.set_xlabel(feature_name)
243
+ ax.set_ylabel("Normalized entries")
244
+ ax.legend(frameon=False)
245
+ ax.grid(alpha=0.25)
246
+ fig.tight_layout()
247
+ return fig
248
+
249
+
250
+ def safe_filename(name):
251
+ return re.sub(r"[^A-Za-z0-9_.-]+", "_", name).strip("_")
252
+
253
+
254
+ def main():
255
+ parser = argparse.ArgumentParser(
256
+ description="Plot model-input node feature distributions for every dataset in a config."
257
+ )
258
+ parser.add_argument("--config", required=True, help="YAML config containing Datasets.")
259
+ parser.add_argument(
260
+ "--output-dir",
261
+ default=None,
262
+ help="Directory for optional PNG outputs. Defaults to plots/<config-stem>_distributions.",
263
+ )
264
+ parser.add_argument(
265
+ "--output-pdf",
266
+ default=None,
267
+ help="Path for the multi-page PDF. Defaults to plots/<config-stem>_distributions.pdf.",
268
+ )
269
+ parser.add_argument("--write-pngs", action="store_true", help="Also write one PNG per plot.")
270
+ parser.add_argument("--bins", type=int, default=80, help="Number of bins for continuous features.")
271
+ parser.add_argument("--max-events", type=int, default=None, help="Optional maximum events per dataset.")
272
+ args = parser.parse_args()
273
+
274
+ config = load_config(args.config)
275
+ output_dir = Path(args.output_dir) if args.output_dir else Path("plots") / f"{Path(args.config).stem}_distributions"
276
+ output_pdf = Path(args.output_pdf) if args.output_pdf else Path("plots") / f"{Path(args.config).stem}_distributions.pdf"
277
+ output_pdf.parent.mkdir(parents=True, exist_ok=True)
278
+ if args.write_pngs:
279
+ output_dir.mkdir(parents=True, exist_ok=True)
280
+
281
+ dataset_features = {}
282
+ for dataset_name, dataset_config in config["Datasets"].items():
283
+ print(f"Loading {dataset_name}", flush=True)
284
+ data = load_arrays(dataset_config, max_events=args.max_events)
285
+ mask = selection_mask(data, dataset_config.get("selections", []))
286
+ if mask is not None:
287
+ data = data[mask]
288
+ dataset_features[dataset_name] = build_feature_values(data, dataset_config)
289
+
290
+ feature_names = list(next(iter(dataset_features.values())).keys())
291
+ with PdfPages(output_pdf) as pdf:
292
+ for feature_name in feature_names:
293
+ fig = plot_feature(feature_name, dataset_features, args.bins)
294
+ pdf.savefig(fig)
295
+ if args.write_pngs:
296
+ output_path = output_dir / f"{safe_filename(feature_name)}.png"
297
+ fig.savefig(output_path, dpi=160)
298
+ print(f"Wrote {output_path}", flush=True)
299
+ plt.close(fig)
300
+ print(f"Wrote {output_pdf}", flush=True)
301
+
302
+
303
+ if __name__ == "__main__":
304
+ main()
root_gnn_dgl/scripts/prep_data.py CHANGED
@@ -1,8 +1,10 @@
1
  import sys
2
  import os
3
  import glob
4
- file_path = os.getcwd()
5
- sys.path.append(file_path)
 
 
6
 
7
  import root_gnn_base.utils as utils
8
  from root_gnn_base.dataset import compute_cutflow, print_cutflow
 
1
  import sys
2
  import os
3
  import glob
4
+ from pathlib import Path
5
+
6
+ REPO_ROOT = Path(__file__).resolve().parents[1]
7
+ sys.path.insert(0, str(REPO_ROOT))
8
 
9
  import root_gnn_base.utils as utils
10
  from root_gnn_base.dataset import compute_cutflow, print_cutflow
root_gnn_dgl/scripts/selections.py CHANGED
@@ -2,12 +2,13 @@ import argparse
2
  import glob
3
  import os
4
  import sys
 
5
 
6
  import awkward as ak
7
  import uproot
8
 
9
- file_path = os.getcwd()
10
- sys.path.append(file_path)
11
 
12
  from root_gnn_base import utils
13
  from root_gnn_base.dataset import selection_branches, check_selection, print_cutflow, init_cutflow
 
2
  import glob
3
  import os
4
  import sys
5
+ from pathlib import Path
6
 
7
  import awkward as ak
8
  import uproot
9
 
10
+ REPO_ROOT = Path(__file__).resolve().parents[1]
11
+ sys.path.insert(0, str(REPO_ROOT))
12
 
13
  from root_gnn_base import utils
14
  from root_gnn_base.dataset import selection_branches, check_selection, print_cutflow, init_cutflow
root_gnn_dgl/scripts/training_script.py CHANGED
@@ -3,6 +3,7 @@ import time
3
  import datetime
4
  import yaml
5
  import os
 
6
 
7
  start_time = time.time()
8
 
@@ -11,8 +12,8 @@ import torch
11
  import torch.nn as nn
12
 
13
  import sys
14
- file_path = os.getcwd()
15
- sys.path.append(file_path)
16
  import root_gnn_base.batched_dataset as datasets
17
  from root_gnn_base import utils
18
  import root_gnn_base.custom_scheduler as lr_utils
 
3
  import datetime
4
  import yaml
5
  import os
6
+ from pathlib import Path
7
 
8
  start_time = time.time()
9
 
 
12
  import torch.nn as nn
13
 
14
  import sys
15
+ REPO_ROOT = Path(__file__).resolve().parents[1]
16
+ sys.path.insert(0, str(REPO_ROOT))
17
  import root_gnn_base.batched_dataset as datasets
18
  from root_gnn_base import utils
19
  import root_gnn_base.custom_scheduler as lr_utils