Skip to content

Commit 0d0558a

Browse files
Benedikt Volkelsawenzel
authored andcommitted
Metrics to resource estimate
* o2dpg_sim_metrics.py resources produces a resource estimate for mem and CPU based on previously derived metrics from one or more runs --take argument can be used to define to take * average * min * max in case multiple metric JSONs are passed. The estimates are written to a JSON again Treat every TF the same so the estimate file only contains "task_name": { "mem": <memory>, "cpu": <cpu> } without TF suffix * o2_dpg_workflow_runner --update-resources takes a JSON produced by previous resource estimation and updates the resources on the fly Due to above comment, all TFs get the same resource estimate
1 parent f0ee0c3 commit 0d0558a

2 files changed

Lines changed: 120 additions & 3 deletions

File tree

MC/bin/o2_dpg_workflow_runner.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
parser.add_argument('--produce-script', help='Produces a shell script that runs the workflow in serialized manner and quits.')
4444
parser.add_argument('--rerun-from', help='Reruns the workflow starting from given task (or pattern). All dependent jobs will be rerun.')
4545
parser.add_argument('--list-tasks', help='Simply list all tasks by name and quit.', action='store_true')
46+
parser.add_argument('--update-resources', dest="update_resources", help='Read resource estimates from a JSON and apply where possible.')
4647

4748
parser.add_argument('--mem-limit', help='Set memory limit as scheduling constraint (in MB)', default=0.9*max_system_mem/1024./1024)
4849
parser.add_argument('--cpu-limit', help='Set CPU limit (core count)', default=8)
@@ -286,8 +287,8 @@ def build_graph(taskuniverse, workflowspec):
286287
return (edges, nodes)
287288

288289

289-
# loads the workflow specification
290-
def load_workflow(workflowfile):
290+
# loads json into dict, e.g. for workflow specification
291+
def load_json(workflowfile):
291292
fp=open(workflowfile)
292293
workflowspec=json.load(fp)
293294
return workflowspec
@@ -408,6 +409,32 @@ def getweight(tid):
408409
# print (global_next_tasks)
409410
return { 'nexttasks' : global_next_tasks, 'weights' : task_weights, 'topological_ordering' : tup[0] }
410411

412+
# update the resource estimates of a workflow based on resources given via JSON
413+
def update_resource_estimates(workflow, resource_json):
414+
resource_dict = load_json(resource_json)
415+
stages = workflow["stages"]
416+
417+
for task in stages:
418+
if task["timeframe"] >= 1:
419+
tf = task["timeframe"]
420+
name = "_".join(task["name"].split("_")[:-1])
421+
else:
422+
name = task["name"]
423+
424+
if name not in resource_dict:
425+
continue
426+
427+
new_resources = resource_dict[name]
428+
429+
task["resources"]["mem"] = new_resources.get("mem", task["resources"]["mem"])
430+
# CPU is a bit more invlolved
431+
if "cpu" in new_resources:
432+
cpu = new_resources["cpu"]
433+
rel_cpu = task["resources"]["relative_cpu"]
434+
if rel_cpu is not None:
435+
# respect the relative CPU settings
436+
cpu *= rel_cpu
437+
task["resources"]["cpu"] = cpu
411438

412439
#
413440
# functions for execution; encapsulated in a WorkflowExecutor class
@@ -417,7 +444,7 @@ class WorkflowExecutor:
417444
def __init__(self, workflowfile, args, jmax=100):
418445
self.args=args
419446
self.workflowfile = workflowfile
420-
self.workflowspec = load_workflow(workflowfile)
447+
self.workflowspec = load_json(workflowfile)
421448
self.workflowspec = filter_workflow(self.workflowspec, args.target_tasks, args.target_labels)
422449

423450
if not self.workflowspec['stages']:
@@ -440,6 +467,9 @@ def __init__(self, workflowfile, args, jmax=100):
440467
self.tasktoid[self.taskuniverse[i]]=i
441468
self.idtotask[i]=self.taskuniverse[i]
442469

470+
if args.update_resources:
471+
update_resource_estimates(self.workflowspec, args.update_resources)
472+
443473
self.maxmemperid = [ self.workflowspec['stages'][tid]['resources']['mem'] for tid in range(len(self.taskuniverse)) ]
444474
self.cpuperid = [ self.workflowspec['stages'][tid]['resources']['cpu'] for tid in range(len(self.taskuniverse)) ]
445475
self.curmembooked = 0

MC/utils/o2dpg_sim_metrics.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,86 @@ def influx(args):
554554
return make_for_influxDB(arrange_into_categories(metrics_map), metrics_map["tags"], args.table_base, args.output)
555555

556556

