From b6d2d0a6a96a45f243c6c2d56db712d65e66964b Mon Sep 17 00:00:00 2001 From: Lukas Rothenberger Date: Wed, 27 Sep 2023 10:54:50 +0200 Subject: [PATCH] feat: experimental performance modeller / browser for CPU multithreading Added the experimental "discopop_optimizer" to the discopop_library and GUI --- discopop_explorer/pattern_detection.py | 83 ++-- discopop_explorer/utils.py | 61 ++- .../CostModels/CostModel.py | 250 ++++++++++ .../DataTransfer/DataTransferCosts.py | 45 ++ .../CostModels/DataTransfer/__init__.py | 0 .../discopop_optimizer/CostModels/__init__.py | 0 .../CostModels/utilities.py | 375 +++++++++++++++ .../DataTransfers/DataTransfers.py | 135 ++++++ .../DataTransfers/__init__.py | 0 .../ExtrapInterpolatedMicrobench.py | 5 +- .../Microbench/Microbench.py | 1 + .../discopop_optimizer/Microbench/utils.py | 63 +++ .../discopop_optimizer/OptimizationGraph.py | 219 ++++++++- .../DataAccesses/CalculateUpdates.py | 7 + .../discopop_optimizer/PETParser/PETParser.py | 15 +- .../Variables/Experiment.py | 83 +++- .../Variables/ExperimentUtils.py | 168 +++++++ .../discopop_optimizer/__main__.py | 301 +++++++++++++ .../bindings/CodeGenerator.py | 362 +++++++++++++++ .../bindings/CodeStorageObject.py | 34 ++ .../discopop_optimizer/bindings/__init__.py | 0 .../discopop_optimizer/bindings/utilities.py | 33 ++ .../classes/context/ContextObjectUtils.py | 56 +++ .../classes/edges/DataFlowEdge.py | 4 + .../classes/edges/GenericEdge.py | 5 + .../classes/edges/OptionEdge.py | 4 + .../classes/edges/RequirementEdge.py | 4 + .../classes/edges/SuccessorEdge.py | 1 + .../classes/nodes/ContextMerge.py | 3 +- .../classes/nodes/ContextNode.py | 10 +- .../classes/nodes/ContextRestore.py | 3 +- .../classes/nodes/ContextSave.py | 3 +- .../classes/nodes/ContextSnapshot.py | 3 +- .../classes/nodes/ContextSnapshotPop.py | 3 +- .../classes/nodes/FunctionRoot.py | 42 +- .../classes/nodes/GenericNode.py | 36 +- .../discopop_optimizer/classes/nodes/Loop.py | 84 +++- .../classes/nodes/Workload.py | 106 ++++- .../classes/system/Network.py | 41 ++ .../classes/system/System.py | 121 +++++ .../classes/system/__init__.py | 0 .../classes/system/devices/CPU.py | 19 + .../classes/system/devices/Device.py | 83 ++++ .../classes/system/devices/GPU.py | 21 + .../classes/system/devices/__init__.py | 0 .../discopop_optimizer/execution/__init__.py | 0 .../execution/stored_models.py | 281 ++++++++++++ .../discopop_optimizer/gui/__init__.py | 0 .../gui/plotting/CostModels.py | 426 ++++++++++++++++++ .../gui/plotting/__init__.py | 0 .../gui/presentation/ChoiceDetails.py | 91 ++++ .../gui/presentation/OptionTable.py | 333 ++++++++++++++ .../gui/presentation/__init__.py | 0 .../gui/queries/ValueTableQuery.py | 189 ++++++++ .../gui/queries/__init__.py | 0 .../gui/widgets/ScrollableFrame.py | 54 +++ .../gui/widgets/__init__.py | 0 .../discopop_optimizer/requirements.txt | 11 + .../suggestions/__init__.py | 0 .../suggestions/importers/__init__.py | 0 .../suggestions/importers/base.py | 31 ++ .../suggestions/importers/do_all.py | 171 +++++++ .../suggestions/importers/reduction.py | 144 ++++++ .../utilities/MOGUtilities.py | 192 +++++++- .../GlobalOptimization/RandomSamples.py | 111 +++++ .../GlobalOptimization/__init__.py | 0 .../optimization/LocalOptimization/TopDown.py | 193 ++++++++ .../LocalOptimization/__init__.py | 0 .../utilities/optimization/__init__.py | 0 .../classes/ExecutionConfiguration.py | 15 +- discopop_wizard/screens/main.py | 2 + discopop_wizard/screens/optimizer/__init__.py | 0 discopop_wizard/screens/optimizer/binding.py | 148 ++++++ scripts/runDiscoPoP | 3 +- 74 files changed, 5180 insertions(+), 107 deletions(-) create mode 100644 discopop_library/discopop_optimizer/CostModels/CostModel.py create mode 100644 discopop_library/discopop_optimizer/CostModels/DataTransfer/DataTransferCosts.py create mode 100644 discopop_library/discopop_optimizer/CostModels/DataTransfer/__init__.py create mode 100644 discopop_library/discopop_optimizer/CostModels/__init__.py create mode 100644 discopop_library/discopop_optimizer/CostModels/utilities.py create mode 100644 discopop_library/discopop_optimizer/DataTransfers/DataTransfers.py create mode 100644 discopop_library/discopop_optimizer/DataTransfers/__init__.py create mode 100644 discopop_library/discopop_optimizer/Microbench/utils.py create mode 100644 discopop_library/discopop_optimizer/PETParser/DataAccesses/CalculateUpdates.py create mode 100644 discopop_library/discopop_optimizer/Variables/ExperimentUtils.py create mode 100644 discopop_library/discopop_optimizer/__main__.py create mode 100644 discopop_library/discopop_optimizer/bindings/CodeGenerator.py create mode 100644 discopop_library/discopop_optimizer/bindings/CodeStorageObject.py create mode 100644 discopop_library/discopop_optimizer/bindings/__init__.py create mode 100644 discopop_library/discopop_optimizer/bindings/utilities.py create mode 100644 discopop_library/discopop_optimizer/classes/context/ContextObjectUtils.py create mode 100644 discopop_library/discopop_optimizer/classes/system/Network.py create mode 100644 discopop_library/discopop_optimizer/classes/system/System.py create mode 100644 discopop_library/discopop_optimizer/classes/system/__init__.py create mode 100644 discopop_library/discopop_optimizer/classes/system/devices/CPU.py create mode 100644 discopop_library/discopop_optimizer/classes/system/devices/Device.py create mode 100644 discopop_library/discopop_optimizer/classes/system/devices/GPU.py create mode 100644 discopop_library/discopop_optimizer/classes/system/devices/__init__.py create mode 100644 discopop_library/discopop_optimizer/execution/__init__.py create mode 100644 discopop_library/discopop_optimizer/execution/stored_models.py create mode 100644 discopop_library/discopop_optimizer/gui/__init__.py create mode 100644 discopop_library/discopop_optimizer/gui/plotting/CostModels.py create mode 100644 discopop_library/discopop_optimizer/gui/plotting/__init__.py create mode 100644 discopop_library/discopop_optimizer/gui/presentation/ChoiceDetails.py create mode 100644 discopop_library/discopop_optimizer/gui/presentation/OptionTable.py create mode 100644 discopop_library/discopop_optimizer/gui/presentation/__init__.py create mode 100644 discopop_library/discopop_optimizer/gui/queries/ValueTableQuery.py create mode 100644 discopop_library/discopop_optimizer/gui/queries/__init__.py create mode 100644 discopop_library/discopop_optimizer/gui/widgets/ScrollableFrame.py create mode 100644 discopop_library/discopop_optimizer/gui/widgets/__init__.py create mode 100644 discopop_library/discopop_optimizer/requirements.txt create mode 100644 discopop_library/discopop_optimizer/suggestions/__init__.py create mode 100644 discopop_library/discopop_optimizer/suggestions/importers/__init__.py create mode 100644 discopop_library/discopop_optimizer/suggestions/importers/base.py create mode 100644 discopop_library/discopop_optimizer/suggestions/importers/do_all.py create mode 100644 discopop_library/discopop_optimizer/suggestions/importers/reduction.py create mode 100644 discopop_library/discopop_optimizer/utilities/optimization/GlobalOptimization/RandomSamples.py create mode 100644 discopop_library/discopop_optimizer/utilities/optimization/GlobalOptimization/__init__.py create mode 100644 discopop_library/discopop_optimizer/utilities/optimization/LocalOptimization/TopDown.py create mode 100644 discopop_library/discopop_optimizer/utilities/optimization/LocalOptimization/__init__.py create mode 100644 discopop_library/discopop_optimizer/utilities/optimization/__init__.py create mode 100644 discopop_wizard/screens/optimizer/__init__.py create mode 100644 discopop_wizard/screens/optimizer/binding.py diff --git a/discopop_explorer/pattern_detection.py b/discopop_explorer/pattern_detection.py index da94ef417..bfc01445e 100644 --- a/discopop_explorer/pattern_detection.py +++ b/discopop_explorer/pattern_detection.py @@ -7,9 +7,11 @@ # directory for details. import os import sys +from typing import Dict, Union from discopop_library.discopop_optimizer.OptimizationGraph import OptimizationGraph from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.system.System import System from discopop_library.discopop_optimizer.scheduling.workload_delta import ( get_workload_delta_for_cu_node, ) @@ -124,39 +126,52 @@ def __identify_scheduling_clauses( ) -> DetectionResult: """Identifies scheduling clauses for suggestions and returns the updated DetectionResult""" # construct optimization graph (basically an acyclic representation of the PET) - experiment = Experiment(project_folder_path, res, file_mapping_path) - print("\tcreating optimization graph...") - # saves optimization graph in experiment - optimization_graph = OptimizationGraph(project_folder_path, experiment) - print("\tDetermining scheduling clauses...") - with alive_bar(len(res.do_all)) as progress_bar: - for do_all_suggestion in res.do_all: - for node_id in get_nodes_from_cu_id( - experiment.optimization_graph, do_all_suggestion.node_id - ): - workload_delta, min_workload, max_workload = get_workload_delta_for_cu_node( - experiment, node_id - ) - print( - "DOALL @ ", - do_all_suggestion.node_id, - " -> ", - "node_id: ", - node_id, - " --> Delta WL: ", - workload_delta, - " (", - min_workload, - "/", - max_workload, - ")", - file=sys.stderr, - ) - # todo - # very naive and non-robust approach, needs improvement in the future - # reflects the behavior as described in https://dl.acm.org/doi/pdf/10.1145/3330345.3330375 - if workload_delta != 0: - do_all_suggestion.scheduling_clause = "dynamic" - progress_bar() + system = System(headless=True) + discopop_output_path = project_folder_path + discopop_optimizer_path = "INVALID_DUMMY" + code_export_path = "INVALID_DUMMY" + arguments_1 = {"--compile-command": "make"} + experiment = Experiment( + project_folder_path, + discopop_output_path, + discopop_optimizer_path, + code_export_path, + file_mapping_path, + system, + res, + arguments_1, + ) + arguments_2 = {"--exhaustive-search": False, "--headless-mode": True} + optimization_graph = OptimizationGraph( + project_folder_path, experiment, arguments_2, None, False + ) + + for do_all_suggestion in res.do_all: + for node_id in get_nodes_from_cu_id( + experiment.optimization_graph, do_all_suggestion.node_id + ): + workload_delta, min_workload, max_workload = get_workload_delta_for_cu_node( + experiment, node_id + ) + print( + "DOALL @ ", + do_all_suggestion.node_id, + " -> ", + "node_id: ", + node_id, + " --> Delta WL: ", + workload_delta, + " (", + min_workload, + "/", + max_workload, + ")", + file=sys.stderr, + ) + # todo + # very naive and non-robust approach, needs improvement in the future + # reflects the behavior as described in https://dl.acm.org/doi/pdf/10.1145/3330345.3330375 + if workload_delta != 0: + do_all_suggestion.scheduling_clause = "dynamic" return res diff --git a/discopop_explorer/utils.py b/discopop_explorer/utils.py index 00ad1eb62..6a9d64bc8 100644 --- a/discopop_explorer/utils.py +++ b/discopop_explorer/utils.py @@ -98,7 +98,9 @@ def is_loop_index2(pet: PETGraphX, root_loop: Node, var_name: str) -> bool: # NOTE: left old code as it may become relevant again in the near future # We decided to omit the information that computes the workload and the relevant codes. For large programs (e.g., ffmpeg), the generated Data.xml file becomes very large. However, we keep the code here because we would like to integrate a hotspot detection algorithm (TODO: Bertin) with the parallelism discovery. Then, we need to retrieve the information to decide which code sections (loops or functions) are worth parallelizing. -def calculate_workload(pet: PETGraphX, node: Node) -> int: +def calculate_workload( + pet: PETGraphX, node: Node, ignore_function_calls_and_cached_values: bool = False +) -> int: """Calculates and stores the workload for a given node The workload is the number of instructions multiplied by respective number of iterations @@ -108,7 +110,8 @@ def calculate_workload(pet: PETGraphX, node: Node) -> int: """ # check if value already present if node.workload is not None: - return node.workload + if not ignore_function_calls_and_cached_values: + return node.workload res = 0 if node.type == NodeType.DUMMY: # store workload @@ -118,15 +121,25 @@ def calculate_workload(pet: PETGraphX, node: Node) -> int: # if a function is called, replace the instruction with the costs of the called function # note: recursive function calls are counted as a single instruction res += cast(CUNode, node).instructions_count - for calls_edge in pet.out_edges(cast(CUNode, node).id, EdgeType.CALLSNODE): - # add costs of the called function - res += calculate_workload(pet, pet.node_at(calls_edge[1])) - # substract 1 to ignore the call instruction - # todo: should we keep the cost for the call instruction and just add the costs of the called funciton? - res -= 1 + if not ignore_function_calls_and_cached_values: + for calls_edge in pet.out_edges(cast(CUNode, node).id, EdgeType.CALLSNODE): + # add costs of the called function + res += calculate_workload( + pet, + pet.node_at(calls_edge[1]), + ignore_function_calls_and_cached_values=ignore_function_calls_and_cached_values, + ) + # substract 1 to ignore the call instruction + # todo: should we keep the cost for the call instruction and just add the costs of the called funciton? + res -= 1 elif node.type == NodeType.FUNC: - for child in find_subnodes(pet, node, EdgeType.CHILD): - res += calculate_workload(pet, child) + if not ignore_function_calls_and_cached_values: + for child in find_subnodes(pet, node, EdgeType.CHILD): + res += calculate_workload( + pet, + child, + ignore_function_calls_and_cached_values=ignore_function_calls_and_cached_values, + ) elif node.type == NodeType.LOOP: for child in find_subnodes(pet, node, EdgeType.CHILD): if child.type == NodeType.CU: @@ -139,7 +152,15 @@ def calculate_workload(pet: PETGraphX, node: Node) -> int: if cast(LoopNode, node).loop_data is None else cast(LoopData, cast(LoopNode, node).loop_data).average_iteration_count ) - res += calculate_workload(pet, child) * average_iteration_count + 1 + res += ( + calculate_workload( + pet, + child, + ignore_function_calls_and_cached_values=ignore_function_calls_and_cached_values, + ) + * average_iteration_count + + 1 + ) else: # determine average iteration count. Use traditional iteration count as a fallback average_iteration_count = ( @@ -147,7 +168,14 @@ def calculate_workload(pet: PETGraphX, node: Node) -> int: if cast(LoopNode, node).loop_data is None else cast(LoopData, cast(LoopNode, node).loop_data).average_iteration_count ) - res += calculate_workload(pet, child) * average_iteration_count + res += ( + calculate_workload( + pet, + child, + ignore_function_calls_and_cached_values=ignore_function_calls_and_cached_values, + ) + * average_iteration_count + ) else: # determine average iteration count. Use traditional iteration count as a fallback average_iteration_count = ( @@ -155,7 +183,14 @@ def calculate_workload(pet: PETGraphX, node: Node) -> int: if cast(LoopNode, node).loop_data is None else cast(LoopData, cast(LoopNode, node).loop_data).average_iteration_count ) - res += calculate_workload(pet, child) * average_iteration_count + res += ( + calculate_workload( + pet, + child, + ignore_function_calls_and_cached_values=ignore_function_calls_and_cached_values, + ) + * average_iteration_count + ) # store workload node.workload = res return res diff --git a/discopop_library/discopop_optimizer/CostModels/CostModel.py b/discopop_library/discopop_optimizer/CostModels/CostModel.py new file mode 100644 index 000000000..6e8f81788 --- /dev/null +++ b/discopop_library/discopop_optimizer/CostModels/CostModel.py @@ -0,0 +1,250 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import random +import sys +import warnings +from functools import cmp_to_key +from typing import List, Dict, Tuple, Optional + +import numpy as np +import sympy +from matplotlib import pyplot as plt # type: ignore +from sympy import Function, Symbol, init_printing, Expr, N, nsimplify, Integer # type: ignore + +from discopop_library.discopop_optimizer.classes.enums.Distributions import FreeSymbolDistribution + + +class CostModel(object): + path_decisions: List[int] + identifier: str + parallelizable_costs: Expr + sequential_costs: Expr + raw_parallelizable_costs: Optional[Expr] + raw_sequential_costs: Optional[Expr] + free_symbol_ranges: Dict[Symbol, Tuple[float, float]] + free_symbol_distributions: Dict[Symbol, FreeSymbolDistribution] + symbol_value_suggestions: Dict[Symbol, Expr] + + def toJSON(self): + return "AsDF" + + def __init__( + self, + parallelizable_costs: Expr, + sequential_costs: Expr, + identifier: str = "None", + path_decisions=None, + symbol_value_suggestions: Optional[Dict[Symbol, Expr]] = None, + ): + if sequential_costs == sympy.nan: + raise ValueError("NAN: ", sequential_costs) + if path_decisions is None: + self.path_decisions = [] + else: + # used for the construction of combined models + self.path_decisions = path_decisions + if symbol_value_suggestions is None: + self.symbol_value_suggestions = dict() + else: + self.symbol_value_suggestions = symbol_value_suggestions + self.identifier = identifier + self.parallelizable_costs = parallelizable_costs + self.sequential_costs = sequential_costs + + def __str__(self): + return str(self.parallelizable_costs) + "\n" + str(self.sequential_costs) + + def print(self, file=sys.stdout): + init_printing() + print("\tPARALLEL:") + print("\t", self.parallelizable_costs, file=file) + print("\tSERIAL") + print("\t", self.sequential_costs, file=file) + + def parallelizable_plus_combine(self, other): + """Combines both models in the following fashion: + f(x,y) = + ==> x.parallelizable_costs + y.parallelizable_costs + ==> x.sequential_costs + y.sequential_costs""" + if other is None: + return self + parallelizable_costs = self.parallelizable_costs + other.parallelizable_costs + sequential_costs = self.sequential_costs + other.sequential_costs + path_decisions = self.path_decisions + other.path_decisions + # merge dictionaries + value_suggestions = {**self.symbol_value_suggestions, **other.symbol_value_suggestions} + return CostModel( + parallelizable_costs, + sequential_costs, + path_decisions=path_decisions, + symbol_value_suggestions=value_suggestions, + ) + + def parallelizable_divide_combine(self, other): + """Combines both models in the following fashion: + f(x,y) = + ==> x.parallelizable_costs / y.parallelizable_costs + ==> x.sequential_costs / y.sequential_costs""" + if other is None: + return self + parallelizable_costs = self.parallelizable_costs / other.parallelizable_costs + sequential_costs = self.sequential_costs / other.sequential_costs + path_decisions = self.path_decisions + other.path_decisions + value_suggestions = self.symbol_value_suggestions | other.symbol_value_suggestions + return CostModel( + parallelizable_costs, + sequential_costs, + path_decisions=path_decisions, + symbol_value_suggestions=value_suggestions, + ) + + def parallelizable_multiply_combine(self, other): + """Combines both models in the following fashion: + f(x,y) = + ==> x.parallelizable_costs * y.parallelizable_costs + ==> x.sequential_costs * y.sequential_costs""" + if other is None: + return self + parallelizable_costs = self.parallelizable_costs * other.parallelizable_costs + sequential_costs = self.sequential_costs * other.sequential_costs + path_decisions = self.path_decisions + other.path_decisions + # merge dictionaries + value_suggestions = {**self.symbol_value_suggestions, **other.symbol_value_suggestions} + return CostModel( + parallelizable_costs, + sequential_costs, + path_decisions=path_decisions, + symbol_value_suggestions=value_suggestions, + ) + + def register_child(self, other, root_node, experiment, all_function_nodes, current_device): + """Registers a child node for the given model. + Does not modify the stored model in self or other.""" + return root_node.register_child(other, experiment, all_function_nodes, current_device) + + def register_successor(self, other, root_node): + """Registers a successor node for the given model. + Does not modify the stored model in self or other.""" + return root_node.register_successor(other) + + def __lt__(self, other): + """Compare both models. + The comparison is based on random sampling and may not be correct in all cases! + """ + decision_tendency = ( + 0 # positive -> self was evaluated to be smaller more often than the other way around + ) + decided = False + counter = 0 + # Sampling parameters + min_count = 50 + max_count = 300 + decision_threshold = 0.85 + # Weibull distribution parameters + alpha, beta = 0.8, 1.3 + + # draw and evaluate random samples until either the max_count has been reached, or a decision has been made + # sampling points may be drawn from a uniform or weibull distribution in a left-skewed or right-skewed + # configuration. The Parameters of the weibull distribution can be defined above. + while not (decided or counter > max_count) or counter < min_count: + counter += 1 + # determine random sampling point + sampling_point = dict() + for symbol in self.free_symbol_ranges: + range_min, range_max = self.free_symbol_ranges[symbol] + if self.free_symbol_distributions[symbol] == FreeSymbolDistribution.UNIFORM: + # draw from uniform distribution + sampling_point[symbol] = random.uniform(range_min, range_max) + else: + # use_weibull_distribution + # get normalized random value from distribution + normalized_pick = 42.0 + while normalized_pick < 0 or normalized_pick > 1: + normalized_pick = random.weibullvariate(alpha, beta) + if self.free_symbol_distributions[symbol] == FreeSymbolDistribution.LEFT_HEAVY: + # calculate sampling point using the range starting from minimum + sampling_point[symbol] = ( + range_min + (range_max - range_min) * normalized_pick + ) + else: + # simulate a right heavy distribution + # calculate sampling point using the range starting from maximum + sampling_point[symbol] = ( + range_max - (range_max - range_min) * normalized_pick + ) + + # evaluate both functions at the sampling point + substituted_model_1_1 = self.parallelizable_costs.xreplace(sampling_point) + numerical_result_1_1 = substituted_model_1_1.evalf() + + substituted_model_1_2 = self.sequential_costs.xreplace(sampling_point) + numerical_result_1_2 = substituted_model_1_2.evalf() + + substituted_model_2_1 = other.parallelizable_costs.xreplace(sampling_point) + numerical_result_2_1 = substituted_model_2_1.evalf() + + substituted_model_2_2 = other.sequential_costs.xreplace(sampling_point) + numerical_result_2_2 = substituted_model_2_2.evalf() + + # use re() to get real values in case extrap has introduced sqrt's + total_1 = sympy.re(numerical_result_1_1 + numerical_result_1_2) + sympy.im( + numerical_result_1_1 + numerical_result_1_2 + ) + total_2 = sympy.re(numerical_result_2_1 + numerical_result_2_2) + sympy.im( + numerical_result_2_1 + numerical_result_2_2 + ) + + # replace Expr(0) with 0 + total_1 = total_1.subs({Expr(Integer(0)): Integer(0)}) + total_2 = total_2.subs({Expr(Integer(0)): Integer(0)}) + + # determine relation between the numerical results + try: + if total_1 < total_2: + decision_tendency += 1 + else: + decision_tendency -= 1 + except TypeError as te: + print("Total 1: ", total_1) + print("Total 2: ", total_2) + raise te + + if counter > min_count: + # check if a decision in either direction can be made + if decision_threshold < (abs(decision_tendency) / counter): + decided = True + + # check if a decision has been made + if decided: + if decision_tendency > 0: + return True + else: + return False + else: + return False + + def __plot_weibull_distributions(self, alpha: float, beta: float): + """For Debug reasons. Plots the left and right side heavy weibull distributions using the given parameters.""" + x = np.arange(1, 100.0) / 100.0 # normalized to [0,1] + + def weibull(x, n, a): + return (a / n) * (x / n) ** (a - 1) * np.exp(-((x / n) ** a)) + + plt.plot(x, weibull(x, alpha, beta), label="Alpha: " + str(alpha) + " Beta: " + str(beta)) + + # show random picks + ax = plt.subplot(1, 1, 1) # type: ignore + k = 100 + for i in range(0, k): + # get normalized values + y_rnd = 42.0 + while y_rnd < 0 or y_rnd > 1: + y_rnd = random.weibullvariate(alpha, beta) + ax.plot(y_rnd, 1, "or") + plt.legend() + plt.show() diff --git a/discopop_library/discopop_optimizer/CostModels/DataTransfer/DataTransferCosts.py b/discopop_library/discopop_optimizer/CostModels/DataTransfer/DataTransferCosts.py new file mode 100644 index 000000000..d0049cdc8 --- /dev/null +++ b/discopop_library/discopop_optimizer/CostModels/DataTransfer/DataTransferCosts.py @@ -0,0 +1,45 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import Dict, List, Tuple, Set + +import networkx as nx # type: ignore +from sympy import Integer # type: ignore + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.context.ContextObjectUtils import ( + get_transfer_costs, +) +from discopop_library.discopop_optimizer.classes.context.Update import Update +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot + + +def add_data_transfer_costs( + graph: nx.DiGraph, + function_performance_models: Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]], + environment: Experiment, +) -> Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]]: + """Calculates the data transfer costs for each of the given performance models and adds them to the respective model.""" + result_dict: Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]] = dict() + + for function in function_performance_models: + result_dict[function] = [] + for cost_model, context in function_performance_models[function]: + # calculate costs of data transfers + # For now, it is assumed, that only a single data transfer happens at once + # and no asynchronous transfers happen. + # todo: This should be extended in the future. + data_transfer_costs = get_transfer_costs(context, environment=environment) + + # extend the cost_model + cost_model = cost_model.parallelizable_plus_combine(data_transfer_costs) + + # add the updated entry to result_dict + result_dict[function].append((cost_model, context)) + return result_dict diff --git a/discopop_library/discopop_optimizer/CostModels/DataTransfer/__init__.py b/discopop_library/discopop_optimizer/CostModels/DataTransfer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/CostModels/__init__.py b/discopop_library/discopop_optimizer/CostModels/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/CostModels/utilities.py b/discopop_library/discopop_optimizer/CostModels/utilities.py new file mode 100644 index 000000000..c14bb2c26 --- /dev/null +++ b/discopop_library/discopop_optimizer/CostModels/utilities.py @@ -0,0 +1,375 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import copy +import random +from typing import List, Dict, cast, Set, Optional + +import networkx as nx # type: ignore +import sympy # type: ignore +from sympy import Integer, Expr, Symbol # type: ignore + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.classes.nodes.GenericNode import GenericNode +from discopop_library.discopop_optimizer.classes.nodes.Loop import Loop +from discopop_library.discopop_optimizer.classes.system.devices.GPU import GPU +from discopop_library.discopop_optimizer.utilities.MOGUtilities import ( + get_successors, + get_children, + data_at, + get_edge_data, + get_requirements, + get_out_options, + get_in_options, + get_all_parents, + get_all_function_nodes, +) + + +def get_performance_models_for_functions( + experiment: Experiment, graph: nx.DiGraph +) -> Dict[FunctionRoot, List[CostModel]]: + performance_models: Dict[FunctionRoot, List[CostModel]] = dict() + # get called FunctionRoots from cu ids + all_function_nodes = [ + cast(FunctionRoot, data_at(experiment.optimization_graph, fn_id)) + for fn_id in get_all_function_nodes(experiment.optimization_graph) + ] + + for node_id in graph.nodes: + node_data = graph.nodes[node_id]["data"] + node_data.node_id = node_id # fix potential mismatches due to node copying + + if isinstance(node_data, FunctionRoot): + # start the collection at the first child of the function + for child_id in get_children(graph, node_id): + performance_models[node_data] = get_node_performance_models( + experiment, graph, child_id, set(), all_function_nodes + ) + + # filter out NaN - Models + performance_models[node_data] = [ + model + for model in performance_models[node_data] + if model.parallelizable_costs != sympy.nan + ] + + return performance_models + + +def get_node_performance_models( + experiment: Experiment, + graph: nx.DiGraph, + node_id: int, + visited_nodes: Set[int], + all_function_nodes: List[FunctionRoot], + restrict_to_decisions: Optional[Set[int]] = None, + do_not_allow_decisions: Optional[Set[int]] = None, + get_single_random_model: bool = False, + ignore_node_costs: Optional[List[int]] = None, + current_device_id=None, +) -> List[CostModel]: + """Returns the performance models for the given node. + If a set of decision is specified for restrict_to_decisions, only those non-sequential decisions will be allowed. + Caution: List might be empty! + """ + result_list: List[CostModel] = [] + successors = get_successors(graph, node_id) + successor_count = len(successors) + node_data = data_at(graph, node_id) + if node_data.execute_in_parallel: + current_device_id = node_data.device_id + visited_nodes.add(node_id) + + # consider performance models of children + children_models = get_performance_models_for_children( + experiment, + graph, + node_id, + copy.deepcopy(visited_nodes), + all_function_nodes, + restrict_to_decisions=restrict_to_decisions, + do_not_allow_decisions=do_not_allow_decisions, + get_single_random_model=get_single_random_model, + ) + + if len(children_models) == 0: + if ignore_node_costs is not None: + if node_data.node_id in ignore_node_costs: + children_models = [CostModel(Integer(0), Integer(0))] + else: + children_models = [ + node_data.get_cost_model( + experiment, + all_function_nodes, + experiment.get_system().get_device(current_device_id), + ) + ] + + else: + if ignore_node_costs is not None: + if node_data.node_id in ignore_node_costs: + tmp_node_cost_model = CostModel(Integer(0), Integer(0)) + else: + tmp_node_cost_model = node_data.get_cost_model( + experiment, + all_function_nodes, + experiment.get_system().get_device(current_device_id), + ) + + for idx, child_model in enumerate(children_models): + if ignore_node_costs is not None: + if node_data.node_id not in ignore_node_costs: + children_models[idx] = tmp_node_cost_model.register_child( + child_model, + node_data, + experiment, + all_function_nodes, + experiment.get_system().get_device(current_device_id), + ) + else: + children_models[idx] = tmp_node_cost_model.register_child( + child_model, + node_data, + experiment, + all_function_nodes, + experiment.get_system().get_device(current_device_id), + ) + + # construct the performance models + if successor_count >= 1: + removed_successors = False + if get_single_random_model and successor_count > 1: + # pick only a single successor + successors = [random.choice(successors)] + removed_successors = True + + for children_model in children_models: + for successor in successors: + # ## CHECK REQUIREMENTS ## + # check if successor validates a requirements edge to restrain the created combinations + # 1.1. check if optionEdge between any node in visited_nodes and successor exists + # 1.2. if so, check if option edge to other node in visited nodes exists + # 1.3. if so, check if a requirements edge between both option exists. + # 1.4. if not, the path is not valid since two options for the same + # source code location would be selected + path_invalid = False + # 1.1 + for visited_node_id in visited_nodes: + options = get_out_options(graph, visited_node_id) + if successor in options: + # 1.2 + visited_options = [opt for opt in options if opt in visited_nodes] + if len(visited_options) > 0: + # 1.3 + for vo in visited_options: + # 1.4 + if successor not in get_requirements(graph, vo): + path_invalid = True + break + if path_invalid: + break + if path_invalid: + continue + + # 2 check if a sibling of successor exists which has a requirements edge to a visited node + # 2.1 check if an incoming or outgoing option edge exists, get the node id for the sequential version + # 2.2 for all parallelization options + # 2.3 check if a requirements edge to a visited node exists + # 2.4 if so, stop if successor is NOT the parallelization option with the requirements edge + # 2.1 + for sibling in successors: + sequential_version_ids = [] + if len(get_out_options(graph, sibling)) > 0: + sequential_version_ids = [sibling] + else: + for seq in get_in_options(graph, sibling): + sequential_version_ids.append(seq) + # 2.2 + for seq in sequential_version_ids: + for option in get_out_options(graph, seq): + # 2.3 + for visited_req in [ + req + for req in get_requirements(graph, option) + if req in visited_nodes + ]: + # 2.4 + if visited_req != successor: + path_invalid = True + break + if path_invalid: + break + if path_invalid: + break + + # do not allow nested parallelization suggestions on devices of type GPU + if True: # option to disable this check + combined_visited_nodes = visited_nodes + combined_visited_nodes.add(successor) + gpu_suggestions = [ + node_id + for node_id in combined_visited_nodes + if isinstance( + experiment.get_system().get_device(data_at(graph, node_id).device_id), + GPU, + ) + ] + # check if two suggestions are in a contained-in relation + for suggestion_1 in gpu_suggestions: + all_parents = get_all_parents(graph, suggestion_1) + for suggestion_2 in gpu_suggestions: + if suggestion_1 == suggestion_2: + continue + if suggestion_2 in all_parents: + path_invalid = True + break + if path_invalid: + break + + # check if the current decision invalidates decision requirements, if some are specified + if restrict_to_decisions is not None: + if not ( + successor in restrict_to_decisions + or data_at(graph, successor).suggestion is None + ): + path_invalid = True + if not path_invalid: + if data_at(graph, successor).suggestion is None: + # if the sequential "fallback" has been used, check if a different option is specifically + # mentioned in restrict_to_decisions. If so, the sequential fallback shall be ignored. + options = get_out_options(graph, successor) + restricted_options = [ + opt for opt in options if opt in restrict_to_decisions + ] + if len(restricted_options) != 0: + # do not use he sequential fallback since a required option exists + path_invalid = True + + if do_not_allow_decisions is not None: + if successor in do_not_allow_decisions: + path_invalid = True + + if path_invalid: + continue + + # ## END OF REQUIREMENTS CHECK ## + + combined_model = children_model + # add transfer costs + transfer_costs_model = get_edge_data(graph, node_id, successor).get_cost_model() + combined_model = combined_model.parallelizable_plus_combine(transfer_costs_model) + + # if the successor is "determined" by a path decision, add path decision to the combined model + if len(successors) > 1 or removed_successors: + combined_model.path_decisions.append(successor) + # append the model of the successor + for model in get_node_performance_models( + experiment, + graph, + successor, + copy.deepcopy(visited_nodes), + all_function_nodes, + restrict_to_decisions=restrict_to_decisions, + do_not_allow_decisions=do_not_allow_decisions, + get_single_random_model=get_single_random_model, + ignore_node_costs=ignore_node_costs, + ): + tmp = combined_model.parallelizable_plus_combine(model) + tmp.path_decisions += [ + d for d in model.path_decisions if d not in tmp.path_decisions + ] + result_list.append(tmp) + if len(result_list) >= 1: + return result_list + + # successor count == 0 or successor count > 1 + return children_models + + +def get_performance_models_for_children( + experiment: Experiment, + graph: nx.DiGraph, + node_id: int, + visited_nodes: Set[int], + all_function_nodes: List[FunctionRoot], + restrict_to_decisions: Optional[Set[int]] = None, + do_not_allow_decisions: Optional[Set[int]] = None, + get_single_random_model: bool = False, +) -> List[CostModel]: + """Construct a performance model for the children of the given node, or return None if no children exist""" + # todo: consider children + child_models: List[CostModel] = [] + + # create all combinations for models from children + first_iteration = True + for child_id in get_children(graph, node_id): + if first_iteration: + first_iteration = False + for model in get_node_performance_models( + experiment, + graph, + child_id, + copy.deepcopy(visited_nodes), + all_function_nodes, + restrict_to_decisions=restrict_to_decisions, + do_not_allow_decisions=do_not_allow_decisions, + get_single_random_model=get_single_random_model, + ): + # initialize list of child models + child_models.append(model) + else: + # create "product set" of child models + product_set = [] + for model in get_node_performance_models( + experiment, + graph, + child_id, + copy.deepcopy(visited_nodes), + all_function_nodes, + restrict_to_decisions=restrict_to_decisions, + do_not_allow_decisions=do_not_allow_decisions, + get_single_random_model=get_single_random_model, + ): + temp_models = [cm.parallelizable_plus_combine(model) for cm in child_models] + product_set += temp_models + child_models = product_set + return child_models + + +def print_introduced_symbols_per_node(graph: nx.DiGraph): + print("Introduced Symbols:") + for node_id in graph.nodes: + print("NodeID: ", node_id) + for symbol in data_at(graph, node_id).introduced_symbols: + print("\t: ", symbol) + print() + + +def get_random_path( + experiment: Experiment, graph: nx.DiGraph, root_id: int, must_contain: Optional[Set[int]] = None +) -> CostModel: + # get called FunctionRoots from cu ids + all_function_nodes = [ + cast(FunctionRoot, data_at(experiment.optimization_graph, fn_id)) + for fn_id in get_all_function_nodes(experiment.optimization_graph) + ] + random_models = get_node_performance_models( + experiment, + graph, + root_id, + set(), + all_function_nodes, + restrict_to_decisions=must_contain, + get_single_random_model=True, + ignore_node_costs=[cast(FunctionRoot, data_at(graph, root_id)).node_id], + ) + # filter out NaN - Models + random_models = [model for model in random_models if model.parallelizable_costs != sympy.nan] + return random.choice(random_models) diff --git a/discopop_library/discopop_optimizer/DataTransfers/DataTransfers.py b/discopop_library/discopop_optimizer/DataTransfers/DataTransfers.py new file mode 100644 index 000000000..9a947f9bd --- /dev/null +++ b/discopop_library/discopop_optimizer/DataTransfers/DataTransfers.py @@ -0,0 +1,135 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import Dict, List, Tuple, Set, cast + +import networkx as nx # type: ignore + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.context.Update import Update +from discopop_library.discopop_optimizer.classes.nodes.ContextNode import ContextNode +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.classes.types.Aliases import DeviceID +from discopop_library.discopop_optimizer.utilities.MOGUtilities import ( + get_successors, + data_at, + get_children, +) + + +def calculate_data_transfers( + graph: nx.DiGraph, function_performance_models: Dict[FunctionRoot, List[CostModel]] +) -> Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]]: + """Calculate data transfers for each performance model and append the respective ContextObject to the result.""" + result_dict: Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]] = dict() + for function in function_performance_models: + result_dict[function] = [] + for model in function_performance_models[function]: + # create a ContextObject for the current path + context = ContextObject(function.node_id, [function.device_id]) + context = get_path_context(function.node_id, graph, model, context) + result_dict[function].append((model, context)) + return result_dict + + +def get_path_context( + node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject +) -> ContextObject: + """passes the context Object along the path and returns the context once the end has been reached""" + # push device id to stack if necessary + node_data = data_at(graph, node_id) + if node_data.device_id is not None: + context.last_seen_device_ids.append(node_data.device_id) + + # calculate context modifications for the current node + context = __check_current_node(node_id, graph, model, context) + + # calculate context modifications for the children of the current node + context = __check_children(node_id, graph, model, context) + + # pop device id from stack if necessary + if node_data.device_id is not None: + context.last_seen_device_ids.pop() + + # set last_visited_node_id to the original node_id, + # since the calculation continues from node_id after the children have been visited + context.last_visited_node_id = node_id + + # pass context to the right successor of the current node + # At most a single node can be a successor, since the given model represents a single path through the graph. + successors = get_successors(graph, node_id) + if len(successors) == 1: + # pass context to the single successor + return get_path_context(successors[0], graph, model, context) + + elif len(successors) == 0: + # no successor exists, return the current context + return context + + else: + # multiple successors exist + # find the successor which represents the path decision included in the model + suitable_successors = [succ for succ in successors if succ in model.path_decisions] + if len(suitable_successors) != 1: + raise ValueError( + "Invalid amount of potential successors (", + len(suitable_successors), + ") for path split at node:", + node_id, + "using decisions: ", + model.path_decisions, + "successors:", + successors, + ) + # suitable successor identified. + # pass the current context to the successor + return get_path_context(suitable_successors[0], graph, model, context) + + +def __check_current_node( + node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject +) -> ContextObject: + """Check if the given node results in modifications to the given context. + Return a modified version of the context which contains the required updates.""" + # due to the Read-Compute-Write paradigm used to create the Computational Units, + # this structure is assumed for the nodes and their MemoryAccesses as well. + + # check if any data needs to be updated from a different device before reading + # if so, the context will be supplemented with the identified updates + node_data = data_at(graph, node_id) + + if isinstance(node_data, ContextNode): + # apply modifications according to encountered context node + updated_context = cast(ContextNode, data_at(graph, node_id)).get_modified_context( + node_id, graph, model, context + ) + return updated_context + + context = context.calculate_and_perform_necessary_updates( + node_data.read_memory_regions, + cast(int, context.last_seen_device_ids[-1]), + node_data.node_id, + ) + + # add the writes performed by the given node to the context + context = context.add_writes( + node_data.written_memory_regions, cast(int, context.last_seen_device_ids[-1]) + ) + + return context + + +def __check_children( + node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject +) -> ContextObject: + # pass context to all children + for child in get_children(graph, node_id): + # reset last_visited_node_id inbetween visiting children + context.last_visited_node_id = node_id + context = get_path_context(child, graph, model, context) + return context diff --git a/discopop_library/discopop_optimizer/DataTransfers/__init__.py b/discopop_library/discopop_optimizer/DataTransfers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/Microbench/ExtrapInterpolatedMicrobench.py b/discopop_library/discopop_optimizer/Microbench/ExtrapInterpolatedMicrobench.py index 4a97fed23..7f0e1076b 100644 --- a/discopop_library/discopop_optimizer/Microbench/ExtrapInterpolatedMicrobench.py +++ b/discopop_library/discopop_optimizer/Microbench/ExtrapInterpolatedMicrobench.py @@ -22,6 +22,7 @@ from extrap.modelers.model_generator import ModelGenerator # type: ignore from extrap.modelers.multi_parameter.multi_parameter_modeler import MultiParameterModeler # type: ignore from sympy.parsing.sympy_parser import parse_expr # type: ignore +import sympy # This class uses extrap to extrapolate microbench measurements. @@ -68,7 +69,9 @@ def getFunctionSympy( function_str = function_str.replace("r", "iterations") function_str = function_str.replace("p", "threads") function_str = function_str.replace("q", "workload") - expr = parse_expr(function_str) + # define replacements to match representations used in extrap output + function_mappings = {"log2": lambda x: sympy.log(x, 2)} + expr = parse_expr(function_str, local_dict=function_mappings) return expr def getMeasurements(self): diff --git a/discopop_library/discopop_optimizer/Microbench/Microbench.py b/discopop_library/discopop_optimizer/Microbench/Microbench.py index bee1eea76..6963eb8f8 100644 --- a/discopop_library/discopop_optimizer/Microbench/Microbench.py +++ b/discopop_library/discopop_optimizer/Microbench/Microbench.py @@ -29,6 +29,7 @@ class MicrobenchType(str, Enum): SEPARATED = "SEPARATED" SHARED = "SHARED" PRIVATE = "PRIVATE" + FOR = "FOR" class MicrobenchDimension(str, Enum): diff --git a/discopop_library/discopop_optimizer/Microbench/utils.py b/discopop_library/discopop_optimizer/Microbench/utils.py new file mode 100644 index 000000000..ccc4d5473 --- /dev/null +++ b/discopop_library/discopop_optimizer/Microbench/utils.py @@ -0,0 +1,63 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import cast + +from sympy import Expr, Integer, Max + + +def convert_discopop_to_microbench_workload( + discopop_per_iteration_workload: Expr, iteration_count: Expr +) -> Expr: + """Converts the estimated workload into the workload measurement used by Microbench. + According to Formula: + + Wm = ((Wd/iD)-w0)/(iD*wi) with + + Wm = Microbench workload + Wd = DiscoPoP workload + iD = iteration count + w0 = 13, initialization workload for inner loops + wi = 14, observed workload added by a single iteration of the inner loop according to DiscoPoP's workload definition + + returns: Wm + + source: Bertins Thesis: TODO + """ + # todo + w0 = 13 + wi = 14 + + # discopop_per_iteration_workload is equivalent to (Wd/iD) + # for the same reason, iteration_count is disregarded + Wm = cast(Expr, (discopop_per_iteration_workload - w0) / wi) + # do not allow negative return values + return Max(Wm, Integer(1)) + + +def convert_microbench_to_discopop_workload( + microbench_workload: Expr, iteration_count: Expr +) -> Expr: + """Converts the estimated workload into the workload measurement used by DiscoPoP. + According to Formula: + + Wd = iD(Wm * iD * wi + w0) with + + Wm = Microbench workload + Wd = DiscoPoP workload + iD = iteration count + w0 = 13, initialization workload for inner loops + wi = 14, observed workload added by a single iteration of the inner loop according to DiscoPoP's workload definition + + returns: Wd + + source: Bertins Thesis: TODO + """ + wi = Integer(14) + w0 = Integer(13) + + return cast(Expr, iteration_count * (microbench_workload * iteration_count * wi + w0)) diff --git a/discopop_library/discopop_optimizer/OptimizationGraph.py b/discopop_library/discopop_optimizer/OptimizationGraph.py index 2f943e24b..2338485f2 100644 --- a/discopop_library/discopop_optimizer/OptimizationGraph.py +++ b/discopop_library/discopop_optimizer/OptimizationGraph.py @@ -5,39 +5,232 @@ # This software may be modified and distributed under the terms of # the 3-Clause BSD License. See the LICENSE file in the package base # directory for details. -from typing import Dict, cast, List, Tuple, Set +from typing import Dict, cast, List, Tuple, Optional import jsonpickle # type: ignore import networkx as nx # type: ignore import sympy # type: ignore +import tkinter as tk from spb import plot3d, MB # type: ignore from sympy import Integer, Expr, Symbol, lambdify, plot, Float, init_printing, simplify, diff # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.CostModels.DataTransfer.DataTransferCosts import ( + add_data_transfer_costs, +) +from discopop_library.discopop_optimizer.CostModels.utilities import ( + get_performance_models_for_functions, +) +from discopop_library.discopop_optimizer.DataTransfers.DataTransfers import calculate_data_transfers from discopop_library.discopop_optimizer.PETParser.PETParser import PETParser from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.Variables.ExperimentUtils import ( + show_function_models, + export_to_json, + perform_headless_execution, +) +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.enums.Distributions import FreeSymbolDistribution +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.gui.queries.ValueTableQuery import ( + query_user_for_symbol_values, +) +from discopop_library.discopop_optimizer.suggestions.importers.base import import_suggestions +from discopop_library.discopop_optimizer.utilities.optimization.LocalOptimization.TopDown import ( + get_locally_optimized_models, +) class OptimizationGraph(object): next_free_node_id: int - experiment: Experiment - pet_parser: PETParser - def __init__(self, project_folder_path, experiment: Experiment): + def __init__( + self, + project_folder_path, + experiment: Experiment, + arguments: Dict, + parent_frame: Optional[tk.Frame], + destroy_window_after_execution: bool, + ): # construct optimization graph from PET Graph + # save graph in experiment + experiment.optimization_graph, self.next_free_node_id = PETParser( + experiment.detection_result.pet, experiment + ).parse() - # save reference to experiment - self.experiment = experiment + # get performance models for sequential execution + sequential_function_performance_models = get_performance_models_for_functions( + experiment, experiment.optimization_graph + ) + sequential_function_performance_models_with_transfers = calculate_data_transfers( + experiment.optimization_graph, sequential_function_performance_models + ) + sequential_complete_performance_models = add_data_transfer_costs( + experiment.optimization_graph, + sequential_function_performance_models_with_transfers, + experiment, + ) - # construct PETParser - self.pet_parser = PETParser(experiment.detection_result.pet, experiment) + # import parallelization suggestions + experiment.optimization_graph = import_suggestions( + experiment.detection_result, + experiment.optimization_graph, + self.get_next_free_node_id, + experiment, + ) - # save graph in experiment - self.experiment.optimization_graph, self.next_free_node_id = self.pet_parser.parse() + # perform an exhaustive search if requested + if arguments["--exhaustive-search"]: + # calculate performance models without data transfers + function_performance_models = get_performance_models_for_functions( + experiment, experiment.optimization_graph + ) + + # calculate and append necessary data transfers to the models + function_performance_models_with_transfers = calculate_data_transfers( + experiment.optimization_graph, function_performance_models + ) + + # calculate and append costs of data transfers to the performance models + exhaustive_performance_models = add_data_transfer_costs( + experiment.optimization_graph, + function_performance_models_with_transfers, + experiment, + ) + else: + exhaustive_performance_models = dict() + + # collect free symbols + # free_symbols: Set[Symbol] = set() + free_symbol_ranges: Dict[Symbol, Tuple[float, float]] = dict() + free_symbol_distributions: Dict[Symbol, FreeSymbolDistribution] = dict() + + # for function in complete_performance_models: + # for pair in complete_performance_models[function]: + # model, context = pair + # free_symbols.update(cast(List[Symbol], model.parallelizable_costs.free_symbols)) + # free_symbols.update(cast(List[Symbol], model.sequential_costs.free_symbols)) + # suggested_values = suggested_values | model.symbol_value_suggestions + sorted_free_symbols = sorted(list(experiment.free_symbols), key=lambda x: x.name) + + # query user for values for free symbols + query_results = query_user_for_symbol_values( + sorted_free_symbols, experiment.suggested_values, arguments, parent_frame + ) + for symbol, value, start_value, end_value, symbol_distribution in query_results: + if value is not None: + experiment.substitutions[symbol] = Float(value) + else: + free_symbol_ranges[symbol] = (cast(float, start_value), cast(float, end_value)) + free_symbol_distributions[symbol] = cast( + FreeSymbolDistribution, symbol_distribution + ) + + # by default, select the sequential version of each function for substitution + for function in sequential_complete_performance_models: + experiment.selected_paths_per_function[ + function + ] = sequential_complete_performance_models[function][0] + + # add function symbols to list of substitutions + # collect substitutions + for function in experiment.selected_paths_per_function: + # register substitution + experiment.substitutions[ + cast(Symbol, function.sequential_costs) + ] = experiment.selected_paths_per_function[function][0].sequential_costs + experiment.substitutions[ + cast(Symbol, function.parallelizable_costs) + ] = experiment.selected_paths_per_function[function][0].parallelizable_costs + + # TODO END OF DUMMY + + # set free symbol ranges and distributions for comparisons + # for idx, function in enumerate(complete_performance_models): + # for pair in complete_performance_models[function]: + # model, context = pair + # model.free_symbol_ranges = free_symbol_ranges + # model.free_symbol_distributions = free_symbol_distributions + + # save substitutions, sorted_free_symbols, free_symbol_ranges and free_symbol_distributions in experiment + experiment.sorted_free_symbols = sorted_free_symbols + experiment.free_symbol_ranges = free_symbol_ranges + experiment.free_symbol_distributions = free_symbol_distributions + + # create locally optimized model + locally_optimized_models = get_locally_optimized_models( + experiment, + experiment.optimization_graph, + experiment.substitutions, + experiment, + free_symbol_ranges, + free_symbol_distributions, + ) + + # set free symbol ranges and distributions for comparisons + for idx, function in enumerate(locally_optimized_models): + for pair in locally_optimized_models[function]: + model, context = pair + model.free_symbol_ranges = free_symbol_ranges + model.free_symbol_distributions = free_symbol_distributions + + # set free symbol ranges and distributions for comparisons + for idx, function in enumerate(exhaustive_performance_models): + for pair in exhaustive_performance_models[function]: + model, context = pair + model.free_symbol_ranges = free_symbol_ranges + model.free_symbol_distributions = free_symbol_distributions + + # find the minimum of the exhaustive list of models + exhaustive_minima: Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]] = dict() + if len(exhaustive_performance_models) > 0: + print("Sorting exhaustive models...") + for function in exhaustive_performance_models: + sorted_exhaustive_performance_models = sorted( + exhaustive_performance_models[function], key=lambda x: x[0] + ) + exhaustive_minima[function] = [sorted_exhaustive_performance_models[0]] + print("\tDone.") + + for function in sequential_complete_performance_models: + # show table of options + options: List[Tuple[CostModel, ContextObject, str]] = [] + # add exhaustive minima + if function in exhaustive_minima: + for model_tmp in exhaustive_minima[function]: + options.append((model_tmp[0], model_tmp[1], "Exhaustive Min")) + + options.append( + ( + sequential_complete_performance_models[function][0][0], + sequential_complete_performance_models[function][0][1], + "Sequential", + ) + ) + if function in locally_optimized_models: + options.append( + ( + locally_optimized_models[function][0][0], + locally_optimized_models[function][0][1], + "Locally Optimized", + ) + ) + # save options to experiment + experiment.function_models[function] = options + + # show function models + if not arguments["--headless-mode"]: + if parent_frame is None: + raise ValueError("No frame provided!") + show_function_models(experiment, parent_frame, destroy_window_after_execution) + else: + # perform actions for headless mode + perform_headless_execution(experiment) + + # save experiment to disk + export_to_json(experiment) def get_next_free_node_id(self): buffer = self.next_free_node_id self.next_free_node_id += 1 return buffer - - def show(self): - show(self.experiment.optimization_graph) diff --git a/discopop_library/discopop_optimizer/PETParser/DataAccesses/CalculateUpdates.py b/discopop_library/discopop_optimizer/PETParser/DataAccesses/CalculateUpdates.py new file mode 100644 index 000000000..c184ac298 --- /dev/null +++ b/discopop_library/discopop_optimizer/PETParser/DataAccesses/CalculateUpdates.py @@ -0,0 +1,7 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. diff --git a/discopop_library/discopop_optimizer/PETParser/PETParser.py b/discopop_library/discopop_optimizer/PETParser/PETParser.py index 8b85adff2..a83811aa5 100644 --- a/discopop_library/discopop_optimizer/PETParser/PETParser.py +++ b/discopop_library/discopop_optimizer/PETParser/PETParser.py @@ -41,10 +41,10 @@ add_temporary_edge, redirect_edge, convert_temporary_edges, + show, get_all_function_nodes, get_read_and_written_data_from_subgraph, ) -from discopop_library.discopop_optimizer.utilities.visualization.plotting import show class PETParser(object): @@ -120,7 +120,6 @@ def __parse_path_node( # create a duplicate of the root node duplicate_node_id = self.get_new_node_id() tmp_node_data = data_at(self.graph, root_node_id) - tmp_node_data.node_id = duplicate_node_id self.graph.add_node(duplicate_node_id, data=tmp_node_data) # connect duplicated entry node to children @@ -161,7 +160,6 @@ def __parse_branching_point( # Step 1: create duplicate of root node duplicate_node_id = self.get_new_node_id() tmp_node_data = data_at(self.graph, root_node_id) - tmp_node_data.node_id = duplicate_node_id self.graph.add_node(duplicate_node_id, data=tmp_node_data) # Step 2: create and connect context snapshot @@ -236,8 +234,10 @@ def __add_cu_nodes(self): node_id=new_node_id, experiment=self.experiment, cu_id=cu_node.id, - sequential_workload=calculate_workload(self.pet, cu_node), - parallelizable_workload=0, + sequential_workload=0, + parallelizable_workload=calculate_workload( + self.pet, cu_node, ignore_function_calls_and_cached_values=True + ), written_memory_regions=written_memory_regions, read_memory_regions=read_memory_regions, ), @@ -266,7 +266,9 @@ def __add_loop_nodes(self): data=Loop( node_id=new_node_id, cu_id=loop_node.id, - parallelizable_workload=calculate_workload(self.pet, loop_node), + discopop_workload=calculate_workload( + self.pet, loop_node, ignore_function_calls_and_cached_values=True + ), iterations=iteration_count, position=loop_node.start_position(), experiment=self.experiment, @@ -302,7 +304,6 @@ def __add_loop_nodes(self): entry_node_id = self.cu_id_to_graph_node_id[entry_node_cu_id] entry_node_data = data_at(self.graph, entry_node_id) copied_entry_node_id = self.get_new_node_id() - entry_node_data.node_id = copied_entry_node_id self.graph.add_node(copied_entry_node_id, data=entry_node_data) # redirect edges from inside the loop to the copy of the entry node diff --git a/discopop_library/discopop_optimizer/Variables/Experiment.py b/discopop_library/discopop_optimizer/Variables/Experiment.py index e01f3f3db..5a5e68e30 100644 --- a/discopop_library/discopop_optimizer/Variables/Experiment.py +++ b/discopop_library/discopop_optimizer/Variables/Experiment.py @@ -5,14 +5,22 @@ # This software may be modified and distributed under the terms of # the 3-Clause BSD License. See the LICENSE file in the package base # directory for details. +import os from pathlib import Path -from typing import Dict, Set, Optional +from typing import Dict, Tuple, Set, Optional, List import networkx as nx # type: ignore -from sympy import Integer, Symbol, Expr # type: ignore +from sympy import Integer, Symbol, Expr, Float # type: ignore +from discopop_explorer.PETGraphX import MemoryRegion from discopop_library.result_classes.DetectionResult import DetectionResult from discopop_library.PathManagement.PathManagement import load_file_mapping +from discopop_library.MemoryRegions.utils import get_sizes_of_memory_regions +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.enums.Distributions import FreeSymbolDistribution +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.classes.system.System import System class Experiment(object): @@ -32,27 +40,92 @@ class Experiment(object): ## END OF SETTINGS + __system: System + # all free symbols will be added to this list for simple retrieval and user query free_symbols: Set[Symbol] = set() + substitutions: Dict[Symbol, Expr] = dict() + sorted_free_symbols: List[Symbol] = [] + free_symbol_ranges: Dict[Symbol, Tuple[float, float]] = dict() + free_symbol_distributions: Dict[Symbol, FreeSymbolDistribution] = dict() # value suggestions for all free symbols will be stored in this dictionary suggested_values: Dict[Symbol, Expr] = dict() - project_path: str + __memory_region_sizes: Dict[MemoryRegion, int] # sizes in Bytes + + project_path: Path + discopop_output_path: Path + discopop_optimizer_path: Path + code_export_path: Path + file_mapping: Dict[int, Path] # file-mapping + detection_result: DetectionResult + + function_models: Dict[FunctionRoot, List[Tuple[CostModel, ContextObject, str]]] + selected_paths_per_function: Dict[FunctionRoot, Tuple[CostModel, ContextObject]] + optimization_graph: nx.DiGraph + compile_check_command: str # passed to code generator for the validation of generated code + def __init__( self, - project_path: str, - detection_result: DetectionResult, + project_path, + discopop_output_path, + discopop_optimizer_path, + code_export_path, file_mapping_path: str, + system: System, + detection_result: DetectionResult, + arguments: Dict, ): + self.__system = system self.detection_result = detection_result + + self.__memory_region_sizes = get_sizes_of_memory_regions( + set(), + os.path.join(discopop_output_path, "memory_regions.txt"), + return_all_memory_regions=True, + ) + self.project_path = project_path + self.discopop_output_path = discopop_output_path + self.discopop_optimizer_path = discopop_optimizer_path + self.code_export_path = code_export_path + self.file_mapping = load_file_mapping(file_mapping_path) + # collect free symbols from system + for free_symbol, value_suggestion in system.get_free_symbols(): + self.register_free_symbol(free_symbol, value_suggestion) + + self.function_models = dict() + self.selected_paths_per_function = dict() + + self.compile_check_command = arguments["--compile-command"] + + def get_memory_region_size( + self, memory_region: MemoryRegion, use_symbolic_value: bool = False + ) -> Tuple[Expr, Expr]: + if memory_region not in self.__memory_region_sizes: + self.__memory_region_sizes[memory_region] = 8 # assume 8 Bytes for unknown sizes + + if use_symbolic_value: + symbolic_memory_region_size = Symbol("mem_reg_size_" + str(memory_region)) + # register the symbolic value in the environment + self.register_free_symbol( + symbolic_memory_region_size, + value_suggestion=Integer(self.__memory_region_sizes[memory_region]), + ) + return symbolic_memory_region_size, Integer(self.__memory_region_sizes[memory_region]) + else: + return Integer(self.__memory_region_sizes[memory_region]), Integer(0) + def register_free_symbol(self, symbol: Symbol, value_suggestion: Optional[Expr] = None): self.free_symbols.add(symbol) if value_suggestion is not None: self.suggested_values[symbol] = value_suggestion + + def get_system(self) -> System: + return self.__system diff --git a/discopop_library/discopop_optimizer/Variables/ExperimentUtils.py b/discopop_library/discopop_optimizer/Variables/ExperimentUtils.py new file mode 100644 index 000000000..25c4f9869 --- /dev/null +++ b/discopop_library/discopop_optimizer/Variables/ExperimentUtils.py @@ -0,0 +1,168 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import os +import pickle +from tkinter import Tk, Button +from typing import List, Optional, cast, Set + +import jsonpickle # type: ignore +import jsons # type: ignore + +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.bindings.CodeGenerator import export_code +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.gui.presentation.OptionTable import ( + show_options, + add_random_models, +) +from discopop_library.discopop_optimizer.gui.widgets.ScrollableFrame import ScrollableFrameWidget +from discopop_library.discopop_optimizer.utilities.MOGUtilities import data_at +import tkinter as tk + + +def show_function_models( + experiment: Experiment, + parent_frame: tk.Frame, + destroy_window_after_execution: bool, + show_functions: Optional[List[FunctionRoot]] = None, +): + considered_functions = ( + show_functions if show_functions is not None else experiment.function_models + ) + # show function selection dialogue + parent_frame.rowconfigure(0, weight=1) + parent_frame.rowconfigure(1, weight=1) + parent_frame.columnconfigure(1, weight=1) + parent_frame.columnconfigure(0, weight=1) + + scrollable_frame_widget = ScrollableFrameWidget(parent_frame) + scrollable_frame = scrollable_frame_widget.get_scrollable_frame() + + spawned_windows: List[tk.Toplevel] = [] + + # populate scrollable frame + for idx, function in enumerate(considered_functions): + function_button = Button( + scrollable_frame, + text=function.name, + command=lambda func=function: show_options( # type: ignore + # random options might be added by show_options + experiment.detection_result.pet, + experiment.optimization_graph, + experiment, + experiment.function_models[func], + experiment.sorted_free_symbols, + experiment.free_symbol_ranges, + experiment.free_symbol_distributions, + func, + parent_frame, + spawned_windows, + window_title="Function: " + func.name, + ), + ) + function_button.grid(row=idx, column=0) + + # finalize scrollable frame + scrollable_frame_widget.finalize(row_count=len(considered_functions), row=0, col=0) + + def __on_press(): + for w in spawned_windows: + try: + w.destroy() + except tk.TclError: + pass + if destroy_window_after_execution: + for c in parent_frame.winfo_children(): + c.destroy() + parent_frame.winfo_toplevel().destroy() + else: + parent_frame.quit() + + # add exit button + exit_button = Button(parent_frame, text="Exit", command=lambda: __on_press()) # type: ignore + exit_button.grid(row=1, column=0) + + parent_frame.mainloop() + + +def perform_headless_execution( + experiment: Experiment, +): + print("Headless execution...") + for function in experiment.function_models: + print("\t", function.name) + # generate random models + updated_options = add_random_models( + None, + experiment.detection_result.pet, + experiment.optimization_graph, + experiment, + experiment.function_models[function], + experiment.sorted_free_symbols, + experiment.free_symbol_ranges, + experiment.free_symbol_distributions, + function, + None, + [], + show_results=False, + ) + + # save models + experiment.function_models[function] = updated_options + # export models to code + for opt, ctx, label in experiment.function_models[function]: + export_code( + experiment.detection_result.pet, + experiment.optimization_graph, + experiment, + opt, + ctx, + label, + function, + ) + + +def export_to_json(experiment: Experiment): + # convert functionRoot in function_models to node ids + to_be_added = [] + to_be_deleted = [] + for old_key in experiment.function_models: + new_key = old_key.node_id + to_be_added.append((new_key, experiment.function_models[old_key])) + to_be_deleted.append(old_key) + + for k1 in to_be_deleted: + del experiment.function_models[k1] + for k2, v in to_be_added: + experiment.function_models[k2] = v # type: ignore + + experiment_dump_path: str = os.path.join( + experiment.discopop_optimizer_path, "last_experiment.pickle" + ) + if not os.path.exists(experiment.discopop_optimizer_path): + os.makedirs(experiment.discopop_optimizer_path) + pickle.dump(experiment, open(experiment_dump_path, "wb")) + + +def restore_session(json_file: str) -> Experiment: + experiment: Experiment = pickle.load(open(json_file, "rb")) + + # convert keys of function_models to FunctionRoot objects + to_be_added = [] + to_be_deleted = [] + for old_key in experiment.function_models: + new_key = cast(FunctionRoot, data_at(experiment.optimization_graph, cast(int, old_key))) + to_be_added.append((new_key, experiment.function_models[old_key])) + to_be_deleted.append(old_key) + + for k, v in to_be_added: + experiment.function_models[k] = v + for k in to_be_deleted: + del experiment.function_models[k] + + return experiment diff --git a/discopop_library/discopop_optimizer/__main__.py b/discopop_library/discopop_optimizer/__main__.py new file mode 100644 index 000000000..3c2b5ff67 --- /dev/null +++ b/discopop_library/discopop_optimizer/__main__.py @@ -0,0 +1,301 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. + +"""Discopop Suggestion Optimizer + +Usage: + discopop_optimizer --compile-command [--project ] [--file-mapping ] [--detection-result-dump ] + [--execute-created-models] [--execute-single-model ] [--clean-created-code] [--code-export-path ] [--dp-output-path ] + [--executable-arguments ] [--linker-flags ] [--make-target ] [--make-flags ] + [--executable-name ] [--execution-repetitions ] [--execution-append-measurements] + [--exhaustive-search] [--headless-mode] [--doall-microbench-file ] [--reduction-microbench-file ] + +OPTIONAL ARGUMENTS: + --project= Path to the directory that contains your makefile [default: .] + --file-mapping= Path to the FileMapping.txt. [default: FileMapping.txt] + --detection-result-dump= Path to the dumped detection result JSON. [default: detection_result_dump.json] + --execute-created-models Compiles, executes and measures models already stored in the project folder. + Does not start the optimization pipeline. + Required: --executable-name + --execute-single-model= Execute the given model only. Path to the JSON or filename, if the model is located in --code-export-path. + Does not start the optimization pipeline. + Required: --executable-name [default: ] + --execution-repetitions= Repeat measurements [default: 1] + --execution-append-measurements If set, measurement files from previous executions will be kept and new results + will be appended. + --clean-created-code Removes all stored code modifications. + --code-export-path= Directory where generated CodeStorageObjects are located. [default: .discopop_optimizer/code_exports] + --dp-output-path= Directory where output files of DiscoPoP are located. [default: .discopop] + --executable-name= Name of the executable generate by your makefile. + Must be specified if --execute-created-models is used! [default: ] + --executable-arguments= + run your application with these arguments [default: ] + --linker-flags= if your build requires to link other libraries + please provide the necessary linker flags. e.g. -lm [default: ] + --make-target= specify a specific Make target to be built, + if not specified the default make target is used. [default: ] + --make-flags= specify flags which will be passed through to make. [default: ] + --compile-command= specify a command which shall be executed to compile the application + --exhaustive-search Perform an exhaustive search for the optimal set of parallelization suggestions for the + given environment + --headless-mode Do not show any GUI prompts. Does not reuse prior results. + Uses the suggested default values. Creates random models. + Exports all models to code. Saves all models. + --reduction-microbench-file= Path to the microbenchmark output which represents the overhead + of reduction suggestions. + Default model: 0 + --doall-microbench-file= Path to the microbenchmark output which represents the overhead + of reduction suggestions. + Default model: 0 + -h --help Show this screen +""" +import os +import shutil +import sys +import tkinter as tk +from typing import Optional, Union + +import jsonpickle # type: ignore +import pstats2 # type:ignore +from docopt import docopt # type:ignore +from schema import Schema, Use, SchemaError # type:ignore +from sympy import Symbol, Integer + +from discopop_library.result_classes.DetectionResult import DetectionResult +from discopop_library.discopop_optimizer.Microbench.ExtrapInterpolatedMicrobench import ( + ExtrapInterpolatedMicrobench, +) +from discopop_library.discopop_optimizer.Microbench.Microbench import MicrobenchType +from discopop_library.discopop_optimizer.OptimizationGraph import OptimizationGraph +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.Variables.ExperimentUtils import ( + restore_session, + show_function_models, + export_to_json, +) +from discopop_library.discopop_optimizer.classes.system.System import System +from discopop_library.discopop_optimizer.classes.system.devices.CPU import CPU +from discopop_library.discopop_optimizer.classes.system.devices.GPU import GPU +from discopop_library.discopop_optimizer.execution.stored_models import ( + execute_stored_models, + execute_single_model, +) +import tkinter.messagebox + +docopt_schema = Schema( + { + "--project": Use(str), + "--file-mapping": Use(str), + "--detection-result-dump": Use(str), + "--execute-created-models": Use(str), + "--clean-created-code": Use(str), + "--code-export-path": Use(str), + "--dp-output-path": Use(str), + "--executable-arguments": Use(str), + "--executable-name": Use(str), + "--linker-flags": Use(str), + "--make-target": Use(str), + "--make-flags": Use(str), + "--execution-repetitions": Use(str), + "--execute-single-model": Use(str), + "--compile-command": Use(str), + "--execution-append-measurements": Use(str), + "--exhaustive-search": Use(str), + "--headless-mode": Use(str), + "--doall-microbench-file": Use(str), + "--reduction-microbench-file": Use(str), + } +) + + +def get_path(base_path: str, file_name: str) -> str: + """Combines path and filename if it is not absolute + + :param base_path: path + :param file_name: file name + :return: path to file + """ + result_path = file_name if os.path.isabs(file_name) else os.path.join(base_path, file_name) + return os.path.normpath(result_path) + + +def main(): + """Invokes the discopop_optimizer using the given parameters""" + arguments = docopt(__doc__) + + try: + arguments = docopt_schema.validate(arguments) + except SchemaError as e: + exit(e) + + # prepare arguments + arguments["--project"] = get_path(os.getcwd(), arguments["--project"]) + arguments["--execute-created-models"] = ( + False if arguments["--execute-created-models"] == "False" else True + ) + arguments["--execution-append-measurements"] = ( + False if arguments["--execution-append-measurements"] == "False" else True + ) + arguments["--clean-created-code"] = ( + False if arguments["--clean-created-code"] == "False" else True + ) + arguments["--code-export-path"] = get_path( + arguments["--project"], arguments["--code-export-path"] + ) + arguments["--exhaustive-search"] = ( + False if arguments["--exhaustive-search"] == "False" else True + ) + arguments["--headless-mode"] = False if arguments["--headless-mode"] == "False" else True + arguments["--dp-output-path"] = get_path(arguments["--project"], arguments["--dp-output-path"]) + arguments["--file-mapping"] = get_path( + arguments["--dp-output-path"], arguments["--file-mapping"] + ) + arguments["--detection-result-dump"] = get_path( + arguments["--dp-output-path"], arguments["--detection-result-dump"] + ) + arguments["--dp-optimizer-path"] = os.path.join(arguments["--project"], ".discopop_optimizer") + arguments["--make-target"] = ( + None if arguments["--make-target"] == "None" else arguments["--make-target"] + ) + arguments["--execute-single-model"] = ( + None + if len(arguments["--execute-single-model"]) == 0 + else get_path(arguments["--code-export-path"], arguments["--execute-single-model"]) + ) + arguments["--compile-command"] = ( + None if len(arguments["--compile-command"]) == 0 else arguments["--compile-command"] + ) + + print("Starting discopop_optimizer...") + for arg_name in arguments: + print("\t", arg_name, "=", arguments[arg_name]) + + if arguments["--execute-created-models"] or arguments["--execute-single-model"] is not None: + if arguments["--executable-name"] == "": + raise ValueError("Please specify the name of your executable using --executable-name!") + if arguments["--execute-single-model"] is None: + execute_stored_models(arguments) + else: + execute_single_model(arguments) + sys.exit(0) + + if arguments["--clean-created-code"]: + # remove stored parallel code if requested + print("Removing old exported code...", end="") + if os.path.exists(arguments["--code-export-path"]): + shutil.rmtree(arguments["--code-export-path"]) + os.makedirs(arguments["--code-export-path"]) + print("Done.") + + start_optimizer(arguments) + + +def start_optimizer(arguments, parent_frame: Optional[tk.Frame] = None): + # create gui frame if none given + if parent_frame is None: + tk_root: Union[tk.Tk, tk.Toplevel] = tk.Tk() + # configure window size + tk_root.geometry("1000x600") + parent_frame = tk.Frame(tk_root) + parent_frame.pack(fill=tk.BOTH) + destroy_window_after_execution = True + else: + tk_root = parent_frame.winfo_toplevel() + destroy_window_after_execution = False + + # ask if previous session should be loaded + load_result: bool = False + if ( + os.path.exists(os.path.join(arguments["--dp-optimizer-path"], "last_experiment.pickle")) + and not arguments["--headless-mode"] + ): + load_result = tkinter.messagebox.askyesno( + parent=parent_frame, + title="Restore Results?", + message="Do you like to load the experiment from the previous session?", + ) + if load_result: + # load results from previous session + experiment = restore_session( + os.path.join(arguments["--dp-optimizer-path"], "last_experiment.pickle") + ) + show_function_models(experiment, parent_frame, destroy_window_after_execution) + # save experiment to disk + export_to_json(experiment) + + else: + # create a new session + # load detection result + print("Loading detection result and PET...", end="") + detection_result_dump_str = "" + with open(arguments["--detection-result-dump"], "r") as f: + detection_result_dump_str = f.read() + detection_result: DetectionResult = jsonpickle.decode(detection_result_dump_str) + print("Done") + + # define System + # todo make system user-configurable, or detect it using a set of benchmarks + system = System() + + # todo connections between devices might happen as update to host + update to second device. + # As of right now, connections between two devices are implemented in this manner. + # todo check if OpenMP allows direct data transfers between devices + + # load overhead measurements into system if existent + if arguments["--doall-microbench-file"] != "None": + microbench_file = arguments["--doall-microbench-file"] + if not os.path.isfile(microbench_file): + raise FileNotFoundError(f"Microbenchmark file not found: {microbench_file}") + # construct and set overhead model for doall suggestions + system.set_device_doall_overhead_model( + system.get_device(0), + ExtrapInterpolatedMicrobench(microbench_file).getFunctionSympy(), + ) + if arguments["--reduction-microbench-file"] != "None": + microbench_file = arguments["--reduction-microbench-file"] + if not os.path.isfile(microbench_file): + raise FileNotFoundError(f"Microbenchmark file not found: {microbench_file}") + # construct and set overhead model for reduction suggestions + system.set_reduction_overhead_model( + system.get_device(0), + ExtrapInterpolatedMicrobench(microbench_file).getFunctionSympy( + benchType=MicrobenchType.FOR + ), + ) + + # define Environment + experiment = Experiment( + arguments["--project"], + arguments["--dp-output-path"], + arguments["--dp-optimizer-path"], + arguments["--code-export-path"], + arguments["--file-mapping"], + system, + detection_result, + arguments, + ) + + # invoke optimization graph + optimization_graph = OptimizationGraph( + arguments["--dp-output-path"], + experiment, + arguments, + parent_frame, + destroy_window_after_execution, + ) + + if destroy_window_after_execution: + try: + tk_root.destroy() + except tkinter.TclError: + # Window has been destroyed already + pass + + +if __name__ == "__main__": + main() diff --git a/discopop_library/discopop_optimizer/bindings/CodeGenerator.py b/discopop_library/discopop_optimizer/bindings/CodeGenerator.py new file mode 100644 index 000000000..98d3d0e13 --- /dev/null +++ b/discopop_library/discopop_optimizer/bindings/CodeGenerator.py @@ -0,0 +1,362 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import os +import random +import string +import subprocess +import warnings +from typing import List, Tuple, Dict, cast, Optional + +import jsonpickle # type: ignore +import jsons # type: ignore +import networkx as nx # type: ignore + +from discopop_explorer.PETGraphX import NodeID, PETGraphX +from discopop_explorer.pattern_detectors.PatternInfo import PatternInfo +from discopop_explorer.pattern_detectors.device_updates import DeviceUpdateInfo +from discopop_explorer.variable import Variable +from discopop_library.CodeGenerator.CodeGenerator import ( + from_pattern_info as code_gen_from_pattern_info, +) +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.bindings.CodeStorageObject import CodeStorageObject +from discopop_library.discopop_optimizer.bindings.utilities import is_child_of_any +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.classes.system.devices.Device import Device +from discopop_library.discopop_optimizer.classes.system.devices.GPU import GPU +from discopop_library.discopop_optimizer.utilities.MOGUtilities import ( + data_at, + get_parents, +) + + +def export_code( + pet: PETGraphX, + graph: nx.DiGraph, + experiment: Experiment, + cost_model: CostModel, + context: ContextObject, + label: str, + parent_function: FunctionRoot, +): + """Provides a binding to the discopop code generator and exports the code corresponding to the given cost model""" + # only consider "empty", i.e. sequential cases, if they are either the sequential or the locally optimized option + if len(cost_model.path_decisions) == 0: + if label not in ["Sequential", "Locally Optimized"]: + print("warning: skipped empty model in code export.") + + # collect suggestions to be applied + suggestions: List[Tuple[Device, PatternInfo, str, Optional[int]]] = [] + for decision in cost_model.path_decisions: + graph_node = data_at(graph, decision) + device: Device = experiment.get_system().get_device( + 0 if graph_node.device_id is None else graph_node.device_id + ) + if graph_node.suggestion is None: + continue + suggestion, suggestion_type = device.get_device_specific_pattern_info( + graph_node.suggestion, graph_node.suggestion_type + ) + suggestions.append((device, suggestion, suggestion_type, decision)) + + # todo collect updates to be applied + for update in context.necessary_updates: + if update.source_device_id != update.target_device_id: + # calculate correct position for update (updates inside target regions are NOT allowed by OpenMP!) + # insert pragma before the usage position of the data + unchecked_target_nodes: List[int] = [update.target_node_id] + checked_target_nodes: List[int] = [] + + while len(unchecked_target_nodes) != 0: + cur_node = unchecked_target_nodes.pop() + if is_child_of_any( + graph, + cur_node, + [ + s_node_id + for _, sugg, s_type, s_node_id in suggestions + if s_type.startswith("gpu_") + ], + ): + tmp_parents = get_parents(graph, cur_node) + if len(tmp_parents) != 0: + unchecked_target_nodes += [ + p + for p in tmp_parents + if p not in checked_target_nodes and p not in unchecked_target_nodes + ] + + else: + if cur_node not in checked_target_nodes: + checked_target_nodes.append(cur_node) + + for checked_node_id in checked_target_nodes: + start_line = pet.node_at( + cast(NodeID, data_at(graph, checked_node_id).original_cu_id) + ).start_position() + end_line = start_line + + # register update + source_cu_id = cast(NodeID, data_at(graph, update.source_node_id).original_cu_id) + target_cu_id = cast(NodeID, data_at(graph, update.target_node_id).original_cu_id) + + if source_cu_id is None or target_cu_id is None: + warnings.warn( + "Could not register update: " + str(update) + " @ Line: " + start_line + ) + else: + # get updated variable + var_obj = pet.get_variable( + target_cu_id, cast(str, update.write_data_access.var_name) + ) + if var_obj is None: + raise ValueError( + "Could not find variable object for: " + + str(update) + + " -> " + + str(update.write_data_access.var_name) + ) + + # get amount of elements targeted by the update + update_elements = int( + int( + experiment.get_memory_region_size( + update.write_data_access.memory_region + )[0].evalf() + ) + / var_obj.sizeInByte + ) + + # debug! + # add memory access to var_name + dbg_info = ( + "-" + + str(update.write_data_access.memory_region) + + "-" + + str(update.write_data_access.unique_id) + + "-vartype:" + + var_obj.type + ) + + # add range to updated var name if necessary + if ( + update_elements > 1 + and update.write_data_access.var_name is not None + and "**" in var_obj.type + ): + updated_var_name: Optional[str] = ( + str(update.write_data_access.var_name) + + "[:" + + str(update_elements) + + "]" + # + dbg_info + ) + else: + updated_var_name = update.write_data_access.var_name + + suggestions.append( + ( + experiment.get_system().get_device(update.source_device_id), + DeviceUpdateInfo( + pet, + pet.node_at(source_cu_id), + pet.node_at(target_cu_id), + update.write_data_access.memory_region, + updated_var_name, + 0 if update.source_device_id is None else update.source_device_id, + 0 + if update.source_device_id is None + else cast(int, update.target_device_id), + start_line, + end_line, + update.is_first_data_occurrence, + cast( + GPU, experiment.get_system().get_device(update.source_device_id) + ).openmp_device_id, + cast( + GPU, experiment.get_system().get_device(update.target_device_id) + ).openmp_device_id, + ), + "device_update", + None, + ) + ) + + # remove duplicates + to_be_removed = [] + buffer = [] + for entry in suggestions: + entry_str = str(entry[0]) + ";" + str(entry[1]) + ";" + str(entry[2]) + ";" + str(entry[3]) + if entry_str not in buffer: + buffer.append(entry_str) + else: + to_be_removed.append(entry) + for entry in to_be_removed: + if entry in suggestions: + suggestions.remove(entry) + + # remove updates, if an identical entry exists + to_be_removed = [] + for entry_1 in suggestions: + if not isinstance(entry_1[1], DeviceUpdateInfo): + continue + entry_str_1 = ( + str(entry_1[0]) + + ";" + + ( + entry_1[1].get_str_without_first_data_occurrence() + if isinstance(entry_1[1], DeviceUpdateInfo) + else str(entry_1[1]) + ) + + ";" + + str(entry_1[2]) + + ";" + + str(entry_1[3]) + ) + for entry_2 in suggestions: + if not isinstance(entry_1[1], DeviceUpdateInfo): + continue + entry_str_2 = ( + str(entry_2[0]) + + ";" + + ( + entry_2[1].get_str_without_first_data_occurrence() + if isinstance(entry_2[1], DeviceUpdateInfo) + else str(entry_2[1]) + ) + + ";" + + str(entry_2[2]) + + ";" + + str(entry_2[3]) + ) + if entry_str_1 == entry_str_2: + if ( + entry_1[1].is_first_data_occurrence + and not cast(DeviceUpdateInfo, entry_2[1]).is_first_data_occurrence + ): + to_be_removed.append(entry_2) + for entry in to_be_removed: + if entry in suggestions: + suggestions.remove(entry) + + if False: + # todo cleanup + for sugg in suggestions: + if type(sugg[1]) == DeviceUpdateInfo: + tmp = cast(DeviceUpdateInfo, sugg[1]) + print( + "First" if tmp.is_first_data_occurrence else "", + "Update: ", + tmp.start_line, + "@", + tmp.source_device_id, + "->", + tmp.start_line, + "@", + tmp.target_device_id, + "|", + tmp.mem_reg, + "|", + tmp.var_name, + ) + + # prepare patterns by type + patterns_by_type: Dict[str, List[PatternInfo]] = dict() + for device, pattern, s_type, s_node_id in suggestions: + if s_type not in patterns_by_type: + patterns_by_type[s_type] = [] + # add device id to pattern + pattern.dp_optimizer_device_id = device.openmp_device_id + patterns_by_type[s_type].append(pattern) + + # invoke the discopop code generator + modified_code = code_gen_from_pattern_info( + experiment.file_mapping, + patterns_by_type, + skip_compilation_check=True, # False, + compile_check_command=experiment.compile_check_command, + ) + # create patches from the modified code + patches = __convert_modified_code_to_patch(experiment, modified_code) + + # save modified code as CostStorageObject + export_dir = os.path.join(experiment.discopop_optimizer_path, "code_exports") + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # generate unique hash name + hash_name = "".join(random.choices(string.ascii_uppercase + string.digits, k=8)) + while os.path.exists(os.path.join(export_dir, hash_name)): + hash_name = "".join(random.choices(string.ascii_uppercase + string.digits, k=8)) + + code_storage = CodeStorageObject(cost_model, patches, parent_function, hash_name, label) + + # export code_storage object to json + print("Export JSON TO: ", os.path.join(export_dir, hash_name + ".json")) + with open(os.path.join(export_dir, hash_name + ".json"), "w+") as f: + f.write(jsonpickle.dumps(code_storage)) + f.flush() + f.close() + + print("\n############################") + print("Modified Code: Decisions: ", cost_model.path_decisions) + print("Exporting to: ", export_dir) + print("############################") + for file_id in patches: + print("#### File ID: ", file_id, " ####\n") + print(patches[file_id]) + print("\n") + + +def __convert_modified_code_to_patch( + experiment: Experiment, modified_code: Dict[int, str] +) -> Dict[int, str]: + patches: Dict[int, str] = dict() + hash_name = "".join(random.choices(string.ascii_uppercase + string.digits, k=8)) + tmp_file_name = os.path.join(os.getcwd(), hash_name + ".tmp") + for file_id in modified_code: + # write modified code to file + with open(tmp_file_name, "w+") as f: + f.write(modified_code[file_id]) + f.flush() + f.close() + + # generate diff + diff_name = tmp_file_name + ".diff" + command = [ + "diff", + "-Naru", + str(experiment.file_mapping[file_id]), + tmp_file_name, + ] + result = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + cwd=os.getcwd(), + ) + if result.returncode != 0: + print("RESULT: ", result.returncode) + print("STDERR:") + print(result.stderr) + print("STDOUT: ") + print(result.stdout) + + # save diff + patches[file_id] = result.stdout + + # cleanup environment + if os.path.exists(tmp_file_name): + os.remove(tmp_file_name) + if os.path.exists(diff_name): + os.remove(diff_name) + + return patches diff --git a/discopop_library/discopop_optimizer/bindings/CodeStorageObject.py b/discopop_library/discopop_optimizer/bindings/CodeStorageObject.py new file mode 100644 index 000000000..99acc2184 --- /dev/null +++ b/discopop_library/discopop_optimizer/bindings/CodeStorageObject.py @@ -0,0 +1,34 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import Dict + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot + + +class CodeStorageObject(object): + cost_model: CostModel + patches: Dict[int, str] + parent_function: FunctionRoot + model_id: str + label: str + + def __init__( + self, + cost_model: CostModel, + patches: Dict[int, str], + parent_function: FunctionRoot, + model_id: str, + label: str, + ): + self.cost_model = cost_model + self.patches = patches + self.parent_function = parent_function + self.model_id = model_id + self.label = label diff --git a/discopop_library/discopop_optimizer/bindings/__init__.py b/discopop_library/discopop_optimizer/bindings/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/bindings/utilities.py b/discopop_library/discopop_optimizer/bindings/utilities.py new file mode 100644 index 000000000..00e9732c7 --- /dev/null +++ b/discopop_library/discopop_optimizer/bindings/utilities.py @@ -0,0 +1,33 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import List, Optional, Set + +import networkx as nx # type: ignore + +from discopop_library.discopop_optimizer.utilities.MOGUtilities import get_parents + + +def is_child_of_any( + graph: nx.DiGraph, start_node: int, potential_parents: List[Optional[int]] +) -> bool: + """returns True, if start_node is a child of any of the potential_parents""" + # get all parents of start_node + all_parents: Set[int] = set() + cur_parents = get_parents(graph, start_node) + while len(cur_parents) != 0: + cp = cur_parents.pop(0) + all_parents.add(cp) + cur_parents += get_parents(graph, cp) + + # check if an intersection exists with potential parents + intersection = all_parents.intersection(set(potential_parents)) + + # if so, start_node is a child of the potential parents + if len(intersection) != 0: + return True + return False diff --git a/discopop_library/discopop_optimizer/classes/context/ContextObjectUtils.py b/discopop_library/discopop_optimizer/classes/context/ContextObjectUtils.py new file mode 100644 index 000000000..394c450f7 --- /dev/null +++ b/discopop_library/discopop_optimizer/classes/context/ContextObjectUtils.py @@ -0,0 +1,56 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import cast + +from sympy import Integer, Symbol + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject + + +def get_transfer_costs(context: ContextObject, environment: Experiment) -> CostModel: + """Calculates the amount of data transferred between devices as specified by self.necessary_updates and + calculates an estimation for the added transfer costs under the assumption, + that no transfers happen concurrently and every transfer is executed in a blocking, synchronous manner. + """ + total_transfer_costs = Integer(0) + symbolic_memory_region_sizes = True + symbol_value_suggestions = dict() + for update in context.necessary_updates: + # add static costs incurred by the transfer initialization + system = environment.get_system() + source_device = system.get_device(cast(int, update.source_device_id)) + target_device = system.get_device(cast(int, update.target_device_id)) + initialization_costs = system.get_network().get_transfer_initialization_costs( + source_device, target_device + ) + + total_transfer_costs += initialization_costs + + # add costs incurred by the transfer itself + transfer_speed = system.get_network().get_transfer_speed(source_device, target_device) + + # value suggestion used for symbolic values + transfer_size, value_suggestion = environment.get_memory_region_size( + update.write_data_access.memory_region, + use_symbolic_value=symbolic_memory_region_sizes, + ) + # save suggested memory region size from Environment + if symbolic_memory_region_sizes: + symbol_value_suggestions[cast(Symbol, transfer_size)] = value_suggestion + + transfer_costs = transfer_size / transfer_speed + + total_transfer_costs += transfer_costs + if symbolic_memory_region_sizes: + return CostModel( + Integer(0), total_transfer_costs, symbol_value_suggestions=symbol_value_suggestions + ) + else: + return CostModel(Integer(0), total_transfer_costs) diff --git a/discopop_library/discopop_optimizer/classes/edges/DataFlowEdge.py b/discopop_library/discopop_optimizer/classes/edges/DataFlowEdge.py index 324ccd346..8c5f7bda4 100644 --- a/discopop_library/discopop_optimizer/classes/edges/DataFlowEdge.py +++ b/discopop_library/discopop_optimizer/classes/edges/DataFlowEdge.py @@ -7,8 +7,12 @@ # directory for details. from sympy import Symbol # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.edges.GenericEdge import GenericEdge class DataFlowEdge(GenericEdge): """Used to represent data flow in the Graph. Used to determine cutoff points for local optimization""" + + def get_cost_model(self) -> CostModel: + raise ValueError("The cost of a DataFlowEdge is not defined and may never be used!") diff --git a/discopop_library/discopop_optimizer/classes/edges/GenericEdge.py b/discopop_library/discopop_optimizer/classes/edges/GenericEdge.py index 45e9055c2..691736dee 100644 --- a/discopop_library/discopop_optimizer/classes/edges/GenericEdge.py +++ b/discopop_library/discopop_optimizer/classes/edges/GenericEdge.py @@ -7,6 +7,11 @@ # directory for details. from sympy import Integer # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel + class GenericEdge(object): pass + + def get_cost_model(self) -> CostModel: + return CostModel(Integer(0), Integer(0)) diff --git a/discopop_library/discopop_optimizer/classes/edges/OptionEdge.py b/discopop_library/discopop_optimizer/classes/edges/OptionEdge.py index ae0fffca3..c4710ce14 100644 --- a/discopop_library/discopop_optimizer/classes/edges/OptionEdge.py +++ b/discopop_library/discopop_optimizer/classes/edges/OptionEdge.py @@ -7,6 +7,7 @@ # directory for details. from sympy import Symbol # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.edges.GenericEdge import GenericEdge @@ -15,3 +16,6 @@ class OptionEdge(GenericEdge): Used in combination with requirement edges to restrain path selections.""" pass + + def get_cost_model(self) -> CostModel: + raise ValueError("The cost of an OptionEdge is not defined and may never be used!") diff --git a/discopop_library/discopop_optimizer/classes/edges/RequirementEdge.py b/discopop_library/discopop_optimizer/classes/edges/RequirementEdge.py index 330a1386a..5466b8e8a 100644 --- a/discopop_library/discopop_optimizer/classes/edges/RequirementEdge.py +++ b/discopop_library/discopop_optimizer/classes/edges/RequirementEdge.py @@ -7,6 +7,7 @@ # directory for details. from sympy import Symbol # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.edges.GenericEdge import GenericEdge @@ -15,3 +16,6 @@ class RequirementEdge(GenericEdge): if A is selected, it is required that B is selected as well.""" pass + + def get_cost_model(self) -> CostModel: + raise ValueError("The cost of a RequirementEdge is not defined and may never be used!") diff --git a/discopop_library/discopop_optimizer/classes/edges/SuccessorEdge.py b/discopop_library/discopop_optimizer/classes/edges/SuccessorEdge.py index fdfb09b9d..7c98a18e6 100644 --- a/discopop_library/discopop_optimizer/classes/edges/SuccessorEdge.py +++ b/discopop_library/discopop_optimizer/classes/edges/SuccessorEdge.py @@ -7,6 +7,7 @@ # directory for details. from sympy import Symbol # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.edges.GenericEdge import GenericEdge diff --git a/discopop_library/discopop_optimizer/classes/nodes/ContextMerge.py b/discopop_library/discopop_optimizer/classes/nodes/ContextMerge.py index f4ef0b270..ed7b4d3ef 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/ContextMerge.py +++ b/discopop_library/discopop_optimizer/classes/nodes/ContextMerge.py @@ -9,6 +9,7 @@ import networkx as nx # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject from discopop_library.discopop_optimizer.classes.nodes.ContextNode import ContextNode @@ -21,7 +22,7 @@ def get_plot_label(self) -> str: return str(self.node_id) + "\nCTX\nmerge" def get_modified_context( - self, node_id: int, graph: nx.DiGraph, context: ContextObject + self, node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject ) -> ContextObject: if len(context.snapshot_stack) < 1 or len(context.save_stack) < 1: raise ValueError("Context can not be merged before creating a snapshot!") diff --git a/discopop_library/discopop_optimizer/classes/nodes/ContextNode.py b/discopop_library/discopop_optimizer/classes/nodes/ContextNode.py index cd795ab85..f7615fdf9 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/ContextNode.py +++ b/discopop_library/discopop_optimizer/classes/nodes/ContextNode.py @@ -7,20 +7,26 @@ # directory for details. import networkx as nx # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject from discopop_library.discopop_optimizer.classes.nodes.Workload import Workload +from sympy import Integer class ContextNode(Workload): def __init__(self, node_id: int, experiment): super().__init__( - node_id, experiment, cu_id=None, sequential_workload=0, parallelizable_workload=0 + node_id, + experiment, + cu_id=None, + sequential_workload=Integer(0), + parallelizable_workload=Integer(0), ) def get_plot_label(self) -> str: return str(self.node_id) + "\nCTX" def get_modified_context( - self, node_id: int, graph: nx.DiGraph, context: ContextObject + self, node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject ) -> ContextObject: return context diff --git a/discopop_library/discopop_optimizer/classes/nodes/ContextRestore.py b/discopop_library/discopop_optimizer/classes/nodes/ContextRestore.py index 63792825c..171de8caf 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/ContextRestore.py +++ b/discopop_library/discopop_optimizer/classes/nodes/ContextRestore.py @@ -9,6 +9,7 @@ import networkx as nx # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject from discopop_library.discopop_optimizer.classes.nodes.ContextNode import ContextNode @@ -21,7 +22,7 @@ def get_plot_label(self) -> str: return str(self.node_id) + "\nCTX\nrestore" def get_modified_context( - self, node_id: int, graph: nx.DiGraph, context: ContextObject + self, node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject ) -> ContextObject: # save save_stack in buffer buffer_save_stack = copy.deepcopy(context.save_stack) diff --git a/discopop_library/discopop_optimizer/classes/nodes/ContextSave.py b/discopop_library/discopop_optimizer/classes/nodes/ContextSave.py index 5162e5409..c32f104f1 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/ContextSave.py +++ b/discopop_library/discopop_optimizer/classes/nodes/ContextSave.py @@ -9,6 +9,7 @@ import networkx as nx # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject from discopop_library.discopop_optimizer.classes.nodes.ContextNode import ContextNode @@ -21,7 +22,7 @@ def get_plot_label(self) -> str: return str(self.node_id) + "\nCTX\nsave" def get_modified_context( - self, node_id: int, graph: nx.DiGraph, context: ContextObject + self, node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject ) -> ContextObject: context.save_stack[-1].append(copy.deepcopy(context)) return context diff --git a/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshot.py b/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshot.py index 161a14cd5..5f502629d 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshot.py +++ b/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshot.py @@ -9,6 +9,7 @@ import networkx as nx # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject from discopop_library.discopop_optimizer.classes.nodes.ContextNode import ContextNode @@ -21,7 +22,7 @@ def get_plot_label(self) -> str: return str(self.node_id) + "\nCTX\nsnapshot" def get_modified_context( - self, node_id: int, graph: nx.DiGraph, context: ContextObject + self, node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject ) -> ContextObject: context.snapshot_stack.append(copy.deepcopy(context)) context.save_stack.append([]) diff --git a/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshotPop.py b/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshotPop.py index a9c68dfbe..c84bea157 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshotPop.py +++ b/discopop_library/discopop_optimizer/classes/nodes/ContextSnapshotPop.py @@ -7,6 +7,7 @@ # directory for details. import networkx as nx # type: ignore +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject from discopop_library.discopop_optimizer.classes.nodes.ContextNode import ContextNode @@ -19,7 +20,7 @@ def get_plot_label(self) -> str: return str(self.node_id) + "\nCTX\nsnap pop" def get_modified_context( - self, node_id: int, graph: nx.DiGraph, context: ContextObject + self, node_id: int, graph: nx.DiGraph, model: CostModel, context: ContextObject ) -> ContextObject: context.snapshot_stack.pop() context.save_stack.pop() diff --git a/discopop_library/discopop_optimizer/classes/nodes/FunctionRoot.py b/discopop_library/discopop_optimizer/classes/nodes/FunctionRoot.py index 1d34af63d..8fb1c26c1 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/FunctionRoot.py +++ b/discopop_library/discopop_optimizer/classes/nodes/FunctionRoot.py @@ -5,25 +5,63 @@ # This software may be modified and distributed under the terms of # the 3-Clause BSD License. See the LICENSE file in the package base # directory for details. -import sys from typing import Optional from sympy import Function, Symbol, Integer, Expr # type: ignore from discopop_explorer.PETGraphX import NodeID +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.nodes.Workload import Workload class FunctionRoot(Workload): name: str + parallelizable_costs: Expr + sequential_costs: Expr + performance_model: CostModel def __init__(self, node_id: int, experiment, cu_id: Optional[NodeID], name: str): super().__init__( - node_id, experiment, cu_id, sequential_workload=0, parallelizable_workload=0 + node_id, + experiment, + cu_id, + sequential_workload=Integer(0), + parallelizable_workload=Integer(0), ) self.name = name self.device_id = 0 function_name = "function" + "_" + str(self.node_id) + "_" + self.name + self.parallelizable_costs = Symbol(function_name + "-parallelizable") + self.sequential_costs = Symbol(function_name + "-sequential") + self.performance_model = CostModel( + self.parallelizable_costs, + self.sequential_costs, + identifier=function_name, + ) def get_plot_label(self) -> str: return self.name + + def get_cost_model(self, experiment, all_function_nodes, current_device) -> CostModel: + """Model: + Spawn overhead + children""" + # todo this is only a dummy, not a finished model! + function_name = "function" + "_" + str(self.node_id) + "_" + self.name + # spawn_overhead = Symbol(function_name + "_spawn_overhead") + # self.introduced_symbols.append(spawn_overhead) + # model = Function(function_name) + # model = spawn_overhead + + # todo: check if the costs of calling functions should be included into the models + # self.performance_model = CostModel(Integer(0), Integer(0), identifier=function_name) + + # return CostModel(Integer(0), Integer(0), identifier=function_name) + # instead of returning an empty model, return symbols for sequential and parallel workload. + # These symbols can be substituted in order to evaluate for different versions of other functions + + cm = self.performance_model + # substitute Expr(0) with Integer(0) + cm.parallelizable_costs = cm.parallelizable_costs.subs({Expr(Integer(0)): Integer(0)}) + cm.sequential_costs = cm.sequential_costs.subs({Expr(Integer(0)): Integer(0)}) + + return cm diff --git a/discopop_library/discopop_optimizer/classes/nodes/GenericNode.py b/discopop_library/discopop_optimizer/classes/nodes/GenericNode.py index 01acf2a2d..92f490d6d 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/GenericNode.py +++ b/discopop_library/discopop_optimizer/classes/nodes/GenericNode.py @@ -10,7 +10,7 @@ from sympy import Symbol, Function, Integer # type: ignore from discopop_explorer.PETGraphX import NodeID -from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.types.Aliases import DeviceID from discopop_library.discopop_optimizer.classes.types.DataAccessType import ( ReadDataAccess, @@ -20,13 +20,16 @@ class GenericNode(object): node_id: int # id of the node in the nx.DiGraph which stores this object # environment object to retrieve and store free symbols and other configurable values + ## Performance modelling + introduced_symbols: List[Symbol] + performance_model: CostModel ## Data transfer calculation written_memory_regions: Set[WriteDataAccess] read_memory_regions: Set[ReadDataAccess] device_id: DeviceID + execute_in_parallel: bool branch_affiliation: List[int] - experiment: Experiment def __init__( self, @@ -37,23 +40,31 @@ def __init__( read_memory_regions: Optional[Set[ReadDataAccess]] = None, device_id: DeviceID = None, ): - self.experiment = experiment self.node_id = node_id self.cu_id = cu_id # used to differentiate between "legacy" and suggestion nodes self.original_cu_id = cu_id # used for the creation of update suggestions + self.introduced_symbols = [] + self.performance_model = CostModel(Integer(0), Integer(0)) self.suggestion = None self.suggestion_type: Optional[str] = None self.branch_affiliation = [] + self.execute_in_parallel = False if written_memory_regions is None: self.written_memory_regions = set() else: self.written_memory_regions = written_memory_regions + # register free variables for memory region sizes + for memory_region in [wmr.memory_region for wmr in written_memory_regions]: + experiment.get_memory_region_size(memory_region, use_symbolic_value=True) if read_memory_regions is None: self.read_memory_regions = set() else: self.read_memory_regions = read_memory_regions + # register free variables for memory region sizes + for memory_region in [rmr.memory_region for rmr in read_memory_regions]: + experiment.get_memory_region_size(memory_region, use_symbolic_value=True) self.device_id = device_id @@ -63,10 +74,21 @@ def get_plot_label(self) -> str: def get_hover_text(self) -> str: return "" - def get_delta_workload(self) -> int: - """calculates the deviation in possible workloads represented by the current node (e.g. due to branching) - and returns the absolute difference value""" + def get_cost_model(self, experiment, all_function_nodes, current_device) -> CostModel: + raise NotImplementedError( + "Implementation needs to be provided by derived class: !", type(self) + ) + + def register_child(self, other, experiment, all_function_nodes, current_device): + """Registers a child node for the given model. + Does not modify the stored model in self or other.""" + raise NotImplementedError( + "Implementation needs to be provided by derived class: !", type(self) + ) + def register_successor(self, other, root_node): + """Registers a successor node for the given model. + Does not modify the stored model in self or other.""" raise NotImplementedError( - "This function needs to be implemented in every inheriting class!" + "Implementation needs to be provided by derived class: !", type(self) ) diff --git a/discopop_library/discopop_optimizer/classes/nodes/Loop.py b/discopop_library/discopop_optimizer/classes/nodes/Loop.py index c3cd5e17e..09126f960 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/Loop.py +++ b/discopop_library/discopop_optimizer/classes/nodes/Loop.py @@ -6,11 +6,12 @@ # the 3-Clause BSD License. See the LICENSE file in the package base # directory for details. import sys -from typing import Optional, cast +from typing import Optional, cast, Set -from sympy import Symbol, Integer # type: ignore +from sympy import Symbol, Integer, Expr, Float # type: ignore from discopop_explorer.PETGraphX import NodeID +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.Variables.Experiment import Experiment from discopop_library.discopop_optimizer.classes.nodes.Workload import Workload @@ -19,13 +20,14 @@ class Loop(Workload): iterations: int position: str iterations_symbol: Symbol + registered_child: Optional[CostModel] def __init__( self, node_id: int, experiment: Experiment, cu_id: Optional[NodeID], - parallelizable_workload: int, + discopop_workload: int, iterations: int, position: str, iterations_symbol: Optional[Symbol] = None, @@ -42,20 +44,47 @@ def __init__( else: self.iterations_symbol = iterations_symbol + # create parallelizable_workload_symbol + self.per_iteration_parallelizable_workload = Symbol( + "loop_" + + str(node_id) + + "_pos_" + + str(self.position) + + "_per_iteration_parallelizable_workload" + ) + self.per_iteration_sequential_workload = Symbol( + "loop_" + + str(node_id) + + "_pos_" + + str(self.position) + + "_per_iteration_sequential_workload" + ) + # calculate workload per iteration - per_iteration_parallelizable_workload = parallelizable_workload / iterations + per_iteration_parallelizable_workload = discopop_workload / iterations + super().__init__( node_id, experiment, cu_id, - sequential_workload=0, - parallelizable_workload=int(per_iteration_parallelizable_workload), + sequential_workload=self.per_iteration_sequential_workload, + parallelizable_workload=self.per_iteration_parallelizable_workload, + # int( + # # per_iteration_parallelizable_workload + # ), # todo this might be wrong! should be 1 instead, since children are registered individually ) # register iteration symbol in environment experiment.register_free_symbol( self.iterations_symbol, value_suggestion=Integer(self.iterations) ) + # register per iteration parallelizable workload symbol in environment + experiment.register_free_symbol( + self.per_iteration_parallelizable_workload, + value_suggestion=Float(per_iteration_parallelizable_workload), + ) + experiment.substitutions[self.per_iteration_sequential_workload] = Integer(0) + self.registered_child = None # todo: note: it might be more beneficial to use the iterations "per entry" instead of the total amount of iterations # example: @@ -68,3 +97,46 @@ def get_hover_text(self) -> str: "Read: " + str([str(e) for e in self.read_memory_regions]) + "\n" "Write: " + str([str(e) for e in self.written_memory_regions]) ) + + def get_cost_model(self, experiment, all_function_nodes, current_device) -> CostModel: + """Performance model of a workload consists of the workload itself. + Individual Workloads are assumed to be not parallelizable. + Workloads of Loop etc. are parallelizable.""" + + # loop costs = self.sequential + overhead + iterations * per_iteration_workload * cost_modifier + + cm = CostModel( + self.iterations_symbol + * self.per_iteration_parallelizable_workload + * self.cost_multiplier.parallelizable_costs, + self.sequential_workload + + self.overhead.sequential_costs + + self.per_iteration_sequential_workload * self.iterations_symbol, + ) + + # substitute Expr(0) with Integer(0) + cm.parallelizable_costs = cm.parallelizable_costs.subs({Expr(Integer(0)): Integer(0)}) + cm.sequential_costs = cm.sequential_costs.subs({Expr(Integer(0)): Integer(0)}) + + return cm + + def register_child(self, other, experiment, all_function_nodes, current_device): + """Registers a child node for the given model. + Does not modify the stored model in self or other.""" + + # at every time, only a single child is possible for each loop node + self.registered_child = other + experiment.substitutions[ + self.per_iteration_parallelizable_workload + ] = other.parallelizable_costs + experiment.substitutions[self.per_iteration_sequential_workload] = other.sequential_costs + + cm = self.get_cost_model(experiment, all_function_nodes, current_device) + cm.path_decisions += other.path_decisions + return cm + + def register_successor(self, other): + """Registers a successor node for the given model. + Does not modify the stored model in self or other.""" + # sequential composition is depicted by simply adding the performance models + return self.performance_model.parallelizable_plus_combine(other) diff --git a/discopop_library/discopop_optimizer/classes/nodes/Workload.py b/discopop_library/discopop_optimizer/classes/nodes/Workload.py index c8645be65..91cd003e5 100644 --- a/discopop_library/discopop_optimizer/classes/nodes/Workload.py +++ b/discopop_library/discopop_optimizer/classes/nodes/Workload.py @@ -5,14 +5,12 @@ # This software may be modified and distributed under the terms of # the 3-Clause BSD License. See the LICENSE file in the package base # directory for details. -import sys -from typing import Optional, Set, List, cast +from typing import Optional, Set, List, cast, Union from sympy import Integer, Expr # type: ignore from discopop_explorer.PETGraphX import NodeID, PETGraphX, EdgeType - -from discopop_library.discopop_optimizer.classes.edges.SuccessorEdge import SuccessorEdge +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel from discopop_library.discopop_optimizer.classes.nodes.GenericNode import GenericNode from discopop_library.discopop_optimizer.classes.types.DataAccessType import ( WriteDataAccess, @@ -23,16 +21,17 @@ class Workload(GenericNode): """This class represents a generic node in the Optimization Graph""" - sequential_workload: Optional[int] - parallelizable_workload: Optional[int] + sequential_workload: Optional[Expr] + parallelizable_workload: Optional[Expr] + cost_multiplier: CostModel def __init__( self, node_id: int, experiment, cu_id: Optional[NodeID], - sequential_workload: Optional[int], - parallelizable_workload: Optional[int], + sequential_workload: Optional[Expr], + parallelizable_workload: Optional[Expr], written_memory_regions: Optional[Set[WriteDataAccess]] = None, read_memory_regions: Optional[Set[ReadDataAccess]] = None, ): @@ -45,6 +44,12 @@ def __init__( ) self.sequential_workload = sequential_workload self.parallelizable_workload = parallelizable_workload + self.performance_model = CostModel( + Integer(0) if self.parallelizable_workload is None else self.parallelizable_workload, + Integer(0) if self.sequential_workload is None else self.sequential_workload, + ) + self.cost_multiplier = CostModel(Integer(1), Integer(1)) + self.overhead = CostModel(Integer(0), Integer(0)) def get_plot_label(self) -> str: if self.sequential_workload is not None: @@ -60,3 +65,88 @@ def get_hover_text(self) -> str: "Write: " + str([str(e) for e in self.written_memory_regions]) + "\n" "Branch: " + str(self.branch_affiliation) ) + + def get_cost_model(self, experiment, all_function_nodes, current_device) -> CostModel: + """Performance model of a workload consists of the workload itself + the workload of called functions. + Individual Workloads are assumed to be not parallelizable. + Workloads of called functions are added as encountered. + Workloads of Loop etc. are parallelizable.""" + + cm: CostModel + + if self.sequential_workload is None: + cm = ( + CostModel(Integer(1), Integer(0)) + .parallelizable_multiply_combine(self.cost_multiplier) + .parallelizable_plus_combine(self.overhead) + .parallelizable_plus_combine( + self.__get_costs_of_function_call( + experiment, all_function_nodes, current_device + ) + ) + ) + else: + cm = ( + CostModel( + current_device.get_estimated_execution_time_in_micro_seconds( + self.parallelizable_workload, True + ), + current_device.get_estimated_execution_time_in_micro_seconds( + self.sequential_workload, True + ), + ) + .parallelizable_multiply_combine(self.cost_multiplier) + .parallelizable_plus_combine(self.overhead) + .parallelizable_plus_combine( + self.__get_costs_of_function_call( + experiment, all_function_nodes, current_device + ) + ) + ) + + # substitute Expr(0) with Integer(0) + cm.parallelizable_costs = cm.parallelizable_costs.subs({Expr(Integer(0)): Integer(0)}) + cm.sequential_costs = cm.sequential_costs.subs({Expr(Integer(0)): Integer(0)}) + + return cm + + def __get_costs_of_function_call( + self, experiment, all_function_nodes, current_device + ) -> CostModel: + """Check if the node performs a function call and returns the total costs for these.""" + total_costs = CostModel(Integer(0), Integer(0)) + # get CUIDs of called functions + if self.original_cu_id is not None: + called_cu_ids: List[str] = [ + str(t) + for s, t, d in cast(PETGraphX, experiment.detection_result.pet).out_edges( + self.original_cu_id, EdgeType.CALLSNODE + ) + ] + # filter for called FunctionRoots + called_function_nodes = [ + fr for fr in all_function_nodes if str(fr.original_cu_id) in called_cu_ids + ] + # remove duplicates + called_function_nodes = list(set(called_function_nodes)) + # add costs of called function nodes to total costs + for called_function_root in called_function_nodes: + total_costs = total_costs.parallelizable_plus_combine( + called_function_root.get_cost_model( + experiment, all_function_nodes, current_device + ) + ) + + return total_costs + + def register_child(self, other, experiment, all_function_nodes, current_device): + """Registers a child node for the given model. + Does not modify the stored model in self or other.""" + # since workloads do not modify their children, the performance model of other is simply added to self. + return self.performance_model.parallelizable_plus_combine(other) + + def register_successor(self, other): + """Registers a successor node for the given model. + Does not modify the stored model in self or other.""" + # sequential composition is depicted by simply adding the performance models + return self.performance_model.parallelizable_plus_combine(other) diff --git a/discopop_library/discopop_optimizer/classes/system/Network.py b/discopop_library/discopop_optimizer/classes/system/Network.py new file mode 100644 index 000000000..829aac6fc --- /dev/null +++ b/discopop_library/discopop_optimizer/classes/system/Network.py @@ -0,0 +1,41 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import Dict, Tuple, List, Optional, cast + +from sympy import Expr, Symbol + +from discopop_library.discopop_optimizer.classes.system.devices.Device import Device + + +class Network(object): + __transfer_speeds: Dict[Tuple[Device, Device], Expr] # (MB/s) + __transfer_initialization_costs: Dict[Tuple[Device, Device], Expr] + + def __init__(self): + self.__transfer_speeds = dict() + self.__transfer_initialization_costs = dict() + + def add_connection( + self, source: Device, target: Device, transfer_speed: Expr, initialization_delay: Expr + ): + self.__transfer_speeds[(source, target)] = transfer_speed + self.__transfer_initialization_costs[(source, target)] = initialization_delay + + def get_transfer_speed(self, source: Device, target: Device): + return self.__transfer_speeds[(source, target)] + + def get_transfer_initialization_costs(self, source: Device, target: Device): + return self.__transfer_initialization_costs[(source, target)] + + def get_free_symbols(self) -> List[Tuple[Symbol, Optional[Expr]]]: + result_list: List[Tuple[Symbol, Optional[Expr]]] = [] + for expr in self.__transfer_speeds.values(): + result_list += [(cast(Symbol, s), None) for s in expr.free_symbols] + for expr in self.__transfer_initialization_costs.values(): + result_list += [(cast(Symbol, s), None) for s in expr.free_symbols] + return result_list diff --git a/discopop_library/discopop_optimizer/classes/system/System.py b/discopop_library/discopop_optimizer/classes/system/System.py new file mode 100644 index 000000000..25d015dda --- /dev/null +++ b/discopop_library/discopop_optimizer/classes/system/System.py @@ -0,0 +1,121 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import warnings +from typing import Dict, List, Tuple, Optional + +from sympy import Symbol, Expr, Integer, simplify + +from discopop_library.discopop_optimizer.classes.system.Network import Network +from discopop_library.discopop_optimizer.classes.system.devices.CPU import CPU +from discopop_library.discopop_optimizer.classes.system.devices.Device import Device +from discopop_library.discopop_optimizer.classes.system.devices.GPU import GPU + + +class System(object): + __devices: Dict[int, Device] + __network: Network + __next_free_device_id: int + __device_do_all_overhead_models: Dict[Device, Expr] + __device_reduction_overhead_models: Dict[Device, Expr] + + def __init__(self, headless: bool = False): + self.__devices = dict() + self.__network = Network() + self.__next_free_device_id = 0 + self.__device_do_all_overhead_models = dict() + self.__device_reduction_overhead_models = dict() + + # define a default system + # todo replace with benchmark results and / or make user definable + if headless: + device_0_threads = Integer(4) + else: + device_0_threads = Symbol("device_0_threads") # Integer(48) + device_0 = CPU( + Integer(3000000000), + device_0_threads, + openmp_device_id=-1, + device_specific_compiler_flags="COMPILE FOR CPU", + ) # Device 0 always acts as the host system + gpu_compiler_flags = "COMPILE FOR CPU" + device_1 = GPU( + Integer(512000000), + Integer(512), + openmp_device_id=0, + device_specific_compiler_flags="COMPILE FOR GPU", + ) + device_2 = GPU( + Integer(512000000), + Integer(512), + openmp_device_id=1, + device_specific_compiler_flags="COMPILE FOR GPU", + ) + self.add_device(device_0) + self.add_device(device_1) + self.add_device(device_2) + # define Network + network = self.get_network() + network.add_connection(device_0, device_0, Integer(100000), Integer(0)) + network.add_connection(device_0, device_1, Integer(100), Integer(1000000)) + network.add_connection(device_1, device_0, Integer(100), Integer(1000000)) + network.add_connection(device_1, device_1, Integer(100000), Integer(0)) + + network.add_connection(device_0, device_2, Integer(100), Integer(10000000)) + network.add_connection(device_2, device_0, Integer(100), Integer(10000000)) + network.add_connection(device_2, device_2, Integer(1000), Integer(0)) + + network.add_connection(device_1, device_2, Integer(100), Integer(500000)) + network.add_connection(device_2, device_1, Integer(100), Integer(500000)) + + # todo: support the replication of device ids (e.g. CPU-0 and GPU-0) + + def set_device_doall_overhead_model(self, device: Device, model: Expr): + print("System: Set DOALL overhead model: ", model) + self.__device_do_all_overhead_models[device] = model + + def set_reduction_overhead_model(self, device: Device, model: Expr): + print("System: Set REDUCTION overhead model: ", model) + self.__device_reduction_overhead_models[device] = model + + def get_device_doall_overhead_model(self, device: Device) -> Expr: + if device not in self.__device_do_all_overhead_models: + warnings.warn("No DOALL overhead model, assuming 0 for device: " + str(device)) + return Expr(Integer(0)) + return self.__device_do_all_overhead_models[device] + + def get_device_reduction_overhead_model(self, device: Device) -> Expr: + if device not in self.__device_reduction_overhead_models: + warnings.warn("No REDUCTION overhead model, assuming 0 for device: " + str(device)) + return Expr(Integer(0)) + return self.__device_reduction_overhead_models[device] + + def add_device(self, device: Device): + device_id = self.__next_free_device_id + self.__next_free_device_id += 1 + self.__devices[device_id] = device + + def get_device(self, device_id: Optional[int]) -> Device: + if device_id is None: + return self.__devices[0] + return self.__devices[device_id] + + def get_network(self) -> Network: + return self.__network + + def get_free_symbols(self) -> List[Tuple[Symbol, Optional[Expr]]]: + result_list: List[Tuple[Symbol, Optional[Expr]]] = [] + for device in self.__devices.values(): + result_list += device.get_free_symbols() + result_list += self.__network.get_free_symbols() + return result_list + + def get_device_id(self, device: Device) -> int: + for key in self.__devices: + if device == self.__devices[key]: + return key + raise ValueError("Unknown device: ", device) diff --git a/discopop_library/discopop_optimizer/classes/system/__init__.py b/discopop_library/discopop_optimizer/classes/system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/classes/system/devices/CPU.py b/discopop_library/discopop_optimizer/classes/system/devices/CPU.py new file mode 100644 index 000000000..981871f04 --- /dev/null +++ b/discopop_library/discopop_optimizer/classes/system/devices/CPU.py @@ -0,0 +1,19 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from discopop_library.discopop_optimizer.classes.system.devices.Device import Device + + +class CPU(Device): + def __init__( + self, + frequency, + thread_count, + openmp_device_id: int, + device_specific_compiler_flags: str, + ): + super().__init__(frequency, thread_count, openmp_device_id, device_specific_compiler_flags) diff --git a/discopop_library/discopop_optimizer/classes/system/devices/Device.py b/discopop_library/discopop_optimizer/classes/system/devices/Device.py new file mode 100644 index 000000000..aacf98173 --- /dev/null +++ b/discopop_library/discopop_optimizer/classes/system/devices/Device.py @@ -0,0 +1,83 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import Tuple, List, Optional, cast + +from sympy import Expr, Symbol + +from discopop_explorer.pattern_detectors.PatternInfo import PatternInfo +from sympy import Integer + + +class Device(object): + __frequency: Expr + __thread_count: Expr + openmp_device_id: int + + def __init__( + self, + frequency: Expr, # GHz + thread_count: Expr, + openmp_device_id: int, + device_specific_compiler_flags: str, + ): + self.__frequency = frequency + self.__thread_count = thread_count + self.openmp_device_id = openmp_device_id + self.device_specific_compiler_flags: str = device_specific_compiler_flags + + def get_device_specific_pattern_info( + self, suggestion: PatternInfo, suggestion_type: str + ) -> Tuple[PatternInfo, str]: + return suggestion, suggestion_type + + def get_compute_capability(self) -> Expr: + return self.__frequency + + def get_thread_count(self) -> Expr: + return self.__thread_count + + def get_free_symbols(self) -> List[Tuple[Symbol, Optional[Expr]]]: + result_list: List[Tuple[Symbol, Optional[Expr]]] = [] + result_list += [(cast(Symbol, s), None) for s in self.__frequency.free_symbols] + result_list += [(cast(Symbol, s), None) for s in self.__thread_count.free_symbols] + return result_list + + def get_estimated_execution_time_in_micro_seconds(self, workload: Expr, is_sequential: bool): + """execution time is estimated by: + - convert workload to estimated amount of CPU instructions using a extra-p model + - NOTE: use "perf stat ./ define functions to evaluate workloads on different devices. include these functions in the construction of the cost model for later evaluation instead of "manually" converting the workload to time values + # todo: correctly set is_sequential argument + # todo mark parallel execution in subtree of suggestions? + + avg_instructions_per_cycle = 0.89 # todo (get from benchmarking) + # current value determined by manual "measurement" based on a single example! + average_cycles_per_instruction = 1 / avg_instructions_per_cycle + instructions_per_core_per_second = self.__frequency / average_cycles_per_instruction + instructions_per_second = instructions_per_core_per_second * ( + Integer(1) if is_sequential else self.__thread_count + ) + workload_in_instructions = ( + workload * 2.120152292 + ) # todo (get from benchmarking / extra-p model) + # current factor determined by manual "measurement" based on a single example! + + execution_time_in_seconds = workload_in_instructions / instructions_per_second + execution_time_in_micro_seconds = execution_time_in_seconds * 1000000 + return execution_time_in_micro_seconds diff --git a/discopop_library/discopop_optimizer/classes/system/devices/GPU.py b/discopop_library/discopop_optimizer/classes/system/devices/GPU.py new file mode 100644 index 000000000..5a8918d10 --- /dev/null +++ b/discopop_library/discopop_optimizer/classes/system/devices/GPU.py @@ -0,0 +1,21 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import Tuple + +from discopop_library.discopop_optimizer.classes.system.devices.Device import Device +from discopop_explorer.pattern_detectors.PatternInfo import PatternInfo + + +class GPU(Device): + def __init__(self, frequency, thread_count, openmp_device_id, device_specific_compiler_flags): + super().__init__(frequency, thread_count, openmp_device_id, device_specific_compiler_flags) + + def get_device_specific_pattern_info( + self, suggestion: PatternInfo, suggestion_type: str + ) -> Tuple[PatternInfo, str]: + return suggestion, "gpu_" + suggestion_type diff --git a/discopop_library/discopop_optimizer/classes/system/devices/__init__.py b/discopop_library/discopop_optimizer/classes/system/devices/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/execution/__init__.py b/discopop_library/discopop_optimizer/execution/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/execution/stored_models.py b/discopop_library/discopop_optimizer/execution/stored_models.py new file mode 100644 index 000000000..fb62ab5d1 --- /dev/null +++ b/discopop_library/discopop_optimizer/execution/stored_models.py @@ -0,0 +1,281 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import os +import random +import shutil +import statistics +import string +import subprocess +import time +import warnings +from pathlib import Path +from typing import Dict, cast, List, TextIO, Tuple + +import jsonpickle # type: ignore + +from discopop_library.PathManagement.PathManagement import load_file_mapping +from discopop_library.discopop_optimizer.bindings.CodeStorageObject import CodeStorageObject + + +def execute_stored_models(arguments: Dict): + """Collects and executes all models stored in the current project path""" + print("Cleaning environment...") + __initialize_measurement_directory(arguments) + print("Executing stored models...") + + # collect models to be executed + working_copy_dir = os.path.join(arguments["--project"], ".discopop_optimizer_code_copy") + for file_name in os.listdir(str(arguments["--code-export-path"])): + print("\t", file_name) + __create_project_copy(arguments["--project"], working_copy_dir) + code_modifications = __load_code_storage_object( + os.path.join(str(arguments["--code-export-path"]), file_name) + ) + __apply_modifications( + arguments["--project"], + working_copy_dir, + code_modifications, + load_file_mapping(arguments["--file-mapping"]), + ) + __compile(arguments, working_copy_dir, arguments["--compile-command"]) + __measure_and_execute(arguments, working_copy_dir, code_modifications) + # __cleanup(working_copy_dir) + + +def execute_single_model(arguments: Dict): + """Executes the single models specified by the given arguments""" + print("Cleaning environment...") + __initialize_measurement_directory(arguments) + + print("Executing stored model...") + + # collect model to be executed + working_copy_dir = os.path.join(arguments["--project"], ".discopop_optimizer_code_copy") + file_name = arguments["--execute-single-model"] + print("\t", file_name) + __create_project_copy(arguments["--project"], working_copy_dir) + code_modifications = __load_code_storage_object( + os.path.join(str(arguments["--code-export-path"]), file_name) + ) + __apply_modifications( + arguments["--project"], + working_copy_dir, + code_modifications, + load_file_mapping(arguments["--file-mapping"]), + ) + __compile(arguments, working_copy_dir, arguments["--compile-command"]) + __measure_and_execute(arguments, working_copy_dir, code_modifications) + # __cleanup(working_copy_dir) + + +def __initialize_measurement_directory(arguments: Dict): + measurement_dir = os.path.join(arguments["--project"], ".discopop_optimizer_measurements") + if not arguments["--execution-append-measurements"]: + # delete measurement directory + if os.path.exists(measurement_dir): + shutil.rmtree(measurement_dir) + if not os.path.exists(measurement_dir): + os.makedirs(measurement_dir) + __initialize_measurement_file(os.path.join(measurement_dir, "measurements.csv")) + + +def __initialize_measurement_file(measurement_file: str): + if not os.path.exists(measurement_file): + with open(measurement_file, "w+") as f: + # write file header + header_line = "Test_case_id;Model_ID;Model_Label;return_code;Executable_name;Executable_arguments;execution_time;Function_name;\n" + f.write(header_line) + + +def __measure_and_execute( + arguments: Dict, working_copy_dir: str, code_mod_object: CodeStorageObject +): + """Setup measurements, execute the compiled program and output the measurement results to a file""" + measurement_dir = os.path.join(arguments["--project"], ".discopop_optimizer_measurements") + # create output file for specific model measurement + measurement_file = os.path.join(measurement_dir, "measurements.csv") + + with open(measurement_file, "a") as f: + execution_times: List[float] = [] + test_case_id = "".join(random.choices(string.ascii_uppercase + string.digits, k=8)) + for execution_idx in range(0, int(arguments["--execution-repetitions"])): + return_code, start_time, end_time = __execute(arguments, working_copy_dir, f) + execution_time = end_time - start_time + execution_times.append(execution_time) + # write execution result + execution_line = ( + test_case_id + + ";" + + code_mod_object.model_id + + ";" + + code_mod_object.label + + ";" + + str(return_code) + + ";" + + arguments["--executable-name"] + + ";" + + arguments["--executable-arguments"] + + ";" + + str(execution_time).replace(".", ",") + + ";" + + code_mod_object.parent_function.name + + "\n" + ) + f.write(execution_line) + f.flush() + + print("\t\t\tREPS: ", len(execution_times)) + print("\t\t\tAVG: ", sum(execution_times) / len(execution_times)) + if len(execution_times) >= 2: + print("\t\t\tVariance: ", statistics.variance(execution_times)) + + +def __execute( + arguments: Dict, working_copy_dir: str, measurements_file: TextIO +) -> Tuple[int, float, float]: + """Executes the current model and returns the exit code as well as the start and end time of the execution""" + print("\t\texecuting...") + command = ["./" + arguments["--executable-name"], arguments["--executable-arguments"]] + clean_command = [c for c in command if len(c) != 0] + start_time = time.time() + try: + result = subprocess.run( + clean_command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + cwd=working_copy_dir, + ) + end_time = time.time() + if str(result.returncode) != "0": + warnings.warn("ERROR DURING EXECUTION...\n" + result.stderr) + print("STDOUT: ") + print(result.stdout) + print("STDERR: ") + print(result.stderr) + return result.returncode, start_time, end_time + except FileNotFoundError as fnfe: + end_time = time.time() + print(fnfe) + return 1, start_time, end_time + + +def __compile(arguments: Dict, working_copy_dir, compile_command): + print("\t\tbuilding...") + # command = compile_command + # command = shlex.split(compile_command) + if len(arguments["--make-flags"]) != 0: + print("MAKE FLAGS: ", arguments["--make-flags"]) + compile_command += ( + " " + arguments["--make-flags"] + ) # shlex.split(arguments["--make-flags"]) # split string, consider quotes + + if len(arguments["--make-target"]) != 0: + compile_command += ( + " " + arguments["--make-target"] + ) # shlex.split(arguments["--make-target"]) # split string, consider quotes + # clean_command = [c for c in command if len(c) != 0] + print("\t\t\tCommand: ", compile_command) # shlex.join(clean_command)) + result = subprocess.run( + compile_command, + # " ".join(clean_command), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + cwd=working_copy_dir, + shell=True, + ) + print("STDOUT: ") + print(result.stdout) + print("STDERR: ") + print(result.stderr) + if str(result.returncode) != "0": + warnings.warn("ERROR / WARNING DURING Compilation...\n" + result.stderr) + + +def __apply_modifications( + project_folder, + working_copy_dir, + modifications: CodeStorageObject, + file_mapping: Dict[int, Path], +): + print("\t\tApplying code modifications...") + print("\t\t\tFunction: ", modifications.parent_function.name) + for file_id in modifications.patches: + file_mapping_path = str(file_mapping[int(file_id)]) + # remove /.discopop/ from path if it occurs + if "/.discopop/" in file_mapping_path: + file_mapping_path = file_mapping_path.replace("/.discopop/", "/") + # get file to be overwritten + replace_path = file_mapping_path.replace(project_folder, working_copy_dir) + # apply patch to the file + if not os.path.exists(replace_path): + raise FileNotFoundError(replace_path) + # save patch to disk + patch = replace_path + ".patch" + with open(patch, "w+") as p: + p.write(modifications.patches[file_id]) + p.flush() + p.close() + + # check existence of files: + if os.path.exists(patch): + print("PATCH FILE EXISTS") + if os.path.exists(replace_path): + print("REPLACE PATH EXISTS") + + # command = ["patch", "-i", patch, "-o", replace_path] + command = ["patch", replace_path, patch] + print("Patch command: ", command) + + result = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + cwd=os.getcwd(), + ) + print("RESULT: ", result.returncode) + print("STDERR:") + print(result.stderr) + print("STDOUT: ") + print(result.stdout) + + if result.returncode == 0: + print("Applied Patch:") + print(modifications.patches[file_id]) + + +# remove temporary patch file +# todo re-enable cleanup + + +# if os.path.exists(patch): +# os.remove(patch) + + +def __load_code_storage_object(file_path) -> CodeStorageObject: + json_contents = "" + with open(file_path, "r") as f: + json_contents = f.read() + return cast(CodeStorageObject, jsonpickle.decode(json_contents)) + + +def __create_project_copy(source, target): + print("\t\tCreating a working copy of the project...", end="") + if os.path.exists(target): + shutil.rmtree(target) + shutil.copytree(str(source), target, ignore=shutil.ignore_patterns(".discopop*")) + print("Done") + + +def __cleanup(working_copy_dir: str): + # remove working copy to free space + print("\t\tRemoving working copy of the project...", end="") + shutil.rmtree(working_copy_dir) + print("Done.") diff --git a/discopop_library/discopop_optimizer/gui/__init__.py b/discopop_library/discopop_optimizer/gui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/gui/plotting/CostModels.py b/discopop_library/discopop_optimizer/gui/plotting/CostModels.py new file mode 100644 index 000000000..abe756759 --- /dev/null +++ b/discopop_library/discopop_optimizer/gui/plotting/CostModels.py @@ -0,0 +1,426 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import copy +from typing import List, Dict, Tuple, Optional, cast + +import numpy as np +from matplotlib import pyplot as plt # type: ignore +import matplotlib +from spb import plot3d, MB, plot # type: ignore +from sympy import Symbol, Expr +import sympy + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.utilities.MOGUtilities import data_at, show +from sympy import Integer + + +def plot_CostModels( + experiment, + models: List[CostModel], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + labels: Optional[List[str]] = None, + title: Optional[str] = None, + super_title: Optional[str] = None, +): + local_sorted_free_symbols = copy.deepcopy(sorted_free_symbols) + local_free_symbol_ranges = copy.deepcopy(free_symbol_ranges) + for symbol in experiment.substitutions: + if symbol in experiment.free_symbols: + experiment.free_symbols.remove(symbol) + if symbol in local_free_symbol_ranges: + del local_free_symbol_ranges[symbol] + if symbol in local_sorted_free_symbols: + local_sorted_free_symbols.remove(symbol) + + # apply selected substitutions + # collect substitutions + local_substitutions = copy.deepcopy(experiment.substitutions) + for function in experiment.selected_paths_per_function: + # register substitution + local_substitutions[ + cast(Symbol, function.sequential_costs) + ] = experiment.selected_paths_per_function[function][0].sequential_costs + local_substitutions[ + cast(Symbol, function.parallelizable_costs) + ] = experiment.selected_paths_per_function[function][0].parallelizable_costs + + local_models = copy.deepcopy(models) + + # perform iterative substitutions + modification_found = True + while modification_found: + modification_found = False + for model in local_models: + # apply substitution to parallelizable costs + tmp_model = model.parallelizable_costs.subs(local_substitutions) + if tmp_model != model.parallelizable_costs: + modification_found = True + model.parallelizable_costs = tmp_model + + # apply substitutions to sequential costs + tmp_model = model.sequential_costs.subs(local_substitutions) + if tmp_model != model.sequential_costs: + modification_found = True + model.sequential_costs = model.sequential_costs.subs(local_substitutions) + + # replace Expr(0) with 0 + for model in local_models: + model.sequential_costs = model.sequential_costs.subs({Expr(Integer(0)): Integer(0)}) + model.parallelizable_costs = model.parallelizable_costs.subs({Expr(Integer(0)): Integer(0)}) + + if len(local_sorted_free_symbols) == 2: + __3d_plot( + local_models, + local_sorted_free_symbols, + local_free_symbol_ranges, + labels=labels, + title=str(title) + str(super_title) if super_title is not None else title, + ) + elif len(local_sorted_free_symbols) == 1: + __2d_plot( + local_models, + local_sorted_free_symbols, + local_free_symbol_ranges, + labels=labels, + title=str(title) + str(super_title) if super_title is not None else title, + ) + elif len(local_sorted_free_symbols) == 0: + __1d_plot( + local_models, + local_sorted_free_symbols, + local_free_symbol_ranges, + labels=labels, + title=title, + super_title=super_title, + ) + else: + print("Plotiting not supported for", len(sorted_free_symbols), "free symbols!") + + +def plot_CostModels_using_function_path_selections( + experiment: Experiment, + models: List[CostModel], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + labels: Optional[List[str]] = None, + title: Optional[str] = None, + super_title: Optional[str] = None, +): + # apply selected substitutions + # collect substitutions + local_substitutions = copy.deepcopy(experiment.substitutions) + for function in experiment.selected_paths_per_function: + # register substitution + local_substitutions[ + cast(Symbol, function.sequential_costs) + ] = experiment.selected_paths_per_function[function][0].sequential_costs + local_substitutions[ + cast(Symbol, function.parallelizable_costs) + ] = experiment.selected_paths_per_function[function][0].parallelizable_costs + + local_models = copy.deepcopy(models) + + # perform iterative substitutions + modification_found = True + while modification_found: + modification_found = False + for model in local_models: + # apply substitution to parallelizable costs + tmp_model = model.parallelizable_costs.subs(local_substitutions) + if tmp_model != model.parallelizable_costs: + modification_found = True + model.parallelizable_costs = tmp_model + + # apply substitutions to sequential costs + tmp_model = model.sequential_costs.subs(local_substitutions) + if tmp_model != model.sequential_costs: + modification_found = True + model.sequential_costs = model.sequential_costs.subs(local_substitutions) + + # replace Expr(0) with 0 + for model in local_models: + model.sequential_costs = model.sequential_costs.subs({Expr(Integer(0)): Integer(0)}) + model.parallelizable_costs = model.parallelizable_costs.subs({Expr(Integer(0)): Integer(0)}) + + local_sorted_free_symbols = copy.deepcopy(sorted_free_symbols) + local_free_symbol_ranges = copy.deepcopy(free_symbol_ranges) + for symbol in experiment.substitutions: + if symbol in experiment.free_symbols: + experiment.free_symbols.remove(symbol) + if symbol in local_free_symbol_ranges: + del local_free_symbol_ranges[symbol] + if symbol in local_sorted_free_symbols: + local_sorted_free_symbols.remove(symbol) + + if len(local_sorted_free_symbols) == 2: + __3d_plot( + local_models, + local_sorted_free_symbols, + local_free_symbol_ranges, + labels=labels, + title=str(title) + str(super_title) if super_title is not None else title, + ) + elif len(local_sorted_free_symbols) == 1: + __2d_plot( + local_models, + local_sorted_free_symbols, + local_free_symbol_ranges, + labels=labels, + title=str(title) + str(super_title) if super_title is not None else title, + ) + elif len(local_sorted_free_symbols) == 0: + __1d_plot( + local_models, + local_sorted_free_symbols, + local_free_symbol_ranges, + labels=labels, + title=title, + super_title=super_title, + ) + else: + print("Plotiting not supported for", len(local_sorted_free_symbols), "free symbols!") + + +def print_current_function_path_selections(experiment: Experiment): + """Prints an overview of the currently selected paths for each function to the console""" + print("###") + print("SELECTIONS:") + for function in experiment.selected_paths_per_function: + print("\t", function.name) + print("\t\t", experiment.selected_paths_per_function[function][0].path_decisions) + print("###") + + +def print_current_substitutions(experiment: Experiment): + """Prints an overview of the currently selected paths for each function to the console""" + print("###") + print("SUBSTITUTIONS:") + for var in experiment.substitutions: + print("->", var) + print("\t", experiment.substitutions[var]) + print("###") + + +def print_simplified_function( + experiment: Experiment, + models: List[CostModel], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + labels: Optional[List[str]] = None, + title: Optional[str] = None, + super_title: Optional[str] = None, +): + """Prints an simplified mathematical function based on the current set of selections""" + + # todo: NOTE: copied from plot_CostModels_using_function_path_selections + + # apply selected substitutions + # collect substitutions + local_substitutions = copy.deepcopy(experiment.substitutions) + for function in experiment.selected_paths_per_function: + # register substitution + local_substitutions[ + cast(Symbol, function.sequential_costs) + ] = experiment.selected_paths_per_function[function][0].sequential_costs + local_substitutions[ + cast(Symbol, function.parallelizable_costs) + ] = experiment.selected_paths_per_function[function][0].parallelizable_costs + + local_models = copy.deepcopy(models) + + # perform iterative substitutions + modification_found = True + while modification_found: + modification_found = False + for model in local_models: + # apply substitution to parallelizable costs + tmp_model = model.parallelizable_costs.subs(local_substitutions) + if tmp_model != model.parallelizable_costs: + modification_found = True + model.parallelizable_costs = tmp_model + + # apply substitutions to sequential costs + tmp_model = model.sequential_costs.subs(local_substitutions) + if tmp_model != model.sequential_costs: + modification_found = True + model.sequential_costs = model.sequential_costs.subs(local_substitutions) + + # replace Expr(0) with 0 + for model in local_models: + model.sequential_costs = model.sequential_costs.subs({Expr(Integer(0)): Integer(0)}) + model.parallelizable_costs = model.parallelizable_costs.subs({Expr(Integer(0)): Integer(0)}) + + local_sorted_free_symbols = copy.deepcopy(sorted_free_symbols) + local_free_symbol_ranges = copy.deepcopy(free_symbol_ranges) + for symbol in experiment.substitutions: + if symbol in experiment.free_symbols: + experiment.free_symbols.remove(symbol) + if symbol in local_free_symbol_ranges: + del local_free_symbol_ranges[symbol] + if symbol in local_sorted_free_symbols: + local_sorted_free_symbols.remove(symbol) + + print("###") + print("FUNCTION:") + for model in local_models: + model_costs = sympy.simplify( + sympy.re(model.parallelizable_costs + model.sequential_costs) + + sympy.im(model.parallelizable_costs + model.sequential_costs) + ) + print("-> ", model_costs) + print("###") + + +__unique_plot_id = 0 + + +def __1d_plot( + models: List[CostModel], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + labels: Optional[List[str]] = None, + title: Optional[str] = None, + super_title: Optional[str] = None, +): + global __unique_plot_id + matplotlib.use("TkAgg") + # Make a dataset from models: + height: List[float] = [] + bars: List[str] = [] + + for idx, model in enumerate(models): + model_label = str(model.path_decisions) if labels is None else labels[idx] + bars.append(model_label) + + # get numeric value from model + num_value = float( + sympy.re(model.sequential_costs.evalf() + model.parallelizable_costs.evalf()) + + sympy.im(model.sequential_costs.evalf() + model.parallelizable_costs.evalf()) + ) + height.append(num_value) + + bars_tuple = tuple(bars) + y_pos = np.arange(len(bars_tuple)) + + # Create bars + plt.figure(__unique_plot_id) + __unique_plot_id += 1 + if title is not None: + plt.title(title) # type: ignore + if super_title is not None: + plt.suptitle(super_title) # type: ignore + plt.bar(y_pos, height) # type: ignore + # Create names on the x-axis + plt.xticks(y_pos, bars_tuple) + # Show graphic + plt.show() + + +def __2d_plot( + models: List[CostModel], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + labels: Optional[List[str]] = None, + title: Optional[str] = None, +): + matplotlib.use("TkAgg") + combined_plot = None + for idx, model in enumerate(models): + model_label = str(model.path_decisions) if labels is None else labels[idx] + model_costs = sympy.re(model.parallelizable_costs + model.sequential_costs) + sympy.im( + model.parallelizable_costs + model.sequential_costs + ) + if combined_plot is None: + combined_plot = plot( + model_costs, + ( + sorted_free_symbols[0], + free_symbol_ranges[sorted_free_symbols[0]][0], + free_symbol_ranges[sorted_free_symbols[0]][1], + ), + show=False, + backend=MB, + label=model_label, + ylabel="Execution time [us]", + title=title, + ) + else: + combined_plot.extend( + plot( + model_costs, + ( + sorted_free_symbols[0], + free_symbol_ranges[sorted_free_symbols[0]][0], + free_symbol_ranges[sorted_free_symbols[0]][1], + ), + show=False, + backend=MB, + label=model_label, + ylabel="Execution time [us]", + ) + ) + combined_plot.show() # type: ignore + + +def __3d_plot( + models: List[CostModel], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + labels: Optional[List[str]] = None, + title: Optional[str] = None, +): + matplotlib.use("TkAgg") + combined_plot = None + for idx, model in enumerate(models): + model_label = str(model.path_decisions) if labels is None else labels[idx] + model_costs = sympy.re(model.parallelizable_costs + model.sequential_costs) + sympy.im( + model.parallelizable_costs + model.sequential_costs + ) + if combined_plot is None: + combined_plot = plot3d( + model_costs, + ( + sorted_free_symbols[0], + free_symbol_ranges[sorted_free_symbols[0]][0], + free_symbol_ranges[sorted_free_symbols[0]][1], + ), + ( + sorted_free_symbols[1], + free_symbol_ranges[sorted_free_symbols[1]][0], + free_symbol_ranges[sorted_free_symbols[1]][1], + ), + show=False, + backend=MB, + label=model_label, + zlabel="Execution time [us]", + title=title, + ) + else: + combined_plot.extend( + plot3d( + model_costs, + ( + sorted_free_symbols[0], + free_symbol_ranges[sorted_free_symbols[0]][0], + free_symbol_ranges[sorted_free_symbols[0]][1], + ), + ( + sorted_free_symbols[1], + free_symbol_ranges[sorted_free_symbols[1]][0], + free_symbol_ranges[sorted_free_symbols[1]][1], + ), + show=False, + backend=MB, + label=model_label, + zlabel="Execution time [us]", + ) + ) + combined_plot.show() # type: ignore diff --git a/discopop_library/discopop_optimizer/gui/plotting/__init__.py b/discopop_library/discopop_optimizer/gui/plotting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/gui/presentation/ChoiceDetails.py b/discopop_library/discopop_optimizer/gui/presentation/ChoiceDetails.py new file mode 100644 index 000000000..f7f0dd54c --- /dev/null +++ b/discopop_library/discopop_optimizer/gui/presentation/ChoiceDetails.py @@ -0,0 +1,91 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +from typing import Optional + +import networkx as nx # type: ignore + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel + +from tkinter import * + +from discopop_library.discopop_optimizer.gui.widgets.ScrollableFrame import ScrollableFrameWidget +from discopop_library.discopop_optimizer.utilities.MOGUtilities import data_at +from discopop_wizard.screens.widgets.ScrollableText import ScrollableTextWidget + + +def display_choices_for_model( + graph: nx.DiGraph, model: CostModel, window_title: Optional[str] = None +): + root = Tk() + if window_title is not None: + root.configure() + root.title(window_title) + # configure window size + root.geometry("1000x600") + # configure weights + root.rowconfigure(0, weight=1) + root.columnconfigure(0, weight=1) + + # create scrollable frame + scrollable_frame_widget = ScrollableFrameWidget(root) + scrollable_frame = scrollable_frame_widget.get_scrollable_frame() + # configure weights + scrollable_frame.rowconfigure(0, weight=1) + scrollable_frame.rowconfigure(1, weight=1) + scrollable_frame.rowconfigure(2, weight=1) + scrollable_frame.columnconfigure(0, weight=1) + scrollable_frame.columnconfigure(1, weight=1) + scrollable_frame.columnconfigure(2, weight=1) + + rows = [] + # print mathematical function for model + fn_label = Entry(scrollable_frame, relief=RIDGE, width=len("Function:")) + fn_label.grid(row=0, column=0, sticky=NSEW) + fn_label.insert(END, "Function:") + fn_label.configure(state=DISABLED, disabledforeground="black") + fn_function_frame = Frame(scrollable_frame) + fn_function_frame.grid(row=0, column=1, columnspan=2, sticky=NSEW) + fn_function = ScrollableTextWidget(fn_function_frame) + fn_function.set_text(str(model.parallelizable_costs + model.sequential_costs)) + fn_function.text_container.config(height=3) + + column_headers = ["Decision", "Device", "Details"] + # set column headers + header_cols = [] + for col_idx, header in enumerate(column_headers): + e = Entry(scrollable_frame, relief=RIDGE, width=len(header)) + e.grid(row=1, column=col_idx, sticky=NSEW) + e.insert(END, header) + e.configure(state=DISABLED, disabledforeground="black") + header_cols.append(e) + rows.append(header_cols) + + for row_idx, decision in enumerate(model.path_decisions): + row_idx = row_idx + 2 # account for mathematical function and header row + # add decision id + e = Entry(scrollable_frame, relief=RIDGE) + e.grid(row=row_idx, column=0, sticky=NSEW) + e.insert(END, str(decision)) + e.configure(state=DISABLED, disabledforeground="black") + + # add device id + i = Entry(scrollable_frame, relief=RIDGE) + i.grid(row=row_idx, column=1, sticky=NSEW) + i.insert(END, str(data_at(graph, decision).device_id)) + i.configure(state=DISABLED, disabledforeground="black") + + # add decision details + d = Frame(scrollable_frame) + d.grid(row=row_idx, column=2, sticky=NSEW) + stw = ScrollableTextWidget(d) + stw.set_text(str(data_at(graph, decision).suggestion)) + stw.text_container.config(height=10) + + scrollable_frame_widget.finalize(len(model.path_decisions)) + + pass diff --git a/discopop_library/discopop_optimizer/gui/presentation/OptionTable.py b/discopop_library/discopop_optimizer/gui/presentation/OptionTable.py new file mode 100644 index 000000000..a3838c1c8 --- /dev/null +++ b/discopop_library/discopop_optimizer/gui/presentation/OptionTable.py @@ -0,0 +1,333 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import tkinter +from tkinter import * +from typing import List, Tuple, Dict, Union, Optional, cast + +import networkx as nx # type: ignore +from sympy import Symbol, Expr + +from discopop_explorer.PETGraphX import PETGraphX +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.bindings.CodeGenerator import export_code +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.enums.Distributions import FreeSymbolDistribution +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.gui.plotting.CostModels import ( + plot_CostModels, + plot_CostModels_using_function_path_selections, + print_current_function_path_selections, + print_current_substitutions, + print_simplified_function, +) +from discopop_library.discopop_optimizer.gui.presentation.ChoiceDetails import ( + display_choices_for_model, +) +from discopop_library.discopop_optimizer.utilities.optimization.GlobalOptimization.RandomSamples import ( + find_quasi_optimal_using_random_samples, +) + + +def show_options( + pet: PETGraphX, + graph: nx.DiGraph, + experiment: Experiment, + options: List[Tuple[CostModel, ContextObject, str]], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + free_symbol_distributions: Dict[Symbol, FreeSymbolDistribution], + function_root: FunctionRoot, + parent_frame: tkinter.Frame, + spawned_windows: List[tkinter.Toplevel], + window_title=None, +) -> List[Tuple[CostModel, ContextObject, str]]: + """Shows a tkinter table to browse and plot models""" + # root = tkinter.Toplevel() + # if window_title is not None: + # root.configure() + # root.title(window_title) + root = tkinter.Toplevel(parent_frame.winfo_toplevel()) + spawned_windows.append(root) + root.configure() + root.title(window_title) + + rows = [] + column_headers = ["Label", "Decisions", "Options"] + # set column headers + header_cols = [] + for col_idx, header in enumerate(column_headers): + e = Entry(root, relief=RIDGE) + e.grid(row=0, column=col_idx, sticky=NSEW) + e.insert(END, header) + e.configure(state=DISABLED, disabledforeground="black") + header_cols.append(e) + rows.append(header_cols) + + label1 = Entry(root, relief=RIDGE) + label1.grid(row=1, column=0, sticky=NSEW) + label1.insert(END, "Current selection:") + label1.configure(state=DISABLED, disabledforeground="black") + + label2 = Entry(root, relief=RIDGE) + label2.grid(row=1, column=1, sticky=NSEW) + label2.insert(END, str(experiment.selected_paths_per_function[function_root][0].path_decisions)) + label2.configure(state=DISABLED, disabledforeground="black") + + plot_button = Button( + root, + text="Plot using selections", + command=lambda: plot_CostModels_using_function_path_selections( # type: ignore + experiment, + [experiment.selected_paths_per_function[function_root][0]], # type: ignore + sorted_free_symbols, + free_symbol_ranges, + [function_root.name], + title=function_root.name, + super_title=function_root.name, + ), + ) + plot_button.grid(row=1, column=2, sticky=NSEW) + + print_selections_button = Button( + root, + text="Print selections", + command=lambda: print_current_function_path_selections( # type: ignore + experiment, + ), + ) + print_selections_button.grid(row=1, column=3, sticky=NSEW) + + print_substitutions_button = Button( + root, + text="Print substitutions", + command=lambda: print_current_substitutions( # type: ignore + experiment, + ), + ) + print_substitutions_button.grid(row=1, column=4, sticky=NSEW) + + print_simplified_function_button = Button( + root, + text="Print simplified", + command=lambda: print_simplified_function( # type: ignore + experiment, + [experiment.selected_paths_per_function[function_root][0]], # type: ignore + sorted_free_symbols, + free_symbol_ranges, + [function_root.name], + title=function_root.name, + super_title=function_root.name, + ), + ) + print_simplified_function_button.grid(row=1, column=5, sticky=NSEW) + + Button( + root, + text="Plot All", + command=lambda: plot_CostModels( + experiment, + [t[0] for t in options], + sorted_free_symbols, + free_symbol_ranges, + [t[2] for t in options], + title="Full Plot", + super_title=function_root.name, + ), + ).grid() # type: ignore + Button( + root, + text="Add Random (10)", + command=lambda: add_random_models( + root, + pet, + graph, + experiment, + [opt for opt in options if not opt[2].startswith("Rand ")], + sorted_free_symbols, + free_symbol_ranges, + free_symbol_distributions, + function_root, + parent_frame, + spawned_windows, + window_title, + ), + ).grid() + Button( + root, text="Save Models", command=lambda: __save_models(experiment, function_root, options) + ).grid() + Button( + root, + text="Export all codes", + command=lambda: __export_all_codes(pet, graph, experiment, options, function_root), + ).grid() + Button(root, text="Close", command=lambda: root.destroy()).grid() + + # create option entries + for row_idx, option_tuple in enumerate(options): + option, context, option_name = option_tuple + row_idx = row_idx + 1 + 5 # to account for column headers + label = Entry(root, relief=RIDGE) + label.grid(row=row_idx, column=0, sticky=NSEW) + label.insert(END, option_name) + label.configure(state=DISABLED, disabledforeground="black") + + decisions = Entry(root, relief=RIDGE) + decisions.grid(row=row_idx, column=1, sticky=NSEW) + decisions.insert(END, str(option.path_decisions)) + decisions.configure(state=DISABLED, disabledforeground="black") + + options_field = Entry(root, relief=RIDGE) + options_field.grid(row=row_idx, column=2, sticky=NSEW) + options_field.configure(state=DISABLED, disabledforeground="black") + + plot_button = Button( + options_field, + text="Plot", + command=lambda opt=option, opt_name=option_name: plot_CostModels( # type: ignore + experiment, + [opt], # type: ignore + sorted_free_symbols, + free_symbol_ranges, + [opt_name], + title=opt_name, + super_title=function_root.name, + ), + ) + plot_button.grid(row=0, column=0) + details_button = Button( + options_field, + text="Details", + command=lambda opt=option, opt_name=option_name: display_choices_for_model( # type: ignore + graph, opt, window_title=opt_name # type: ignore + ), + ) + details_button.grid(row=0, column=1) + + export_code_button = Button( + options_field, + text="Export Code", + command=lambda opt=option, opt_name=option_name, ctx=context: export_code( # type: ignore + pet, graph, experiment, opt, ctx, opt_name, function_root + ), + ) + export_code_button.grid(row=0, column=2) + + def __update_selection(cm, ctx): + experiment.selected_paths_per_function[function_root] = (cm, ctx) + experiment.substitutions[ + cast(Symbol, function_root.sequential_costs) + ] = experiment.selected_paths_per_function[function_root][0].sequential_costs + experiment.substitutions[ + cast(Symbol, function_root.parallelizable_costs) + ] = experiment.selected_paths_per_function[function_root][0].parallelizable_costs + # update displayed value + label2.configure(state=NORMAL) + label2.delete(0, END) + label2.insert(0, str(cm.path_decisions)) + label2.configure(state=DISABLED) + + update_selection_button = Button( + options_field, + text="Update selection", + command=lambda opt=option, opt_name=option_name, ctx=context: __update_selection( # type: ignore + opt, ctx + ), + ) + update_selection_button.grid(row=0, column=3) + + root.mainloop() + + return options + + +def __save_models( + experiment: Experiment, + function_root: FunctionRoot, + options: List[Tuple[CostModel, ContextObject, str]], +): + print("SAVE: ", function_root) + print("\ttype; ", type(function_root)) + experiment.function_models[function_root] = options + print("Saved models for: ", function_root.name) + + +def add_random_models( + root: Optional[tkinter.Toplevel], + pet: PETGraphX, + graph: nx.DiGraph, + experiment: Experiment, + conserve_options: List[Tuple[CostModel, ContextObject, str]], + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + free_symbol_distributions: Dict[Symbol, FreeSymbolDistribution], + function_root: FunctionRoot, + parent_frame: Optional[tkinter.Frame], + spawned_windows: List[tkinter.Toplevel], + window_title=None, + show_results: bool = True, +) -> List[Tuple[CostModel, ContextObject, str]]: + if root is not None: + # close window + root.destroy() + + # generate random models + # sort random models + # add models to the list of options + random_paths = 10 # amount of random models to be generated + + ( + minimum, + maximum, + median, + lower_quartile, + upper_quartile, + ) = find_quasi_optimal_using_random_samples( + experiment, + graph, + function_root, + random_paths, + sorted_free_symbols, + free_symbol_ranges, + free_symbol_distributions, + verbose=True, + ) + conserve_options.append((minimum[0], minimum[1], "Rand Minimum")) + conserve_options.append((maximum[0], maximum[1], "Rand Maximum")) + conserve_options.append((median[0], median[1], "Rand Median")) + conserve_options.append((lower_quartile[0], lower_quartile[1], "Rand 25% Quartile")) + conserve_options.append((upper_quartile[0], upper_quartile[1], "Rand 75% Quartile")) + + # plot + if show_results: + show_options( + pet, + graph, + experiment, + conserve_options, + sorted_free_symbols, + free_symbol_ranges, + free_symbol_distributions, + function_root, + cast(tkinter.Frame, parent_frame), # valid due to show_results flag + spawned_windows, + window_title, + ) + + return conserve_options + + +def __export_all_codes( + pet: PETGraphX, + graph: nx.DiGraph, + experiment: Experiment, + options: List[Tuple[CostModel, ContextObject, str]], + function_root: FunctionRoot, +): + for opt, ctx, label in options: + export_code(pet, graph, experiment, opt, ctx, label, function_root) diff --git a/discopop_library/discopop_optimizer/gui/presentation/__init__.py b/discopop_library/discopop_optimizer/gui/presentation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/gui/queries/ValueTableQuery.py b/discopop_library/discopop_optimizer/gui/queries/ValueTableQuery.py new file mode 100644 index 000000000..dd777e877 --- /dev/null +++ b/discopop_library/discopop_optimizer/gui/queries/ValueTableQuery.py @@ -0,0 +1,189 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import sys +from typing import List, Tuple, Optional, Dict, cast + +from sympy import Symbol, Expr +from tkinter import * +from tkinter import ttk +import tkinter as tk + +from discopop_library.discopop_optimizer.classes.enums.Distributions import FreeSymbolDistribution +from discopop_library.discopop_optimizer.gui.widgets.ScrollableFrame import ScrollableFrameWidget + + +def query_user_for_symbol_values( + symbols: List[Symbol], + suggested_values: Dict[Symbol, Expr], + arguments: Dict, + parent_frame: Optional[tk.Frame], +) -> List[ + Tuple[ + Symbol, Optional[float], Optional[float], Optional[float], Optional[FreeSymbolDistribution] + ] +]: + """Opens a GUI-Table to query values for each given Symbol from the user. + The queried values are: Specific value, Range start, Range end. + In every case, either a specific value, or a range must be given. + In case the optimizer is started in headless mode, the suggested values are used. + Return: [(symbol, symbol_value, range_start, range_end, distribution)]""" + query_result: List[ + Tuple[ + Symbol, + Optional[float], + Optional[float], + Optional[float], + Optional[FreeSymbolDistribution], + ] + ] = [] + + # check for headless mode + if arguments["--headless-mode"]: + # return the suggested values + for symbol in symbols: + query_result.append((symbol, suggested_values[symbol].evalf(), None, None, None)) + return query_result + + column_headers = ["Symbol Name", "Symbol Value", "Range Start", "Range End", "Range Relevance"] + + if parent_frame is None: + raise ValueError("No frame provided!") + + # configure weights + parent_frame.rowconfigure(0, weight=1) + parent_frame.columnconfigure(0, weight=1) + # create scrollable frame + scrollable_frame_widget = ScrollableFrameWidget(parent_frame) + scrollable_frame = scrollable_frame_widget.get_scrollable_frame() + + rows = [] + # set column headers + header_cols = [] + for col_idx, header in enumerate(column_headers): + e = Entry(scrollable_frame, relief=RIDGE) + e.grid(row=0, column=col_idx, sticky=NSEW) + e.insert(END, header) + e.configure(state=DISABLED, disabledforeground="black") + header_cols.append(e) + rows.append(header_cols) + + # create choice vars for range relevance + range_relevance_vars: Dict[Symbol, tk.StringVar] = dict() + for free_symbol in symbols: + range_relevance_vars[free_symbol] = tk.StringVar() + + # create query Table + for row_idx, free_symbol in enumerate(symbols): + row_idx = row_idx + 1 # to account for column headers + cols = [] + for col_idx, column_name in enumerate(column_headers): + e = Entry(scrollable_frame, relief=RIDGE) + e.grid(row=row_idx, column=col_idx, sticky=NSEW) + if col_idx == 0: + # symbol name + e.insert(END, str(free_symbol)) + e.configure(state=DISABLED, disabledforeground="black") + elif col_idx == 4: + # range relevance + choices = ["-->", "<--", "=="] + range_relevance_vars[free_symbol].set(choices[0]) + option_menu = tk.OptionMenu(e, range_relevance_vars[free_symbol], *choices) + option_menu.grid(sticky=NSEW) + else: + # queried value + if col_idx == 1 and free_symbol in suggested_values: + # insert suggested value, if one exists + e.insert(END, str(suggested_values[free_symbol])) + cols.append(e) + rows.append(cols) + + def validate() -> bool: + ret_val = True + # validate entries + for row_idx, row in enumerate(rows): + if row_idx == 0: + continue + row_valid = False + # check if either a specific value or ranges are set + if len(row[1].get()) != 0: + # specific value + row_valid = True + elif len(row[2].get()) != 0 and len(row[3].get()) != 0: + # range specified + row_valid = True + else: + # row invalid + ret_val = False + # mark row + if row_valid: + for col in row[1:]: + col.configure(background="grey") + else: + for col in row[1:]: + col.configure(background="red") + return ret_val + + def onPress(): + if not validate(): + return + # fetch entries + for row_idx, row in enumerate(rows): + if row_idx == 0: + continue + row_element = [] + for col_idx, col in enumerate(row): + if col_idx == 0: + # append symbol to row_element + row_element.append(symbols[row_idx - 1]) # -1 to account for column headers + elif col_idx == 4: + # ignore range relevance, as it is added afterwards + pass + else: + field_value = col.get() + if len(field_value) == 0: + row_element.append(None) + else: + row_element.append(float(field_value)) + # get enum object from range relevance choice if no specific value has been set in the row + if row_element[1] is None: + string_value = range_relevance_vars[row_element[0]].get() + if string_value == "-->": + range_relevance = FreeSymbolDistribution.RIGHT_HEAVY + elif string_value == "<--": + range_relevance = FreeSymbolDistribution.LEFT_HEAVY + else: + range_relevance = FreeSymbolDistribution.UNIFORM + else: + range_relevance = None + row_element.append(range_relevance) + + # create result tuple + query_result.append( + cast( + Tuple[ + Symbol, + Optional[float], + Optional[float], + Optional[float], + Optional[FreeSymbolDistribution], + ], + tuple(row_element), + ) + ) + + # close elements on optimizer_frame + for c in parent_frame.winfo_children(): + c.destroy() + + parent_frame.quit() + + scrollable_frame_widget.finalize(len(symbols), row=0, col=0) + Button(parent_frame, text="Save", command=lambda: onPress()).grid() + Button(parent_frame, text="Validate", command=lambda: validate()).grid() + parent_frame.mainloop() + return query_result diff --git a/discopop_library/discopop_optimizer/gui/queries/__init__.py b/discopop_library/discopop_optimizer/gui/queries/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/gui/widgets/ScrollableFrame.py b/discopop_library/discopop_optimizer/gui/widgets/ScrollableFrame.py new file mode 100644 index 000000000..9cec33f6b --- /dev/null +++ b/discopop_library/discopop_optimizer/gui/widgets/ScrollableFrame.py @@ -0,0 +1,54 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import tkinter as tk +from tkinter import ttk + + +class ScrollableFrameWidget(object): + container: ttk.Frame + canvas: tk.Canvas + scrollbar: tk.Scrollbar + scrollable_frame: tk.Frame # important + + def __init__(self, parent_frame): + self.container = ttk.Frame(parent_frame) + self.canvas = tk.Canvas(self.container) + self.scrollbar = ttk.Scrollbar(self.container, orient="vertical", command=self.canvas.yview) + self.scrollable_frame = ttk.Frame(self.canvas) + + # configure weights + self.container.rowconfigure(0, weight=1) + self.container.columnconfigure(0, weight=1) + self.canvas.rowconfigure(0, weight=1) + self.canvas.columnconfigure(0, weight=1) + self.scrollable_frame.rowconfigure(0, weight=1) + self.scrollable_frame.columnconfigure(0, weight=1) + + self.scrollable_frame.bind( + "", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")) + ) + + self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") + + self.canvas.configure(yscrollcommand=self.scrollbar.set) + + def finalize( + self, row_count: int, row: int = 0, col: int = 0, rowspan: int = 1, columnspan: int = 1 + ): + if rowspan < 1: + rowspan = 1 + if columnspan < 1: + columnspan = 1 + self.container.grid( + row=row, column=col, columnspan=columnspan, rowspan=rowspan, sticky=tk.NSEW + ) + self.canvas.grid(row=0, column=0, sticky=tk.NSEW) + self.scrollbar.grid(row=0, rowspan=max(row_count, 1), column=1, sticky=tk.NS) + + def get_scrollable_frame(self) -> tk.Frame: + return self.scrollable_frame diff --git a/discopop_library/discopop_optimizer/gui/widgets/__init__.py b/discopop_library/discopop_optimizer/gui/widgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/requirements.txt b/discopop_library/discopop_optimizer/requirements.txt new file mode 100644 index 000000000..916dc94b2 --- /dev/null +++ b/discopop_library/discopop_optimizer/requirements.txt @@ -0,0 +1,11 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. + +jsonpickle +sympy +sympy_plot_backends \ No newline at end of file diff --git a/discopop_library/discopop_optimizer/suggestions/__init__.py b/discopop_library/discopop_optimizer/suggestions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/suggestions/importers/__init__.py b/discopop_library/discopop_optimizer/suggestions/importers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/suggestions/importers/base.py b/discopop_library/discopop_optimizer/suggestions/importers/base.py new file mode 100644 index 000000000..1c803761a --- /dev/null +++ b/discopop_library/discopop_optimizer/suggestions/importers/base.py @@ -0,0 +1,31 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import networkx as nx # type: ignore + +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.suggestions.importers.do_all import ( + import_suggestion as import_doall, +) +from discopop_library.discopop_optimizer.suggestions.importers.reduction import ( + import_suggestion as import_reduction, +) + + +def import_suggestions( + detection_result, graph: nx.DiGraph, get_next_free_node_id_function, environment: Experiment +) -> nx.DiGraph: + """Imports the suggestions specified in res into the passed graph and returns the modified graph""" + + # import do-all + for suggestion in detection_result.do_all: + graph = import_doall(graph, suggestion, get_next_free_node_id_function, environment) + + # import reduction + for suggestion in detection_result.reduction: + graph = import_reduction(graph, suggestion, get_next_free_node_id_function, environment) + return graph diff --git a/discopop_library/discopop_optimizer/suggestions/importers/do_all.py b/discopop_library/discopop_optimizer/suggestions/importers/do_all.py new file mode 100644 index 000000000..638be66d9 --- /dev/null +++ b/discopop_library/discopop_optimizer/suggestions/importers/do_all.py @@ -0,0 +1,171 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import copy +from typing import cast, Tuple, List, Dict + +import networkx as nx # type: ignore +from sympy import Expr, Integer, Symbol, log, Float, init_printing # type: ignore + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Microbench.utils import ( + convert_microbench_to_discopop_workload, + convert_discopop_to_microbench_workload, +) +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.edges.OptionEdge import OptionEdge +from discopop_library.discopop_optimizer.classes.edges.RequirementEdge import RequirementEdge +from discopop_library.discopop_optimizer.classes.nodes.Loop import Loop +from discopop_library.discopop_optimizer.classes.nodes.Workload import Workload +from discopop_library.discopop_optimizer.utilities.MOGUtilities import data_at + +do_all_device_ids = [0] + + +def import_suggestion( + graph: nx.DiGraph, suggestion, get_next_free_node_id_function, environment: Experiment +) -> nx.DiGraph: + # find a node which belongs to the suggestion + buffer = [n for n in graph.nodes] + introduced_options = [] + for node in buffer: + if suggestion.node_id == data_at(graph, node).cu_id: + # todo: This implementation for the device id is temporary and MUST be replaced + for device_id in do_all_device_ids: + # reserve a node id for the new parallelization option + new_node_id = get_next_free_node_id_function() + # copy data from existing node + node_data_copy = copy.deepcopy(data_at(graph, node)) + node_data_copy.node_id = new_node_id + + # set the device id for the suggestion + node_data_copy.device_id = device_id + # remove cu_id to prevent using parallelization options as basis for new versions + node_data_copy.cu_id = None + # mark node for parallel execution + node_data_copy.execute_in_parallel = True + + # copy loop iteration variable + cast(Loop, node_data_copy).iterations_symbol = cast( + Loop, node_data_copy + ).iterations_symbol + # add suggestion to node data + node_data_copy.suggestion = suggestion + node_data_copy.suggestion_type = "do_all" + # add the cost multiplier to represent the effects of the suggestion + ( + cast(Workload, node_data_copy).cost_multiplier, + introduced_symbols, + ) = get_cost_multiplier(new_node_id, environment, device_id) + # add the overhead term to represent the overhead incurred by the suggestion + cast(Workload, node_data_copy).overhead, tmp_introduced_symbols = get_overhead_term( + cast(Loop, node_data_copy), environment, device_id + ) + introduced_symbols += tmp_introduced_symbols + + node_data_copy.introduced_symbols += introduced_symbols + + # create a new node for the option + graph.add_node(new_node_id, data=node_data_copy) + # mark the newly created option + graph.add_edge(node, new_node_id, data=OptionEdge()) + + # save the id of the introduced parallelization option to connect them afterwards + introduced_options.append(new_node_id) + + # connect the newly created node to the parent and successor of node + for edge in graph.in_edges(node): + edge_data = copy.deepcopy(graph.edges[edge]["data"]) + graph.add_edge(edge[0], new_node_id, data=edge_data) + for edge in graph.out_edges(node): + edge_data = copy.deepcopy(graph.edges[edge]["data"]) + graph.add_edge(new_node_id, edge[1], data=edge_data) + # if a successor has no device id already, + # set it to 0 to simulate "leaving" the device after the suggestion + # todo: this should not happen here, but be considered when calculating the updates in order to + # prevent suggestions from influencing each other by "mapping" workloads to certain devices. + # todo re-enable? + # if ( + # type(edge_data) == SuccessorEdge + # and data_at(graph, edge[1]).device_id is None + # ): + # data_at(graph, edge[1]).device_id = 0 + + # connect introduced parallelization options to support path restraining + for node_id_1 in introduced_options: + for node_id_2 in introduced_options: + if node_id_1 == node_id_2: + continue + graph.add_edge(node_id_1, node_id_2, data=RequirementEdge()) + return graph + + +def get_cost_multiplier( + node_id: int, environment: Experiment, device_id: int +) -> Tuple[CostModel, List[Symbol]]: + """Creates and returns the multiplier to represent the effects of the given suggestion on the cost model. + A CostModel object is used to store the information on the path selection. + Returns the multiplier and the list of introduces symbols + Multiplier for Do-All: + 1 / Thread_count""" + # get device specifications + + thread_count = environment.get_system().get_device(device_id).get_thread_count() + + multiplier = Integer(1) / thread_count + cm = CostModel(multiplier, Integer(1)) + + # return cm, [thread_count] + return cm, [] + + +def get_overhead_term( + node_data: Loop, environment: Experiment, device_id: int +) -> Tuple[CostModel, List[Symbol]]: + """Creates and returns the Expression which represents the Overhead incurred by the given suggestion. + For testing purposes, the following function is used to represent the overhead incurred by a do-all loop. + The function has been created using Extra-P. + unit of the overhead term are micro seconds.""" + # retrieve DoAll overhead model + overhead_model = environment.get_system().get_device_doall_overhead_model( + environment.get_system().get_device(device_id) + ) + # substitute workload, iterations and threads + thread_count = environment.get_system().get_device(device_id).get_thread_count() + iterations = node_data.iterations_symbol + # since node_data is of type Loop, parallelizable_workload must be set + per_iteration_workload = cast(Expr, node_data.parallelizable_workload) + # convert DiscoPoP workload to Microbench workload + converted_per_iteration_workload = convert_discopop_to_microbench_workload( + per_iteration_workload, iterations + ) + + substitutions: Dict[Symbol, Expr] = {} + + for symbol in cast(List[Symbol], overhead_model.free_symbols): + if symbol.name == "workload": + substitutions[symbol] = converted_per_iteration_workload + elif symbol.name == "iterations": + substitutions[symbol] = iterations + elif symbol.name == "threads": + substitutions[symbol] = thread_count + else: + raise ValueError("Unknown symbol: ", symbol) + + substituted_overhead_model = overhead_model.xreplace(substitutions) + + # register symbol for overhead + doall_overhead_symbol = Symbol( + "doall_" + str(node_data.node_id) + "_pos_" + str(node_data.position) + "_overhead" + ) + + environment.substitutions[doall_overhead_symbol] = substituted_overhead_model + + # cm = CostModel(Integer(0), substituted_overhead_model) + cm = CostModel(Integer(0), doall_overhead_symbol) + # add weight to overhead + return cm, [] diff --git a/discopop_library/discopop_optimizer/suggestions/importers/reduction.py b/discopop_library/discopop_optimizer/suggestions/importers/reduction.py new file mode 100644 index 000000000..714ab3c49 --- /dev/null +++ b/discopop_library/discopop_optimizer/suggestions/importers/reduction.py @@ -0,0 +1,144 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import copy +from typing import cast, Tuple, List, Dict + +import networkx as nx # type: ignore +from sympy import Expr, Integer, Symbol, log, Float, init_printing # type: ignore + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.Microbench.utils import ( + convert_microbench_to_discopop_workload, + convert_discopop_to_microbench_workload, +) +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.nodes.Loop import Loop +from discopop_library.discopop_optimizer.classes.nodes.Workload import Workload +from discopop_library.discopop_optimizer.utilities.MOGUtilities import data_at + +reduction_device_ids = [0] + + +def import_suggestion( + graph: nx.DiGraph, suggestion, get_next_free_node_id_function, environment: Experiment +) -> nx.DiGraph: + # find a node which belongs to the suggestion + buffer = [n for n in graph.nodes] + for node in buffer: + if suggestion.node_id == data_at(graph, node).cu_id: + for device_id in reduction_device_ids: + # reserve a node id for the new parallelization option + new_node_id = get_next_free_node_id_function() + # copy data from existing node + node_data_copy = copy.deepcopy(data_at(graph, node)) + # set the device id for the suggestion + node_data_copy.device_id = device_id + # remove cu_id to prevent using parallelization options as basis for new versions + node_data_copy.cu_id = None + # mark node for parallel execution + node_data_copy.execute_in_parallel = True + # copy loop iteration variable + cast(Loop, node_data_copy).iterations_symbol = cast( + Loop, node_data_copy + ).iterations_symbol + # add suggestion to node data + node_data_copy.suggestion = suggestion + node_data_copy.suggestion_type = "reduction" + # add the cost multiplier to represent the effects of the suggestion + ( + cast(Workload, node_data_copy).cost_multiplier, + introduced_symbols, + ) = get_cost_multiplier(new_node_id, environment, device_id) + # add the overhead term to represent the overhead incurred by the suggestion + cast(Workload, node_data_copy).overhead, tmp_introduced_symbols = get_overhead_term( + cast(Loop, node_data_copy), environment, device_id + ) + introduced_symbols += tmp_introduced_symbols + + node_data_copy.introduced_symbols += introduced_symbols + + # create a new node for the option + graph.add_node(new_node_id, data=node_data_copy) + + # connect the newly created node to the parent and successor of node + for edge in graph.in_edges(node): + edge_data = copy.deepcopy(graph.edges[edge]["data"]) + graph.add_edge(edge[0], new_node_id, data=edge_data) + for edge in graph.out_edges(node): + edge_data = copy.deepcopy(graph.edges[edge]["data"]) + graph.add_edge(new_node_id, edge[1], data=edge_data) + # if the successor has no device id already, + # set it to 0 to simulate "leaving" the device after the suggestion + # todo: this should not happen here, but be considered when calculating the updates in order to + # prevent suggestions from influencing each other by "mapping" workloads to certain devices. + # todo re-enable? + # if data_at(graph, edge[1]).device_id is None: + # data_at(graph, edge[1]).device_id = 0 + return graph + + +def get_cost_multiplier( + node_id: int, environment: Experiment, device_id: int +) -> Tuple[CostModel, List[Symbol]]: + """Creates and returns the multiplier to represent the effects of the given suggestion on the cost model. + A CostModel object is used to store the information on the path selection. + Returns the multiplier and the list of introduces symbols + Multiplier for Reduction: + 1 / Compute_capa""" + # get device specifications + thread_count = environment.get_system().get_device(device_id).get_thread_count() + + multiplier = Integer(1) / thread_count + cm = CostModel(multiplier, Integer(1)) + + # return cm, [thread_count] + return cm, [] + + +def get_overhead_term( + node_data: Loop, environment: Experiment, device_id: int +) -> Tuple[CostModel, List[Symbol]]: + """Creates and returns the Expression which represents the Overhead incurred by the given suggestion. + For testing purposes, the following function is used to represent the overhead incurred by a do-all loop. + The function has been created using Extra-P. + Unit of the overhead term are micro seconds.""" + + # retrieve Reduction overhead model + overhead_model = environment.get_system().get_device_reduction_overhead_model( + environment.get_system().get_device(device_id) + ) + # substitute workload, iterations and threads + thread_count = environment.get_system().get_device(device_id).get_thread_count() + iterations = node_data.iterations_symbol + # since node_data is of type Loop, parallelizable_workload has to exist + per_iteration_workload = cast(Expr, node_data.parallelizable_workload) + # convert DiscoPoP workload to Microbench workload + converted_per_iteration_workload = convert_discopop_to_microbench_workload( + per_iteration_workload, iterations + ) + + substitutions: Dict[Symbol, Expr] = {} + + for symbol in cast(List[Symbol], overhead_model.free_symbols): + if symbol.name == "workload": + substitutions[symbol] = converted_per_iteration_workload + elif symbol.name == "iterations": + substitutions[symbol] = iterations + elif symbol.name == "threads": + substitutions[symbol] = thread_count + else: + raise ValueError("Unknown symbol: ", symbol) + + substituted_overhead_model = overhead_model.xreplace(substitutions) + + cm = CostModel(Integer(0), substituted_overhead_model) + + # todo: convert result (in s) to workload + + # add weight to overhead + return cm, [] diff --git a/discopop_library/discopop_optimizer/utilities/MOGUtilities.py b/discopop_library/discopop_optimizer/utilities/MOGUtilities.py index 359da42e0..f5884d7b5 100644 --- a/discopop_library/discopop_optimizer/utilities/MOGUtilities.py +++ b/discopop_library/discopop_optimizer/utilities/MOGUtilities.py @@ -5,11 +5,13 @@ # This software may be modified and distributed under the terms of # the 3-Clause BSD License. See the LICENSE file in the package base # directory for details. -import sys -from typing import List, cast, Set, Tuple +import random +from typing import List, cast, Set, Optional, Tuple import matplotlib.pyplot as plt # type:ignore +import matplotlib import networkx as nx # type: ignore +import sympy from discopop_explorer.PETGraphX import MemoryRegion, NodeID from discopop_library.discopop_optimizer.classes.edges.ChildEdge import ChildEdge @@ -18,8 +20,11 @@ from discopop_library.discopop_optimizer.classes.edges.RequirementEdge import RequirementEdge from discopop_library.discopop_optimizer.classes.edges.SuccessorEdge import SuccessorEdge from discopop_library.discopop_optimizer.classes.edges.TemporaryEdge import TemporaryEdge +from discopop_library.discopop_optimizer.classes.nodes.ContextNode import ContextNode from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot from discopop_library.discopop_optimizer.classes.nodes.GenericNode import GenericNode +from discopop_library.discopop_optimizer.classes.nodes.Loop import Loop +from discopop_library.discopop_optimizer.classes.nodes.Workload import Workload def get_nodes_from_cu_id(graph: nx.DiGraph, cu_node_id: NodeID) -> List[int]: @@ -110,6 +115,181 @@ def has_temporary_successor(graph: nx.DiGraph, node_id: int) -> bool: ) +def show(graph): + """Plots the graph + + :return: + """ + matplotlib.use("TkAgg") + fig, ax = plt.subplots() + try: + pos = nx.planar_layout(graph) # good + except nx.exception.NetworkXException: + try: + # fallback layouts + pos = nx.shell_layout(graph) # maybe + except nx.exception.NetworkXException: + pos = nx.random_layout(graph) + + drawn_nodes = set() + nodes_lists = dict() + node_ids = dict() + node_insertion_sequence = [] + # draw nodes + node_insertion_sequence.append(FunctionRoot) + nodes_lists[FunctionRoot] = nx.draw_networkx_nodes( + graph, + pos=pos, + ax=ax, + node_size=200, + node_color="#ff5151", + node_shape="d", + nodelist=[n for n in graph.nodes if isinstance(data_at(graph, n), FunctionRoot)], + ) + node_ids[FunctionRoot] = [n for n in graph.nodes if isinstance(data_at(graph, n), FunctionRoot)] + drawn_nodes.update([n for n in graph.nodes if isinstance(data_at(graph, n), FunctionRoot)]) + + node_insertion_sequence.append(Loop) + nodes_lists[Loop] = nx.draw_networkx_nodes( + graph, + pos=pos, + ax=ax, + node_size=200, + node_color="#ff5151", + node_shape="s", + nodelist=[n for n in graph.nodes if isinstance(data_at(graph, n), Loop)], + ) + node_ids[Loop] = [n for n in graph.nodes if isinstance(data_at(graph, n), Loop)] + drawn_nodes.update([n for n in graph.nodes if isinstance(data_at(graph, n), Loop)]) + + node_insertion_sequence.append(ContextNode) + nodes_lists[ContextNode] = nx.draw_networkx_nodes( + graph, + pos=pos, + ax=ax, + node_size=200, + node_color="yellow", + node_shape="s", + nodelist=[n for n in graph.nodes if isinstance(data_at(graph, n), ContextNode)], + ) + node_ids[ContextNode] = [n for n in graph.nodes if isinstance(data_at(graph, n), ContextNode)] + drawn_nodes.update([n for n in graph.nodes if isinstance(data_at(graph, n), ContextNode)]) + + node_insertion_sequence.append(Workload) + nodes_lists[Workload] = nx.draw_networkx_nodes( + graph, + pos=pos, + ax=ax, + node_size=200, + node_color="#2B85FD", + node_shape="o", + nodelist=[ + n + for n in graph.nodes + if isinstance(data_at(graph, n), Workload) and n not in drawn_nodes + ], + ) + node_ids[Workload] = [ + n for n in graph.nodes if isinstance(data_at(graph, n), Workload) and n not in drawn_nodes + ] + drawn_nodes.update( + [n for n in graph.nodes if isinstance(data_at(graph, n), Workload) and n not in drawn_nodes] + ) + + # id as label + labels = {} + for n in graph.nodes: + labels[n] = graph.nodes[n]["data"].get_plot_label() + nx.draw_networkx_labels(graph, pos, labels, font_size=7, ax=ax) + + nx.draw_networkx_edges( + graph, + pos, + ax=ax, + edge_color="black", + edgelist=[e for e in graph.edges(data="data") if isinstance(e[2], SuccessorEdge)], + ) + + nx.draw_networkx_edges( + graph, + pos, + ax=ax, + edge_color="red", + edgelist=[e for e in graph.edges(data="data") if isinstance(e[2], ChildEdge)], + ) + + nx.draw_networkx_edges( + graph, + pos, + ax=ax, + edge_color="green", + edgelist=[e for e in graph.edges(data="data") if isinstance(e[2], TemporaryEdge)], + ) + + nx.draw_networkx_edges( + graph, + pos, + ax=ax, + edge_color="pink", + edgelist=[e for e in graph.edges(data="data") if isinstance(e[2], OptionEdge)], + ) + + nx.draw_networkx_edges( + graph, + pos, + ax=ax, + edge_color="yellow", + edgelist=[e for e in graph.edges(data="data") if isinstance(e[2], RequirementEdge)], + ) + + # define tool tip style when hovering + # based on https://stackoverflow.com/questions/61604636/adding-tooltip-for-nodes-in-python-networkx-graph + annot = ax.annotate( + "", + xy=(0, 0), + xytext=(20, 20), + textcoords="offset points", + bbox=dict(boxstyle="round", fc="w"), + arrowprops=dict(arrowstyle="->"), + ) + annot.set_visible(False) + + idx_to_node_dict = {} + for idx, node in enumerate(graph.nodes): + idx_to_node_dict[idx] = node + + def update_annot(ind, node_ids_list): + node_idx = ind["ind"][0] + node_id = node_ids_list[node_idx] + xy = pos[node_id] + annot.xy = xy + node_attr = {"node": node_id} + node_attr.update(graph.nodes[node_id]) + text = data_at(graph, node_id).get_hover_text() + annot.set_text(text) + + def hover(event): + vis = annot.get_visible() + if event.inaxes == ax: + for node_type in node_insertion_sequence: + nodes = nodes_lists[node_type] + try: + cont, ind = nodes.contains(event) + if cont: + update_annot(ind, node_ids[node_type]) + annot.set_visible(True) + fig.canvas.draw_idle() + else: + if vis: + annot.set_visible(False) + fig.canvas.draw_idle() + except TypeError: + pass + + fig.canvas.mpl_connect("motion_notify_event", hover) + plt.show() + + def add_successor_edge(graph: nx.DiGraph, source_id: int, target_id: int): edge_data = SuccessorEdge() graph.add_edge(source_id, target_id, data=edge_data) @@ -174,6 +354,14 @@ def get_all_function_nodes(graph: nx.DiGraph) -> List[int]: return list(result_set) +def get_all_loop_nodes(graph: nx.DiGraph) -> List[int]: + result_set: Set[int] = set() + for node_id in graph.nodes: + if type(graph.nodes[node_id]["data"]) == Loop: + result_set.add(node_id) + return list(result_set) + + def get_read_and_written_data_from_subgraph( graph: nx.DiGraph, node_id: int, ignore_successors: bool = False ) -> Tuple[Set[MemoryRegion], Set[MemoryRegion]]: diff --git a/discopop_library/discopop_optimizer/utilities/optimization/GlobalOptimization/RandomSamples.py b/discopop_library/discopop_optimizer/utilities/optimization/GlobalOptimization/RandomSamples.py new file mode 100644 index 000000000..3cb2a16c3 --- /dev/null +++ b/discopop_library/discopop_optimizer/utilities/optimization/GlobalOptimization/RandomSamples.py @@ -0,0 +1,111 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import copy +import random +from random import shuffle +from typing import List, Dict, Tuple + +import networkx as nx # type: ignore +from spb import plot3d, MB, plot # type: ignore +from sympy import Symbol, Expr + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.CostModels.utilities import get_random_path +from discopop_library.discopop_optimizer.DataTransfers.DataTransfers import calculate_data_transfers +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.enums.Distributions import FreeSymbolDistribution +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.gui.plotting.CostModels import plot_CostModels +from discopop_library.discopop_optimizer.utilities.MOGUtilities import show + + +def find_quasi_optimal_using_random_samples( + experiment: Experiment, + graph: nx.DiGraph, + function_root: FunctionRoot, + random_path_count: int, + sorted_free_symbols: List[Symbol], + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + free_symbol_distributions: Dict[Symbol, FreeSymbolDistribution], + verbose: bool = False, +): + """Returns the identified minimum, maximum, median, 25% quartile and 75% quartile of the random_path_count samples. + NOTE: The decisions should be treated as suggestions, not mathematically accurate decisions + due to the used comparison method!""" + random_paths: List[Tuple[CostModel, ContextObject]] = [] + if verbose: + print("Generating ", random_path_count, "random paths") + i = 0 + # create a temporary copy of the substitutions list to roll back unwanted modifications + substitutions_buffer = copy.deepcopy(experiment.substitutions) + while i < random_path_count: + # reset substitutions + experiment.substitutions = copy.deepcopy(substitutions_buffer) + + tmp_dict = dict() + tmp_dict[function_root] = [ + get_random_path(experiment, graph, function_root.node_id, must_contain=None) + ] + try: + random_paths.append(calculate_data_transfers(graph, tmp_dict)[function_root][0]) + i += 1 + except ValueError as ve: + if verbose: + print(ve) + # might occur as a result of invalid paths due to restrictions + pass + # reset substitutions + experiment.substitutions = copy.deepcopy(substitutions_buffer) + + # apply substitutions and set free symbol ranges and distributions + if verbose: + print("\tApplying substitutions...") + + random_paths_with_substitutions: List[Tuple[CostModel, ContextObject, CostModel]] = [] + for model, context in random_paths: + substituted_model = copy.deepcopy(model) + + # apply substitutions iteratively + modification_found = True + while modification_found: + modification_found = False + # apply substitutions to parallelizable costs + tmp_model = substituted_model.parallelizable_costs.subs(experiment.substitutions) + if tmp_model != substituted_model.parallelizable_costs: + modification_found = True + substituted_model.parallelizable_costs = tmp_model + + # apply substitutions to sequential costs + tmp_model = substituted_model.sequential_costs.subs(experiment.substitutions) + if tmp_model != substituted_model.sequential_costs: + modification_found = True + substituted_model.sequential_costs = tmp_model + substituted_model.free_symbol_ranges = free_symbol_ranges + substituted_model.free_symbol_distributions = free_symbol_distributions + + random_paths_with_substitutions.append((model, context, substituted_model)) + + if verbose: + print("\tSorting...") + sorted_list = sorted(random_paths_with_substitutions, key=lambda x: x[2]) # BOTTLENECK! + if verbose: + print("\tDone.") + minimum = (sorted_list[0][0], sorted_list[0][1]) + maximum = (sorted_list[-1][0], sorted_list[-1][1]) + median = (sorted_list[int(len(sorted_list) / 2)][0], sorted_list[int(len(sorted_list) / 2)][1]) + upper_quartile = ( + sorted_list[int(len(sorted_list) / 4 * 3)][0], + sorted_list[int(len(sorted_list) / 4 * 3)][1], + ) + lower_quartile = ( + sorted_list[int(len(sorted_list) / 4 * 1)][0], + sorted_list[int(len(sorted_list) / 4 * 1)][1], + ) + + return minimum, maximum, median, lower_quartile, upper_quartile diff --git a/discopop_library/discopop_optimizer/utilities/optimization/GlobalOptimization/__init__.py b/discopop_library/discopop_optimizer/utilities/optimization/GlobalOptimization/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/utilities/optimization/LocalOptimization/TopDown.py b/discopop_library/discopop_optimizer/utilities/optimization/LocalOptimization/TopDown.py new file mode 100644 index 000000000..1e08492e7 --- /dev/null +++ b/discopop_library/discopop_optimizer/utilities/optimization/LocalOptimization/TopDown.py @@ -0,0 +1,193 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import copy +from typing import Dict, List, Tuple, Set, cast + +import networkx as nx # type: ignore +from sympy import Symbol, Expr + +from discopop_library.discopop_optimizer.CostModels.CostModel import CostModel +from discopop_library.discopop_optimizer.CostModels.DataTransfer.DataTransferCosts import ( + add_data_transfer_costs, +) +from discopop_library.discopop_optimizer.CostModels.utilities import get_node_performance_models +from discopop_library.discopop_optimizer.DataTransfers.DataTransfers import calculate_data_transfers +from discopop_library.discopop_optimizer.Variables.Experiment import Experiment +from discopop_library.discopop_optimizer.classes.context.ContextObject import ContextObject +from discopop_library.discopop_optimizer.classes.enums.Distributions import FreeSymbolDistribution +from discopop_library.discopop_optimizer.classes.nodes.FunctionRoot import FunctionRoot +from discopop_library.discopop_optimizer.utilities.MOGUtilities import ( + get_all_function_nodes, + get_successors, + get_children, + data_at, + show, +) + + +def get_locally_optimized_models( + experiment: Experiment, + graph: nx.DiGraph, + substitutions: Dict[Symbol, Expr], + environment: Experiment, + free_symbol_ranges: Dict[Symbol, Tuple[float, float]], + free_symbol_distributions: Dict[Symbol, FreeSymbolDistribution], +) -> Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]]: + result_dict: Dict[FunctionRoot, List[Tuple[CostModel, ContextObject]]] = dict() + all_function_ids = get_all_function_nodes(graph) + all_function_nodes: List[FunctionRoot] = [ + cast(FunctionRoot, data_at(graph, fn_id)) for fn_id in all_function_ids + ] + for function_node in all_function_ids: + # get a list of all decisions that have to be made + decisions_to_be_made = __find_decisions(graph, function_node) + locally_optimal_choices: List[int] = [] + + # create performance models for individual options of decisions + for decision_options in decisions_to_be_made: + decision_models: List[Tuple[int, Tuple[CostModel, ContextObject]]] = [] + for decision in decision_options: + try: + # create a performance model for the specific decision + performance_models = get_node_performance_models( + experiment, + graph, + function_node, + set(), + all_function_nodes, + restrict_to_decisions={decision}, + do_not_allow_decisions=set([o for o in decision_options if o != decision]), + ignore_node_costs=[ + cast(FunctionRoot, data_at(graph, function_node)).node_id + ], # ignore first node to prevent duplication of function costs + ) + # calculate and append necessary data transfers to the models + performance_models_with_transfers = calculate_data_transfers( + graph, + {cast(FunctionRoot, data_at(graph, function_node)): performance_models}, + ) + + # calculate and append costs of data transfers to the performance models + complete_performance_models = add_data_transfer_costs( + graph, performance_models_with_transfers, environment + ) + # add performance models to decision models for the later selection of the best candidate + for function in complete_performance_models: + for pair in complete_performance_models[function]: + decision_models.append((decision, pair)) + except ValueError as ex: + print(ex) + print("==> Ignoring Decision: ", decision) + continue + + # iteratively apply variable substitutions + decision_models_with_substitutions: List[ + Tuple[int, Tuple[CostModel, ContextObject, CostModel]] + ] = [] + # initialize substitution + for decision, pair in decision_models: + model, context = pair + decision_models_with_substitutions.append( + (decision, (model, context, copy.deepcopy(model))) + ) + + modification_found = True + while modification_found: + modification_found = False + for decision, tpl in decision_models_with_substitutions: + model, context, substituted_model = tpl + + # apply substitutions to parallelizable costs + tmp_model = substituted_model.parallelizable_costs.subs(substitutions) + if tmp_model != substituted_model.parallelizable_costs: + modification_found = True + substituted_model.parallelizable_costs = ( + substituted_model.parallelizable_costs.subs(substitutions) + ) + + # apply substitutions to sequential costs + tmp_model = substituted_model.sequential_costs.subs(substitutions) + if tmp_model != substituted_model.sequential_costs: + modification_found = True + substituted_model.sequential_costs = substituted_model.sequential_costs.subs( + substitutions + ) + + # decision_models_with_substitutions.append( + ## (decision, (model, context, substituted_model)) + # ) + + # set free symbol ranges and distributions for comparisons + for decision, tpl in decision_models_with_substitutions: + model, context, substituted_model = tpl + model.free_symbol_ranges = free_symbol_ranges + model.free_symbol_distributions = free_symbol_distributions + substituted_model.free_symbol_ranges = free_symbol_ranges + substituted_model.free_symbol_distributions = free_symbol_distributions + + # find minimum in decision_models + unpacked_models: List[Tuple[int, CostModel, CostModel]] = [] + for decision, tpl in decision_models_with_substitutions: + model, context, substituted_model = tpl + unpacked_models.append((decision, model, substituted_model)) + if len(unpacked_models) == 0: + continue + minimum = sorted(unpacked_models, key=lambda x: x[2])[0] + locally_optimal_choices.append(minimum[0]) + + if len(locally_optimal_choices) == 0: + continue + + # construct locally optimal model + performance_models = get_node_performance_models( + experiment, + graph, + function_node, + set(), + all_function_nodes, + restrict_to_decisions=set(locally_optimal_choices), + ignore_node_costs=[ + cast(FunctionRoot, data_at(graph, function_node)).node_id + ], # ignore first node to prevent duplication of function costs + ) + # calculate and append necessary data transfers to the models + performance_models_with_transfers = calculate_data_transfers( + graph, {cast(FunctionRoot, data_at(graph, function_node)): performance_models} + ) + + # calculate and append costs of data transfers to the performance models + complete_performance_models = add_data_transfer_costs( + graph, performance_models_with_transfers, environment + ) + # merge dictionaries + result_dict = {**result_dict, **complete_performance_models} + + return result_dict + + +def __find_decisions(graph: nx.DiGraph, root_node: int) -> Set[Tuple[int, ...]]: + result_set: Set[Tuple[int, ...]] = set() + successors = get_successors(graph, root_node) + children = get_children(graph, root_node) + + # check if children have decisions + for child in children: + result_set.update(__find_decisions(graph, child)) + + # check if current node has a decision + if len(successors) > 1: + local_decision = list() + for successor in successors: + local_decision.append(successor) + result_set.add(tuple(local_decision)) + + # check if successors have decisions + for successor in successors: + result_set.update(__find_decisions(graph, successor)) + + return result_set diff --git a/discopop_library/discopop_optimizer/utilities/optimization/LocalOptimization/__init__.py b/discopop_library/discopop_optimizer/utilities/optimization/LocalOptimization/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_library/discopop_optimizer/utilities/optimization/__init__.py b/discopop_library/discopop_optimizer/utilities/optimization/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_wizard/classes/ExecutionConfiguration.py b/discopop_wizard/classes/ExecutionConfiguration.py index b4e254e7f..ad977662e 100644 --- a/discopop_wizard/classes/ExecutionConfiguration.py +++ b/discopop_wizard/classes/ExecutionConfiguration.py @@ -18,6 +18,7 @@ import jsons # type:ignore from discopop_wizard.screens.execution import ExecutionView +from discopop_wizard.screens.optimizer.binding import create_optimizer_screen from discopop_wizard.screens.suggestions.overview import ( show_suggestions_overview_screen, get_suggestion_objects, @@ -43,7 +44,7 @@ def __init__(self, wizard): "notes": "", "working_copy_path": "", "tags": "", - "explorer_flags": "--json=patterns.json", + "explorer_flags": "--json=patterns.json --dump-detection-result", } self.value_dict["id"] = "".join(random.choices(string.ascii_uppercase + string.digits, k=8)) self.wizard = wizard @@ -131,8 +132,14 @@ def highlight_and_update_notebook_screens( main_screen_obj.notebook.tab( main_screen_obj.results_frame, state=self.__button_state_from_result_existence() ) + main_screen_obj.notebook.tab( + main_screen_obj.optimizer_frame, state=self.__button_state_from_result_existence() + ) + if self.__button_state_from_result_existence() == "normal": show_suggestions_overview_screen(wizard, main_screen_obj.results_frame, self) + if self.__button_state_from_result_existence() == "normal": + create_optimizer_screen(wizard, main_screen_obj.optimizer_frame, self) def show_details_screen(self, wizard, main_screen_obj): # delete previous frame contents @@ -487,8 +494,14 @@ def execute_configuration( main_screen_obj.notebook.tab( main_screen_obj.results_frame, state=self.__button_state_from_result_existence() ) + main_screen_obj.notebook.tab( + main_screen_obj.optimizer_frame, state=self.__button_state_from_result_existence() + ) + # show results tab main_screen_obj.notebook.select(main_screen_obj.results_frame) + # initialize optimizer tab + create_optimizer_screen(wizard, main_screen_obj.optimizer_frame, self) def get_tags(self) -> List[str]: """Returns a list of strings which represents the tags assigned to the configuration.""" diff --git a/discopop_wizard/screens/main.py b/discopop_wizard/screens/main.py index 8853d2675..aaab6ae55 100644 --- a/discopop_wizard/screens/main.py +++ b/discopop_wizard/screens/main.py @@ -42,8 +42,10 @@ def push_main_screen(self, wizard, window_frame: tk.Frame): self.details_frame = tk.Frame(self.notebook) self.results_frame = tk.Frame(self.notebook) + self.optimizer_frame = tk.Frame(self.notebook) self.notebook.add(self.details_frame, text="Details") self.notebook.add(self.results_frame, text="Results") + self.notebook.add(self.optimizer_frame, text="Optimizer (experimental)") self.build_configurations_frame(wizard) diff --git a/discopop_wizard/screens/optimizer/__init__.py b/discopop_wizard/screens/optimizer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/discopop_wizard/screens/optimizer/binding.py b/discopop_wizard/screens/optimizer/binding.py new file mode 100644 index 000000000..d4afbf9d6 --- /dev/null +++ b/discopop_wizard/screens/optimizer/binding.py @@ -0,0 +1,148 @@ +# This file is part of the DiscoPoP software (http://www.discopop.tu-darmstadt.de) +# +# Copyright (c) 2020, Technische Universitaet Darmstadt, Germany +# +# This software may be modified and distributed under the terms of +# the 3-Clause BSD License. See the LICENSE file in the package base +# directory for details. +import tkinter as tk +from tkinter import filedialog + +from discopop_library.PathManagement.PathManagement import get_path +from discopop_library.discopop_optimizer.__main__ import start_optimizer + + +def create_optimizer_screen(wizard, parent_frame, execution_configuration): + # close elements on optimizer_frame + for c in parent_frame.winfo_children(): + c.destroy() + + canvas = tk.Canvas(parent_frame) + canvas.pack(fill=tk.BOTH) + + arguments = dict() + + def overwrite_with_file_selection(target: tk.Entry): + prompt_result = filedialog.askopenfilename() + if len(prompt_result) != 0: + target.delete(0, tk.END) + target.insert(0, prompt_result) + + ### + tk.Label( + canvas, + text="Compile_command", + justify=tk.RIGHT, + anchor="e", + font=wizard.style_font_bold_small, + ).grid(row=1, column=0, sticky="ew") + compile_command = tk.Entry(canvas, width=100) + compile_command.insert(tk.END, "make") + compile_command.grid(row=1, column=1, sticky="ew") + ### + tk.Label( + canvas, + text="DoAll microbench file", + justify=tk.RIGHT, + anchor="e", + font=wizard.style_font_bold_small, + ).grid(row=2, column=0, sticky="ew") + doall_microbench_file = tk.Entry(canvas, width=100) + doall_microbench_file.insert(tk.END, "None") + doall_microbench_file.grid(row=2, column=1, sticky="ew") + + doall_microbench_file_path_selector = tk.Button( + canvas, text="Select", command=lambda: overwrite_with_file_selection(doall_microbench_file) + ) + doall_microbench_file_path_selector.grid(row=2, column=3) + ### + tk.Label( + canvas, + text="Reduction microbench file", + justify=tk.RIGHT, + anchor="e", + font=wizard.style_font_bold_small, + ).grid(row=3, column=0, sticky="ew") + reduction_microbench_file = tk.Entry(canvas, width=100) + reduction_microbench_file.insert(tk.END, "None") + reduction_microbench_file.grid(row=3, column=1, sticky="ew") + + reduction_microbench_file_path_selector = tk.Button( + canvas, + text="Select", + command=lambda: overwrite_with_file_selection(reduction_microbench_file), + ) + reduction_microbench_file_path_selector.grid(row=3, column=3) + ### + tk.Label( + canvas, + text="Exhaustive search", + justify=tk.RIGHT, + anchor="e", + font=wizard.style_font_bold_small, + ).grid(row=4, column=0, sticky="ew") + exhaustive_search = tk.IntVar(canvas) + exhaustive_search.set(0) + cb = tk.Checkbutton(canvas, onvalue=1, offvalue=0, variable=exhaustive_search) + cb.grid(row=4, column=1) + + start_button = tk.Button( + canvas, + text="Start Optimizer for " + execution_configuration.value_dict["label"], + command=lambda: __start_optimizer( + execution_configuration, + compile_command, + doall_microbench_file, + reduction_microbench_file, + exhaustive_search, + parent_frame, + ), + ) + start_button.grid(row=5, column=0) + + +def __start_optimizer( + execution_configuration, + compile_command, + doall_microbench_file, + reduction_microbench_file, + exhaustive_search, + parent_frame, +): + arguments = { + "--project": execution_configuration.value_dict["project_path"], + "--detection-result-dump": get_path( + execution_configuration.value_dict["working_copy_path"], "detection_result_dump.json" + ), + "--execute-created-models": False, + "--clean-created-code": False, + "--code-export-path": get_path( + execution_configuration.value_dict["project_path"], ".discopop_optimizer/code_exports" + ), + "--dp-output-path": execution_configuration.value_dict["working_copy_path"], + "--file-mapping": get_path( + execution_configuration.value_dict["working_copy_path"], "FileMapping.txt" + ), + "--executable-arguments": execution_configuration.value_dict["executable_arguments"], + "--executable-name": execution_configuration.value_dict["executable_name"], + "--linker-flags": execution_configuration.value_dict["linker_flags"], + "--make-target": execution_configuration.value_dict["make_target"], + "--make-flags": execution_configuration.value_dict["make_flags"], + "--execution-repetitions": 1, + "--execute-single-model": False, + "--compile-command": compile_command.get(), + "--execution-append-measurements": False, + "--exhaustive-search": True if exhaustive_search.get() == 1 else False, + "--headless-mode": False, + "--doall-microbench-file": doall_microbench_file.get(), + "--reduction-microbench-file": reduction_microbench_file.get(), + "--dp-optimizer-path": get_path( + execution_configuration.value_dict["project_path"], ".discopop_optimizer" + ), + } + + # close elements on optimizer_frame + for c in parent_frame.winfo_children(): + c.destroy() + + start_optimizer(arguments, parent_frame=parent_frame) diff --git a/scripts/runDiscoPoP b/scripts/runDiscoPoP index cbf7c12ab..f092ea6ea 100755 --- a/scripts/runDiscoPoP +++ b/scripts/runDiscoPoP @@ -300,7 +300,8 @@ log -i \ makeTarget: $MAKEFILE_TARGET dp_build: $DP_BUILD gllvmLog: $GLLVM_LOG - pythonPath: $PYTHONPATH_FLAG" + pythonPath: $PYTHONPATH_FLAG + explorer_flags: $EXPLORER_FLAGS" ##### # configure gllvm