557+
def resources(args):
558+
559+
# Collect all metrics we got
560+
metrics = []
561+
for m in args.metrics:
562+
with open(m, "r") as f:
563+
metrics.append(json.load(f))
564+
565+
# We will finally use the intersection of task names
566+
intersection = [m for m in metrics[0]["metrics"]]
567+
# union is built as a cross check, TODO, could be used to identify very fast tasks as well
568+
union = [m for m in metrics[0]["metrics"]]
569+
# collect number of timeframes for each metrics file
570+
ntfs = [metrics[-1]["tags"]["ntfs"]]
571+
572+
for met in metrics[1:]:
573+
intersection = list(set(intersection) & set([m for m in met["metrics"]]))
574+
union = list(set(intersection) | set([m for m in met["metrics"]]))
575+
ntfs.append(met["tags"]["ntfs"])
576+
577+
if len(intersection) != len(union):
578+
print("WARNING: Input metrics seem to be different, union and intersection do not have the same length, using intersection. This can however happen when some tasks finish super fast")
579+
580+
# quick helper to remove TF suffices
581+
def unique_names_wo_tf_suffix(name, tasks_per_tf_, tasks_no_tf_):
582+
name_split = name.split("_")
583+
try:
584+
# assume "_<int>" to reflect a TF suffix
585+
tf = int(name_split[-1])
586+
name = "_".join(name_split[:-1])
587+
tasks_per_tf_.append(name)
588+
except ValueError:
589+
tasks_no_tf_.append(name)
590+
591+
tasks_per_tf = []
592+
tasks_no_tf = []
593+
594+
for name in intersection:
595+
unique_names_wo_tf_suffix(name, tasks_per_tf, tasks_no_tf)
596+
# We treat every tf the same, none of those is special, so strip TF suffices and get unique list of names
597+
tasks_per_tf = list(set(tasks_per_tf))
598+
599+
# what to do in case there were multiple metrics files given as input
600+
derive_func = {"average": lambda l: sum(l) / len(l),
601+
"min": min,
602+
"max": max}[args.take]
603+
# Collect here
604+
resources_map = {t: {} for t in tasks_per_tf + tasks_no_tf}
605+
# now let's only take what we are interested in
606+
metrics = [m["metrics"] for m in metrics]
607+
# for convenience
608+
scaling_map = {"mem": lambda x: int(x), "cpu": lambda x: ceil(x * 0.01)}
609+
# for the workflows we specify mem and cpu, in the metrics we have pss/uss and cpu
610+
metrics_name_map = {"mem": "uss", "cpu": "cpu"}
611+
612+
for w in args.which:
613+
met_ind = MET_TO_IND[metrics_name_map[w]]
614+
scale = scaling_map[w]
615+
for tptf in tasks_per_tf:
616+
values = []
617+
for met, n in zip(metrics, ntfs):
618+
this_value = 0
619+
for i in range(1, n + 1):
620+
key = f"{tptf}_{i}"
621+
# It could happen that a task is missing in a certain TF, e.g. when it went through fast enough to not leave a trace in pipeline iterations
622+
if key not in met:
623+
continue
624+
# now do per TF in current metrics, here we always take the max for now ==> conservative
625+
this_value = max(met[key][met_ind], this_value)
626+
values.append(this_value)
627+
resources_map[tptf][w] = scale(derive_func(values))
628+
629+
for tntf in tasks_no_tf:
630+
resources_map[tntf][w] = scale(derive_func([met[tntf][met_ind] for met in metrics]))
631+
632+
# finally save to JSON
633+
with open(args.output, "w") as f:
634+
json.dump(resources_map, f, indent=2)
635+
636+
557637
def main():
558638

559639
parser = argparse.ArgumentParser(description="Metrics evaluation of O2 simulation workflow")
@@ -580,6 +660,13 @@ def main():
580660
influx_parser.add_argument("--table-base", dest="table_base", help="base name of InfluxDB table name", default="O2DPG_MC")
581661
influx_parser.add_argument("--output", "-o", help="pmetric JSON file to prepare for InfluxDB", default="metrics_influxDB.dat")
582662

663+
resource_parser = sub_parsers.add_parser("resources", help="Derive resource estimate from metrics to be passed to workflow runner")
664+
resource_parser.set_defaults(func=resources)
665+
resource_parser.add_argument("--metrics", nargs="+", help="metric JSON files")
666+
resource_parser.add_argument("--which", help="which resources to estimate", nargs="*", choices=["mem", "cpu"], default=["mem", "cpu"])
667+
resource_parser.add_argument("--take", help="how to treat multiple input metric files", default="average", choices=["average", "max", "min"])
668+
resource_parser.add_argument("--output", "-o", help="pmetric JSON file to prepare for InfluxDB", default="metrics_influxDB.dat")
669+
583670
args = parser.parse_args()
584671
return args.func(args)
585672

0 commit comments

Comments
 (0)