diff --git a/scripts/examples/basic_example/brute_force_gantt.png b/scripts/examples/basic_example/brute_force_gantt.png index 5ad97049..beebb943 100644 Binary files a/scripts/examples/basic_example/brute_force_gantt.png and b/scripts/examples/basic_example/brute_force_gantt.png differ diff --git a/scripts/examples/basic_example/brute_force_gantt_scaled.png b/scripts/examples/basic_example/brute_force_gantt_scaled.png index 0944f421..4235e862 100644 Binary files a/scripts/examples/basic_example/brute_force_gantt_scaled.png and b/scripts/examples/basic_example/brute_force_gantt_scaled.png differ diff --git a/scripts/examples/basic_example/gantt.png b/scripts/examples/basic_example/gantt.png index b3342301..b8682d11 100644 Binary files a/scripts/examples/basic_example/gantt.png and b/scripts/examples/basic_example/gantt.png differ diff --git a/scripts/examples/basic_example/gantt_scaled.png b/scripts/examples/basic_example/gantt_scaled.png index b3342301..b8682d11 100644 Binary files a/scripts/examples/basic_example/gantt_scaled.png and b/scripts/examples/basic_example/gantt_scaled.png differ diff --git a/scripts/examples/basic_example/heft_gantt.png b/scripts/examples/basic_example/heft_gantt.png index b44679af..b1bcddc6 100644 Binary files a/scripts/examples/basic_example/heft_gantt.png and b/scripts/examples/basic_example/heft_gantt.png differ diff --git a/scripts/examples/basic_example/heft_gantt_scaled.png b/scripts/examples/basic_example/heft_gantt_scaled.png index 9d98f146..554ed656 100644 Binary files a/scripts/examples/basic_example/heft_gantt_scaled.png and b/scripts/examples/basic_example/heft_gantt_scaled.png differ diff --git a/scripts/examples/basic_example/network.png b/scripts/examples/basic_example/network.png index 178d5433..3d168103 100644 Binary files a/scripts/examples/basic_example/network.png and b/scripts/examples/basic_example/network.png differ diff --git a/scripts/examples/basic_example/task_graph.png b/scripts/examples/basic_example/task_graph.png index 49df29be..35fa96c7 100644 Binary files a/scripts/examples/basic_example/task_graph.png and b/scripts/examples/basic_example/task_graph.png differ diff --git a/scripts/examples/dynamic_task_graph/BIL_schedule.png b/scripts/examples/dynamic_task_graph/BIL_schedule.png new file mode 100644 index 00000000..c4b1b17e Binary files /dev/null and b/scripts/examples/dynamic_task_graph/BIL_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/CPOP_schedule.png b/scripts/examples/dynamic_task_graph/CPOP_schedule.png new file mode 100644 index 00000000..6ae457e7 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/CPOP_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/DPS_schedule.png b/scripts/examples/dynamic_task_graph/DPS_schedule.png new file mode 100644 index 00000000..6ae457e7 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/DPS_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/Duplex_schedule.png b/scripts/examples/dynamic_task_graph/Duplex_schedule.png new file mode 100644 index 00000000..bc35cfa6 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/Duplex_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/ETF_schedule.png b/scripts/examples/dynamic_task_graph/ETF_schedule.png new file mode 100644 index 00000000..f9e4eac3 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/ETF_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/FCP_schedule.png b/scripts/examples/dynamic_task_graph/FCP_schedule.png new file mode 100644 index 00000000..e28af565 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/FCP_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/FLB_schedule.png b/scripts/examples/dynamic_task_graph/FLB_schedule.png new file mode 100644 index 00000000..8c8ff74f Binary files /dev/null and b/scripts/examples/dynamic_task_graph/FLB_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/FastestNode_schedule.png b/scripts/examples/dynamic_task_graph/FastestNode_schedule.png new file mode 100644 index 00000000..6ae457e7 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/FastestNode_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/GDL_schedule.png b/scripts/examples/dynamic_task_graph/GDL_schedule.png new file mode 100644 index 00000000..8d2272d4 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/GDL_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/HBMCT_schedule.png b/scripts/examples/dynamic_task_graph/HBMCT_schedule.png new file mode 100644 index 00000000..0792daa1 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/HBMCT_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/HEFT_schedule.png b/scripts/examples/dynamic_task_graph/HEFT_schedule.png new file mode 100644 index 00000000..6ae457e7 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/HEFT_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/MCT_schedule.png b/scripts/examples/dynamic_task_graph/MCT_schedule.png new file mode 100644 index 00000000..6ae457e7 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/MCT_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/MET_schedule.png b/scripts/examples/dynamic_task_graph/MET_schedule.png new file mode 100644 index 00000000..6ae457e7 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/MET_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/MSBC_schedule.png b/scripts/examples/dynamic_task_graph/MSBC_schedule.png new file mode 100644 index 00000000..e97f6a3b Binary files /dev/null and b/scripts/examples/dynamic_task_graph/MSBC_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/MaxMin_schedule.png b/scripts/examples/dynamic_task_graph/MaxMin_schedule.png new file mode 100644 index 00000000..7755aa4c Binary files /dev/null and b/scripts/examples/dynamic_task_graph/MaxMin_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/MinMin_schedule.png b/scripts/examples/dynamic_task_graph/MinMin_schedule.png new file mode 100644 index 00000000..bc35cfa6 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/MinMin_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/OLB_schedule.png b/scripts/examples/dynamic_task_graph/OLB_schedule.png new file mode 100644 index 00000000..7bdd5a3c Binary files /dev/null and b/scripts/examples/dynamic_task_graph/OLB_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_BIL_schedule.png b/scripts/examples/dynamic_task_graph/R_BIL_schedule.png new file mode 100644 index 00000000..11137899 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_BIL_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_CPOP_schedule.png b/scripts/examples/dynamic_task_graph/R_CPOP_schedule.png new file mode 100644 index 00000000..2d8e707c Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_CPOP_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_DPS_schedule.png b/scripts/examples/dynamic_task_graph/R_DPS_schedule.png new file mode 100644 index 00000000..225b9d7f Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_DPS_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_Duplex_schedule.png b/scripts/examples/dynamic_task_graph/R_Duplex_schedule.png new file mode 100644 index 00000000..cf52a55b Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_Duplex_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_ETF_schedule.png b/scripts/examples/dynamic_task_graph/R_ETF_schedule.png new file mode 100644 index 00000000..df76e535 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_ETF_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_FCP_schedule.png b/scripts/examples/dynamic_task_graph/R_FCP_schedule.png new file mode 100644 index 00000000..87bf248a Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_FCP_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_FLB_schedule.png b/scripts/examples/dynamic_task_graph/R_FLB_schedule.png new file mode 100644 index 00000000..2a8d14af Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_FLB_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_FastestNode_schedule.png b/scripts/examples/dynamic_task_graph/R_FastestNode_schedule.png new file mode 100644 index 00000000..929fb483 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_FastestNode_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_GDL_schedule.png b/scripts/examples/dynamic_task_graph/R_GDL_schedule.png new file mode 100644 index 00000000..8d2272d4 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_GDL_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_HBMCT_schedule.png b/scripts/examples/dynamic_task_graph/R_HBMCT_schedule.png new file mode 100644 index 00000000..a8a897fc Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_HBMCT_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_HEFT_schedule.png b/scripts/examples/dynamic_task_graph/R_HEFT_schedule.png new file mode 100644 index 00000000..225b9d7f Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_HEFT_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_MCT_schedule.png b/scripts/examples/dynamic_task_graph/R_MCT_schedule.png new file mode 100644 index 00000000..cf52a55b Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_MCT_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_MET_schedule.png b/scripts/examples/dynamic_task_graph/R_MET_schedule.png new file mode 100644 index 00000000..929fb483 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_MET_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_MSBC_schedule.png b/scripts/examples/dynamic_task_graph/R_MSBC_schedule.png new file mode 100644 index 00000000..213ea0ad Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_MSBC_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_MaxMin_schedule.png b/scripts/examples/dynamic_task_graph/R_MaxMin_schedule.png new file mode 100644 index 00000000..cf52a55b Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_MaxMin_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_MinMin_schedule.png b/scripts/examples/dynamic_task_graph/R_MinMin_schedule.png new file mode 100644 index 00000000..cfe6beef Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_MinMin_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_OLB_schedule.png b/scripts/examples/dynamic_task_graph/R_OLB_schedule.png new file mode 100644 index 00000000..81838d1f Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_OLB_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_Sufferage_schedule.png b/scripts/examples/dynamic_task_graph/R_Sufferage_schedule.png new file mode 100644 index 00000000..b867ced7 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_Sufferage_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/R_WBA_schedule.png b/scripts/examples/dynamic_task_graph/R_WBA_schedule.png new file mode 100644 index 00000000..b66a4e34 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/R_WBA_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/Sufferage_schedule.png b/scripts/examples/dynamic_task_graph/Sufferage_schedule.png new file mode 100644 index 00000000..7755aa4c Binary files /dev/null and b/scripts/examples/dynamic_task_graph/Sufferage_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/WBA_schedule.png b/scripts/examples/dynamic_task_graph/WBA_schedule.png new file mode 100644 index 00000000..ab0c7fbb Binary files /dev/null and b/scripts/examples/dynamic_task_graph/WBA_schedule.png differ diff --git a/scripts/examples/dynamic_task_graph/combined_task_graph.png b/scripts/examples/dynamic_task_graph/combined_task_graph.png new file mode 100644 index 00000000..1d44d739 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/combined_task_graph.png differ diff --git a/scripts/examples/dynamic_task_graph/main.py b/scripts/examples/dynamic_task_graph/main.py new file mode 100644 index 00000000..ca996be1 --- /dev/null +++ b/scripts/examples/dynamic_task_graph/main.py @@ -0,0 +1,499 @@ +import random +import numpy as np +from typing import Tuple, Dict, List, Hashable, Optional, Set +import networkx as nx +import pandas as pd +from saga.scheduler import Task, Scheduler +from saga.schedulers.cpop import CpopScheduler +from saga.schedulers.heft import HeftScheduler +from saga.schedulers.bil import BILScheduler +from saga.schedulers.brute_force import BruteForceScheduler +from saga.schedulers.dps import DPSScheduler +from saga.schedulers.minmin import MinMinScheduler +from saga.schedulers.maxmin import MaxMinScheduler +from saga.schedulers.duplex import DuplexScheduler +from saga.schedulers.etf import ETFScheduler +from saga.schedulers.fastest_node import FastestNodeScheduler +from saga.schedulers.fcp import FCPScheduler +from saga.schedulers.flb import FLBScheduler +from saga.schedulers.gdl import GDLScheduler +from saga.schedulers.hbmct import HbmctScheduler +from saga.schedulers.mct import MCTScheduler +from saga.schedulers.met import METScheduler +from saga.schedulers.msbc import MsbcScheduler +from saga.schedulers.olb import OLBScheduler +from saga.schedulers.sufferage import SufferageScheduler +from saga.schedulers.wba import WBAScheduler + +from saga.schedulers.dynamic_task_graph.residual.rheft import ResidualHeftScheduler +# from saga.schedulers.dynamic_task_graph.baseline.heft import BaselineHeftScheduler +from saga.schedulers.dynamic_task_graph.residual.rbil import ResidualBILScheduler +from saga.schedulers.dynamic_task_graph.residual.rbrute_force import ResidualBruteForceScheduler +from saga.schedulers.dynamic_task_graph.residual.rcpop import ResidualCpopScheduler +from saga.schedulers.dynamic_task_graph.residual.rdps import ResidualDPSScheduler +from saga.schedulers.dynamic_task_graph.residual.rminmin import ResidualMinMinScheduler +from saga.schedulers.dynamic_task_graph.residual.rmaxmin import ResidualMaxMinScheduler +from saga.schedulers.dynamic_task_graph.residual.rduplex import ResidualDuplexScheduler +from saga.schedulers.dynamic_task_graph.residual.retf import ResidualETFScheduler +from saga.schedulers.dynamic_task_graph.residual.rfastest_node import ResidualFastestNodeScheduler +from saga.schedulers.dynamic_task_graph.residual.rfcp import ResidualFCPScheduler +from saga.schedulers.dynamic_task_graph.residual.rflb import ResidualFLBScheduler +from saga.schedulers.dynamic_task_graph.residual.rgdl import ResidualGDLScheduler +from saga.schedulers.dynamic_task_graph.residual.rhbmct import ResidualHbmctScheduler +from saga.schedulers.dynamic_task_graph.residual.rmct import ResidualMCTScheduler +from saga.schedulers.dynamic_task_graph.residual.rmet import ResidualMETScheduler +from saga.schedulers.dynamic_task_graph.residual.rmsbc import ResidualMsbcScheduler +from saga.schedulers.dynamic_task_graph.residual.rolb import ResidualOLBScheduler +from saga.schedulers.dynamic_task_graph.residual.rsufferage import ResidualSufferageScheduler +from saga.schedulers.dynamic_task_graph.residual.rwba import ResidualWBAScheduler + +from saga.utils.draw import draw_gantt, draw_network, draw_task_graph +from saga.utils.tools import get_insert_loc +from saga.utils.random_graphs import ( + get_branching_dag, get_chain_dag, get_diamond_dag, get_fork_dag, + get_network, add_random_weights +) +import pathlib +import plotly.express as px +import matplotlib.pyplot as plt +import logging + + +from abc import ABC, abstractmethod + + +logging.basicConfig(level=logging.INFO) + + + +thisdir = pathlib.Path(__file__).parent.absolute() + + + +class ResidualWrapper(ABC): + def schedule( + self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + scheduler: Scheduler + ) -> Dict[str, List[Task]]: + comp_schedule: Dict[Hashable, List[Task]] + + for idx, task_graph_tupple in enumerate(task_graphs): + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + if idx <= 0: + comp_schedule = scheduler.schedule(network, task_graph) + else: + comp_schedule = scheduler.schedule(network, task_graph, comp_schedule, task_graph_arrival_time) + + return comp_schedule + + +# this wrapper is ignorant of the previous task graphs +class HEFTResidualWrapper(ABC): + def schedule( + self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + scheduler: Scheduler + ) -> Dict[str, List[Task]]: + final_schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + + # create a new list for schedule of each task graph + comp_schdeules: List[Dict[str, List[Task]]] = [] + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + # print(f'task_graph: {task_graph}') + comp_schdeule = scheduler.schedule(network, task_graph) + comp_schdeules.append(comp_schdeule) + # print(f'comp_schdeule: {comp_schdeule}') + + # print(f'Length of comp_schdeules: {len(comp_schdeules)}') + # print(f'comp_schdeules: {comp_schdeules}') + + # combine the schedules + for idx, task_graph_tupple in enumerate(task_graphs): + comp_schdeule = comp_schdeules[idx] + # creat a list of all tasks from comp_schedule values with type List[Task] + all_tasks = [task for task_list in comp_schdeule.values() for task in task_list] + for task in all_tasks: + # print(f'task: {task}') + _ , start_time = get_insert_loc( + final_schedule[task.node], + task.start + task_graph_tupple[1], + task.end - task.start + ) + final_schedule[task.node].append(Task(task.node, task.name, start_time, start_time + task.end - task.start)) + + return final_schedule + + + +class CumulativeWrapper(ABC): + + def schedule( + self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + scheduler: Scheduler + ) -> Dict[str, List[Task]]: + comp_schedule: Dict[str, List[Task]] = {} + for idx, task_graph_tupple in enumerate(task_graphs): + # print(f'idx: {idx}') + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + combined_task_graph = nx.DiGraph() + + for i in range(idx + 1): + combined_task_graph = nx.compose(combined_task_graph, task_graphs[i][0]) + + + # if task_graph_arrival_time > 0: + # for task_list in comp_schedule.values(): + # for task in task_list: + # print(f'task {task.name} with start: {task.start}, end: {task.end}') + # if task.end > task_graph_arrival_time: + # task_list.remove(task) + # print(f'task {task.name} with start: {task.start}, end: {task.end} needs to be resheduled') + + if task_graph_arrival_time > 0: + # Flatten all tasks into a single list + all_tasks = [(key, task) for key, task_list in comp_schedule.items() for task in task_list] + + # Create a new schedule after filtering + updated_schedule = {key: [] for key in comp_schedule} # Initialize an empty schedule + for key, task in all_tasks: + # print(f'Task {task.name} with start: {task.start}, end: {task.end}') + if task.end <= task_graph_arrival_time: + # Retain the task if it ends before the graph arrival time + updated_schedule[key].append(task) + if task.name in combined_task_graph: + combined_task_graph.nodes[task.name]['weight'] = 0 + else: + pass + # print(f'Task {task.name} with start: {task.start}, end: {task.end} needs to be rescheduled') + + # Update the original schedule with the filtered tasks + comp_schedule = updated_schedule + + + for node, attributes in combined_task_graph.nodes(data=True): + weight = attributes.get('weight', None) # Get the 'weight' attribute, default to None if not set + # print(f"Node: {node}, Weight: {weight}") + + + + if idx <= 0: + comp_schedule = scheduler.schedule(network, task_graph) + else: + comp_schedule = scheduler.schedule(network, combined_task_graph, comp_schedule, task_graph_arrival_time) + + # print(f'idx: {idx} comp_schedule: {comp_schedule}') + + # print(f'final comp_schedule: {comp_schedule}') + + return comp_schedule + + + + + + +def get_random_network() -> nx.Graph: + network = get_network(num_nodes=random.randint(3, 8)) + # add equal weights to egdes of the network + # weight_range = (1, 10) + # for node in network.nodes: + # network.nodes[node]["weight"] = np.random.uniform(*weight_range) + + # for edge in network.edges: + # if not network.is_directed() and edge[0] == edge[1]: + # network.edges[edge]["weight"] = 1e9 * weight_range[1] # very large communication speed + # else: + # network.edges[edge]["weight"] = 1 + + network = add_random_weights(network) + return network + +def get_random_task_graph() -> nx.DiGraph: + choice = random.choice(["chain", "fork", "diamond", "branching"]) + # choice = random.choice(["branching"]) + + if choice == "chain": + task_graph = get_chain_dag( + # num_nodes=random.randint(2, 2) + num_nodes=random.randint(5, 10) + ) + elif choice == "fork": + task_graph = get_fork_dag() + elif choice == "diamond": + task_graph = get_diamond_dag() + elif choice == "branching": + task_graph = get_branching_dag( + levels=random.randint(2,4), + branching_factor=random.randint(2,4) + ) + + add_random_weights(task_graph) + + return task_graph + +def draw_instance(network: nx.Graph, task_graph: nx.DiGraph): + logging.basicConfig(level=logging.INFO) + ax: plt.Axes = draw_task_graph(task_graph, use_latex=True) + ax.get_figure().savefig(str(thisdir / 'task_graph.png')) + + ax: plt.Figure = draw_network(network, draw_colors=False, use_latex=True) + ax.get_figure().savefig(str(thisdir / 'network.png')) + +def draw_my_network(network: nx.Graph, name: str): + ax: plt.Figure = draw_network(network, draw_colors=False, use_latex=True) + ax.get_figure().savefig(str(thisdir / f'{name}.png')) + +def draw_my_task_graph(task_graph: nx.DiGraph, name: str): + ax: plt.Axes = draw_task_graph(task_graph, use_latex=True) + ax.get_figure().savefig(str(thisdir / f'{name}.png')) + +def draw_schedule(schedule: Dict[str, List[Task]], name: str, xmax: float = None): + ax: plt.Axes = draw_gantt(schedule, use_latex=True, xmax=xmax) + ax.get_figure().savefig(str(thisdir / f'{name}.png')) + +def main(): + # forth try + + network = get_random_network() + + num_task_graphs = 3 + task_graphs = [] + + for i in range(num_task_graphs): + task_graph = get_random_task_graph() + new_node_names = {node: f"{node}_{i}" for node in task_graph.nodes} + nx.relabel_nodes(task_graph, new_node_names, copy=False) + draw_my_task_graph(task_graph, f"task_graph_{i}") + task_graphs.append((task_graph, 0.5 * i)) + + + draw_my_network(network, "network") + + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + for node in task_graph.nodes: + task_graph.nodes[node]["arrival_time"] = task_graph_arrival_time + + # join all task graphs to create a combined task graph + combined_task_graph = nx.DiGraph() + for task_graph_tupple in task_graphs: + combined_task_graph = nx.compose(combined_task_graph, task_graph_tupple[0]) + + draw_my_task_graph(combined_task_graph, "combined_task_graph") + + + + # residual schedulers + residualWBAScheduler = ResidualWBAScheduler() + residualSufferageScheduler = ResidualSufferageScheduler() + residualOLBScheduler = ResidualOLBScheduler() + residualMsbcScheduler = ResidualMsbcScheduler() + residualMETScheduler = ResidualMETScheduler() + residualMCTScheduler = ResidualMCTScheduler() + residualHbmctScheduler = ResidualHbmctScheduler() + residualGDLScheduler = ResidualGDLScheduler() + residualFLBScheduler = ResidualFLBScheduler() + residualHeftScheduler = ResidualHeftScheduler() + residualBILScheduler = ResidualBILScheduler() + residualCpopScheduler = ResidualCpopScheduler() + residualDPSScheduler = ResidualDPSScheduler() + residualMinMinScheduler = ResidualMinMinScheduler() + residualMaxMinScheduler = ResidualMaxMinScheduler() + residualDuplexScheduler = ResidualDuplexScheduler() + residualETFScheduler = ResidualETFScheduler() + residualFastestNodeScheduler = ResidualFastestNodeScheduler() + residualFCPScheduler = ResidualFCPScheduler() + # residualBruteForceScheduler = ResidualBruteForceScheduler() + + + # non-residual schedulers + wbaScheduler = WBAScheduler() + sufferageScheduler = SufferageScheduler() + olbScheduler = OLBScheduler() + msbcScheduler = MsbcScheduler() + metScheduler = METScheduler() + mctScheduler = MCTScheduler() + hbmcScheduler = HbmctScheduler() + gdlscheduler = GDLScheduler() + flbScheduler = FLBScheduler() + heftScheduler = HeftScheduler() + bilScheduler = BILScheduler() + cpopScheduler = CpopScheduler() + dpScheduler = DPSScheduler() + minminScheduler = MinMinScheduler() + maxminScheduler = MaxMinScheduler() + duplexScheduler = DuplexScheduler() + etfScheduler = ETFScheduler() + fastestNodeScheduler = FastestNodeScheduler() + fcpScheduler = FCPScheduler() + + # bruteForceScheduler = BruteForceScheduler() + + + + # todo: implement baseline schedulers for comparison + # baselineHeftScheduler = BaselineHeftScheduler() + + + + + # residual schedulers + residualWBASchedule = residualWBAScheduler.schedule(network, task_graphs) + residualSufferageSchedule = residualSufferageScheduler.schedule(network, task_graphs) + residualOLBSchedule = residualOLBScheduler.schedule(network, task_graphs) + residualMsbcSchedule = residualMsbcScheduler.schedule(network, task_graphs) + residualMETSchedule = residualMETScheduler.schedule(network, task_graphs) + residualMCTSchedule = residualMCTScheduler.schedule(network, task_graphs) + residualHbmctSchedule = residualHbmctScheduler.schedule(network, task_graphs) + residualGDLSchedule = residualGDLScheduler.schedule(network, task_graphs) + residualHeftSchedule = residualHeftScheduler.schedule(network, task_graphs) + residualBILSchedule = residualBILScheduler.schedule(network, task_graphs) + residualCpopSchedule = residualCpopScheduler.schedule(network, task_graphs) + residualDPSSchedule = residualDPSScheduler.schedule(network, task_graphs) + residualMinMinSchedule = residualMinMinScheduler.schedule(network, task_graphs) + residualMaxMinSchedule = residualMaxMinScheduler.schedule(network, task_graphs) + residualDuplexSchedule = residualDuplexScheduler.schedule(network, task_graphs) + residualETFSchedule = residualETFScheduler.schedule(network, task_graphs) + residualFastestNodeSchedule = residualFastestNodeScheduler.schedule(network, task_graphs) + residualFCPSchedule = residualFCPScheduler.schedule(network, task_graphs) + residualFLBSchedule = residualFLBScheduler.schedule(network, task_graphs) + # residualBruteForceSchedule = residualBruteForceScheduler.schedule(network, task_graphs) + + # non-residual schedulers + wbaSchedule = wbaScheduler.schedule(network, task_graphs[0][0]) + sufferageSchedule = sufferageScheduler.schedule(network, task_graphs[0][0]) + olbSchedule = olbScheduler.schedule(network, task_graphs[0][0]) + msbcSchedule = msbcScheduler.schedule(network, task_graphs[0][0]) + metSchedule = metScheduler.schedule(network, task_graphs[0][0]) + mctSchedule = mctScheduler.schedule(network, task_graphs[0][0]) + hbmcSchedule = hbmcScheduler.schedule(network, task_graphs[0][0]) + gdlschedule = gdlscheduler.schedule(network, task_graphs[0][0]) + bilSchedule = bilScheduler.schedule(network, task_graphs[0][0]) + heftSchedule = heftScheduler.schedule(network, task_graphs[0][0]) + cpopSchedule = cpopScheduler.schedule(network, task_graphs[0][0]) + dpSchedule = dpScheduler.schedule(network, task_graphs[0][0]) + minminSchedule = minminScheduler.schedule(network, task_graphs[0][0]) + maxminSchedule = maxminScheduler.schedule(network, task_graphs[0][0]) + duplexSchedule = duplexScheduler.schedule(network, task_graphs[0][0]) + etfSchedule = etfScheduler.schedule(network, task_graphs[0][0]) + fastestNodeSchedule = fastestNodeScheduler.schedule(network, task_graphs[0][0]) + fcpSchedule = fcpScheduler.schedule(network, task_graphs[0][0]) + flbSchedule = flbScheduler.schedule(network, task_graphs[0][0]) + # bruteForceSchedule = bruteForceScheduler.schedule(network, task_graphs[0][0]) + + # baseline schedule + # baselineHeftSchedule = baselineHeftScheduler.schedule(network, combined_task_graph) + + + # draw residual schedules + draw_schedule(residualWBASchedule, "R_WBA_schedule") + draw_schedule(residualSufferageSchedule, "R_Sufferage_schedule") + draw_schedule(residualOLBSchedule, "R_OLB_schedule") + draw_schedule(residualMsbcSchedule, "R_MSBC_schedule") + draw_schedule(residualMETSchedule, "R_MET_schedule") + draw_schedule(residualMCTSchedule, "R_MCT_schedule") + draw_schedule(residualHbmctSchedule, "R_HBMCT_schedule") + draw_schedule(residualGDLSchedule, "R_GDL_schedule") + draw_schedule(residualHeftSchedule, "R_HEFT_schedule") + draw_schedule(residualBILSchedule, "R_BIL_schedule") + draw_schedule(residualCpopSchedule, "R_CPOP_schedule") + draw_schedule(residualDPSSchedule, "R_DPS_schedule") + draw_schedule(residualMinMinSchedule, "R_MinMin_schedule") + draw_schedule(residualMaxMinSchedule, "R_MaxMin_schedule") + draw_schedule(residualDuplexSchedule, "R_Duplex_schedule") + draw_schedule(residualETFSchedule, "R_ETF_schedule") + draw_schedule(residualFastestNodeSchedule, "R_FastestNode_schedule") + draw_schedule(residualFCPSchedule, "R_FCP_schedule") + draw_schedule(residualFLBSchedule, "R_FLB_schedule") + + # draw_schedule(residualBruteForceSchedule, "R_BF_schedule") + + # draw non-residual schedules + draw_schedule(wbaSchedule, "WBA_schedule") + draw_schedule(sufferageSchedule, "Sufferage_schedule") + draw_schedule(olbSchedule, "OLB_schedule") + draw_schedule(msbcSchedule, "MSBC_schedule") + draw_schedule(metSchedule, "MET_schedule") + draw_schedule(mctSchedule, "MCT_schedule") + draw_schedule(hbmcSchedule, "HBMCT_schedule") + draw_schedule(gdlschedule, "GDL_schedule") + draw_schedule(bilSchedule, "BIL_schedule") + draw_schedule(heftSchedule, "HEFT_schedule") + draw_schedule(cpopSchedule, "CPOP_schedule") + draw_schedule(dpSchedule, "DPS_schedule") + draw_schedule(minminSchedule, "MinMin_schedule") + draw_schedule(maxminSchedule, "MaxMin_schedule") + draw_schedule(duplexSchedule, "Duplex_schedule") + draw_schedule(etfSchedule, "ETF_schedule") + draw_schedule(fastestNodeSchedule, "FastestNode_schedule") + draw_schedule(fcpSchedule, "FCP_schedule") + draw_schedule(flbSchedule, "FLB_schedule") + # draw_schedule(bruteForceSchedule, "BF_schedule") + + + # baseline schedule + # draw_schedule(baselineHeftSchedule, "baseline_HEFT_schedule") + + # print maximum makespan for each scheduler + print(f'Residual HEFT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualHeftSchedule.values()])}') + print(f'Residual BIL Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualBILSchedule.values()])}') + print(f'Residual CPOP Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualCpopSchedule.values()])}') + print(f'Residual DPS Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualDPSSchedule.values()])}') + print(f'Residual MinMin Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualMinMinSchedule.values()])}') + print(f'Residual MaxMin Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualMaxMinSchedule.values()])}') + print(f'Residual Duplex Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualDuplexSchedule.values()])}') + print(f'Residual ETF Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualETFSchedule.values()])}') + print(f'Residual FastestNode Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualFastestNodeSchedule.values()])}') + print(f'Residual FCP Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualFCPSchedule.values()])}') + print(f'Residual FLB Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualFLBSchedule.values()])}') + print(f'Residual GDL Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualGDLSchedule.values()])}') + print(f'Residual HBMCT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualHbmctSchedule.values()])}') + print(f'Residual MCT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualMCTSchedule.values()])}') + print(f'Residual MET Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualMETSchedule.values()])}') + print(f'Residual MSBC Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualMsbcSchedule.values()])}') + print(f'Residual OLB Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualOLBSchedule.values()])}') + print(f'Residual Sufferage Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualSufferageSchedule.values()])}') + print(f'Residual WBA Makespan: {max([0 if not tasks else tasks[-1].end for tasks in residualWBASchedule.values()])}') + + # print makespan for each non-residual scheduler + print(f'HEFT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in heftSchedule.values()])}') + print(f'BIL Makespan: {max([0 if not tasks else tasks[-1].end for tasks in bilSchedule.values()])}') + print(f'CPOP Makespan: {max([0 if not tasks else tasks[-1].end for tasks in cpopSchedule.values()])}') + print(f'DPS Makespan: {max([0 if not tasks else tasks[-1].end for tasks in dpSchedule.values()])}') + print(f'MinMin Makespan: {max([0 if not tasks else tasks[-1].end for tasks in minminSchedule.values()])}') + print(f'MaxMin Makespan: {max([0 if not tasks else tasks[-1].end for tasks in maxminSchedule.values()])}') + print(f'Duplex Makespan: {max([0 if not tasks else tasks[-1].end for tasks in duplexSchedule.values()])}') + print(f'ETF Makespan: {max([0 if not tasks else tasks[-1].end for tasks in etfSchedule.values()])}') + print(f'FastestNode Makespan: {max([0 if not tasks else tasks[-1].end for tasks in fastestNodeSchedule.values()])}') + print(f'FCP Makespan: {max([0 if not tasks else tasks[-1].end for tasks in fcpSchedule.values()])}') + print(f'FLB Makespan: {max([0 if not tasks else tasks[-1].end for tasks in flbSchedule.values()])}') + print(f'GDL Makespan: {max([0 if not tasks else tasks[-1].end for tasks in gdlschedule.values()])}') + print(f'HBMCT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in hbmcSchedule.values()])}') + print(f'MCT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in mctSchedule.values()])}') + print(f'MET Makespan: {max([0 if not tasks else tasks[-1].end for tasks in metSchedule.values()])}') + print(f'MSBC Makespan: {max([0 if not tasks else tasks[-1].end for tasks in msbcSchedule.values()])}') + print(f'OLB Makespan: {max([0 if not tasks else tasks[-1].end for tasks in olbSchedule.values()])}') + print(f'Sufferage Makespan: {max([0 if not tasks else tasks[-1].end for tasks in sufferageSchedule.values()])}') + print(f'WBA Makespan: {max([0 if not tasks else tasks[-1].end for tasks in wbaSchedule.values()])}') + + # baseline makespan + # print(f'Baseline HEFT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in baselineHeftSchedule.values()])}') + + + +if __name__ == '__main__': + main() + diff --git a/scripts/examples/dynamic_task_graph/network.png b/scripts/examples/dynamic_task_graph/network.png new file mode 100644 index 00000000..91047290 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/network.png differ diff --git a/scripts/examples/dynamic_task_graph/task_graph_0.png b/scripts/examples/dynamic_task_graph/task_graph_0.png new file mode 100644 index 00000000..aaafab51 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/task_graph_0.png differ diff --git a/scripts/examples/dynamic_task_graph/task_graph_1.png b/scripts/examples/dynamic_task_graph/task_graph_1.png new file mode 100644 index 00000000..07df9e45 Binary files /dev/null and b/scripts/examples/dynamic_task_graph/task_graph_1.png differ diff --git a/scripts/examples/dynamic_task_graph/task_graph_2.png b/scripts/examples/dynamic_task_graph/task_graph_2.png new file mode 100644 index 00000000..2c4919aa Binary files /dev/null and b/scripts/examples/dynamic_task_graph/task_graph_2.png differ diff --git a/scripts/examples/parametric_residual/cumulative_schedule_1.png b/scripts/examples/parametric_residual/cumulative_schedule_1.png new file mode 100644 index 00000000..0df2105d Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_schedule_1.png differ diff --git a/scripts/examples/parametric_residual/cumulative_schedule_2.png b/scripts/examples/parametric_residual/cumulative_schedule_2.png new file mode 100644 index 00000000..ef49e383 Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_schedule_2.png differ diff --git a/scripts/examples/parametric_residual/cumulative_schedule_3.png b/scripts/examples/parametric_residual/cumulative_schedule_3.png new file mode 100644 index 00000000..60f9cb11 Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_schedule_3.png differ diff --git a/scripts/examples/parametric_residual/cumulative_schedule_4.png b/scripts/examples/parametric_residual/cumulative_schedule_4.png new file mode 100644 index 00000000..7b186e3f Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_schedule_4.png differ diff --git a/scripts/examples/parametric_residual/cumulative_task_graph_1.png b/scripts/examples/parametric_residual/cumulative_task_graph_1.png new file mode 100644 index 00000000..340cdd71 Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_task_graph_1.png differ diff --git a/scripts/examples/parametric_residual/cumulative_task_graph_2.png b/scripts/examples/parametric_residual/cumulative_task_graph_2.png new file mode 100644 index 00000000..ca883e01 Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_task_graph_2.png differ diff --git a/scripts/examples/parametric_residual/cumulative_task_graph_3.png b/scripts/examples/parametric_residual/cumulative_task_graph_3.png new file mode 100644 index 00000000..f20c67fb Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_task_graph_3.png differ diff --git a/scripts/examples/parametric_residual/cumulative_task_graph_4.png b/scripts/examples/parametric_residual/cumulative_task_graph_4.png new file mode 100644 index 00000000..2416b9b4 Binary files /dev/null and b/scripts/examples/parametric_residual/cumulative_task_graph_4.png differ diff --git a/scripts/examples/parametric_residual/main.py b/scripts/examples/parametric_residual/main.py new file mode 100644 index 00000000..72022077 --- /dev/null +++ b/scripts/examples/parametric_residual/main.py @@ -0,0 +1,220 @@ +from matplotlib import pyplot as plt +from saga.schedulers.parametric import ParametricScheduler +from saga.schedulers.parametric.components import GreedyInsert, UpwardRanking, ScheduleType + +from saga.utils.random_graphs import get_network, get_branching_dag, add_ccr_weights, add_random_weights +from saga.utils.draw import draw_gantt, draw_network, draw_task_graph + +import pathlib +import networkx as nx + +from typing import List, Tuple + +thisdir = pathlib.Path(__file__).resolve().parent + + + +def residual(network: nx.Graph, task_graphs: nx.DiGraph, arrival_times: List[float]) -> ScheduleType: + scheduler = ParametricScheduler( + initial_priority=UpwardRanking(), + insert_task=GreedyInsert( + append_only=False, + compare="EFT", + critical_path=False, + ), + cumulative=False + ) + + schedule: ScheduleType = {node: [] for node in network.nodes} + for i, (arrival_time, task_graph) in enumerate(zip(arrival_times, task_graphs), start=1): + schedule = scheduler.schedule( + network=network, + task_graph=task_graph, + schedule=schedule, + min_start_time=arrival_time + ) + + ax_task_graph: plt.Axes = draw_task_graph(task_graph) + ax_task_graph.get_figure().savefig(thisdir / f"residual_task_graph_{i}.png") + plt.close(ax_task_graph.get_figure()) + + ax_schedule: plt.Axes = draw_gantt(schedule) + ax_schedule.get_figure().savefig(thisdir / f"residual_schedule_{i}.png") + plt.close(ax_schedule.get_figure()) + + return schedule + + +def cumulative(network: nx.Graph, task_graphs: List[nx.DiGraph], arrival_times: List[float]) -> ScheduleType: + scheduler = ParametricScheduler( + initial_priority=UpwardRanking(), + insert_task=GreedyInsert( + append_only=False, + compare="EFT", + critical_path=False + ), + cumulative=True + ) + + schedule: ScheduleType = {node: [] for node in network.nodes} + for i, (arrival_time, task_graph) in enumerate(zip(arrival_times, task_graphs), start=1): + # join all previous task graphs with the current task graph + task_graph = nx.compose_all([task_graphs[j] for j in range(i)]) + + schedule = scheduler.schedule( + network=network, + task_graph=task_graph, + schedule=schedule, + min_start_time=arrival_time + ) + + ax_task_graph: plt.Axes = draw_task_graph(task_graph) + ax_task_graph.get_figure().savefig(thisdir / f"cumulative_task_graph_{i}.png") + plt.close(ax_task_graph.get_figure()) + + ax_schedule: plt.Axes = draw_gantt(schedule) + ax_schedule.get_figure().savefig(thisdir / f"cumulative_schedule_{i}.png") + plt.close(ax_schedule.get_figure()) + + return schedule + + + + + + + + + + + +def delayed_task(): + # create a task graph 3 nodes + network = get_network(num_nodes=2) + + for node in network.nodes: + network.nodes[node]["weight"] = 1 + for edge in network.edges: + if not network.is_directed() and edge[0] == edge[1]: + network.edges[edge]["weight"] = 1e9 * 1 + else: + network.edges[edge]["weight"] = 1 + + ax_network: plt.Axes = draw_network(network) + ax_network.get_figure().savefig(thisdir / "network.png") + plt.close(ax_network.get_figure()) + + task_graph_1 = nx.DiGraph() + task_graph_1.add_nodes_from(["1.A", "1.B", "1.C"]) + task_graph_1.add_edges_from([("1.A", "1.B"), ("1.A", "1.C")]) + + task_graph_1.nodes["1.A"]["weight"] = 1.3 + task_graph_1.nodes["1.B"]["weight"] = 2 + task_graph_1.nodes["1.C"]["weight"] = 1 + + task_graph_1.edges[("1.A", "1.B")]["weight"] = 1 + task_graph_1.edges[("1.A", "1.C")]["weight"] = 1 + + + task_graph_2 = nx.DiGraph() + task_graph_2.add_nodes_from(["2.A", "2.B", "2.C"]) + task_graph_2.add_edges_from([("2.A", "2.B"), ("2.A", "2.C")]) + + task_graph_2.nodes["2.A"]["weight"] = 1.3 + task_graph_2.nodes["2.B"]["weight"] = 2 + task_graph_2.nodes["2.C"]["weight"] = 1 + + task_graph_2.edges[("2.A", "2.B")]["weight"] = 1 + task_graph_2.edges[("2.A", "2.C")]["weight"] = 1 + + task_graph_3 = nx.DiGraph() + task_graph_3.add_nodes_from(["3.A", "3.B", "3.C"]) + task_graph_3.add_edges_from([("3.A", "3.B"), ("3.A", "3.C")]) + + task_graph_3.nodes["3.A"]["weight"] = 1.3 + task_graph_3.nodes["3.B"]["weight"] = 2 + task_graph_3.nodes["3.C"]["weight"] = 1 + + task_graph_3.edges[("3.A", "3.B")]["weight"] = 1 + task_graph_3.edges[("3.A", "3.C")]["weight"] = 1 + + task_graph_4 = nx.DiGraph() + task_graph_4.add_nodes_from(["4.A", "4.B", "4.C"]) + task_graph_4.add_edges_from([("4.A", "4.B"), ("4.A", "4.C")]) + + task_graph_4.nodes["4.A"]["weight"] = 1.3 + task_graph_4.nodes["4.B"]["weight"] = 2 + task_graph_4.nodes["4.C"]["weight"] = 1 + + task_graph_4.edges[("4.A", "4.B")]["weight"] = 1 + task_graph_4.edges[("4.A", "4.C")]["weight"] = 1 + + arrival_times = [0, 2, 3, 4] + task_graphs = [task_graph_1, task_graph_2, task_graph_3, task_graph_4] + + cumulative_schedule = cumulative(network, task_graphs, arrival_times) + residual_schedule = residual(network, task_graphs, arrival_times) + + # print makespan for each schedule + print("Cumulative makespan:", max(task.end for tasks in cumulative_schedule.values() for task in tasks)) + print("Residual makespan:", max(task.end for tasks in residual_schedule.values() for task in tasks)) + + # calculate the sum of the makespan for each task graph + + tasks_by_task_graph = {} + for tasks in cumulative_schedule.values(): + for task in tasks: + if task.name.split(".")[0] not in tasks_by_task_graph: + tasks_by_task_graph[task.name.split(".")[0]] = [] + tasks_by_task_graph[task.name.split(".")[0]].append(task) + + cumulative_sum_of_makespans = 0 + for i, tasks in enumerate(dict(sorted(tasks_by_task_graph.items())).values()): + cumulative_sum_of_makespans += max(task.end for task in tasks) - arrival_times[i] + print(f'{i+1}: {max(task.end for task in tasks)} - {arrival_times[i]} = {max(task.end for task in tasks) - arrival_times[i]}') + + print(f'cumulative_sum_of_makespans: {cumulative_sum_of_makespans}') + + tasks_by_task_graph = {} + for tasks in residual_schedule.values(): + for task in tasks: + if task.name.split(".")[0] not in tasks_by_task_graph: + tasks_by_task_graph[task.name.split(".")[0]] = [] + tasks_by_task_graph[task.name.split(".")[0]].append(task) + + residual_sum_of_makespans = 0 + for i, tasks in enumerate(dict(sorted(tasks_by_task_graph.items())).values()): + residual_sum_of_makespans += max(task.end for task in tasks) - arrival_times[i] + print(f'{i+1}: {max(task.end for task in tasks)} - {arrival_times[i]} = {max(task.end for task in tasks) - arrival_times[i]}') + + print(f'residual_sum_of_makespans: {residual_sum_of_makespans}') + + +def residual_cumulative_comparison(): + arrival_times = [2, 2.1, 5] + task_graphs = [ + add_random_weights(get_branching_dag(levels=2, branching_factor=2)) + for _ in arrival_times + ] + + # Rename nodes with prefixes t1_, t2_, etc. + for i, _tg in enumerate(task_graphs, start=1): # start=1 to match t1, t2, etc. + mapping = {node: f"{i}.{node}" for node in _tg.nodes} + nx.relabel_nodes(_tg, mapping, copy=False) # Modify in-place + + network = add_ccr_weights(task_graphs[0], add_random_weights(get_network()), ccr=1.0) + + ax_network: plt.Axes = draw_network(network) + ax_network.get_figure().savefig(thisdir / "network.png") + plt.close(ax_network.get_figure()) + + residual_schedule = residual(network, task_graphs, arrival_times) + cumulative_schedule = cumulative(network, task_graphs, arrival_times) + + # print makespan for each schedule + print("Residual makespan:", max(task.end for tasks in residual_schedule.values() for task in tasks)) + print("Cumulative makespan:", max(task.end for tasks in cumulative_schedule.values() for task in tasks)) + +if __name__ == '__main__': + # residual_cumulative_comparison() + delayed_task() \ No newline at end of file diff --git a/scripts/examples/parametric_residual/network.png b/scripts/examples/parametric_residual/network.png new file mode 100644 index 00000000..db6b72c5 Binary files /dev/null and b/scripts/examples/parametric_residual/network.png differ diff --git a/scripts/examples/parametric_residual/residual_schedule_1.png b/scripts/examples/parametric_residual/residual_schedule_1.png new file mode 100644 index 00000000..0df2105d Binary files /dev/null and b/scripts/examples/parametric_residual/residual_schedule_1.png differ diff --git a/scripts/examples/parametric_residual/residual_schedule_2.png b/scripts/examples/parametric_residual/residual_schedule_2.png new file mode 100644 index 00000000..46d666dc Binary files /dev/null and b/scripts/examples/parametric_residual/residual_schedule_2.png differ diff --git a/scripts/examples/parametric_residual/residual_schedule_3.png b/scripts/examples/parametric_residual/residual_schedule_3.png new file mode 100644 index 00000000..f8867e06 Binary files /dev/null and b/scripts/examples/parametric_residual/residual_schedule_3.png differ diff --git a/scripts/examples/parametric_residual/residual_schedule_4.png b/scripts/examples/parametric_residual/residual_schedule_4.png new file mode 100644 index 00000000..872c586f Binary files /dev/null and b/scripts/examples/parametric_residual/residual_schedule_4.png differ diff --git a/scripts/examples/parametric_residual/residual_task_graph_1.png b/scripts/examples/parametric_residual/residual_task_graph_1.png new file mode 100644 index 00000000..340cdd71 Binary files /dev/null and b/scripts/examples/parametric_residual/residual_task_graph_1.png differ diff --git a/scripts/examples/parametric_residual/residual_task_graph_2.png b/scripts/examples/parametric_residual/residual_task_graph_2.png new file mode 100644 index 00000000..7216bd2e Binary files /dev/null and b/scripts/examples/parametric_residual/residual_task_graph_2.png differ diff --git a/scripts/examples/parametric_residual/residual_task_graph_3.png b/scripts/examples/parametric_residual/residual_task_graph_3.png new file mode 100644 index 00000000..9ecacbc3 Binary files /dev/null and b/scripts/examples/parametric_residual/residual_task_graph_3.png differ diff --git a/scripts/examples/parametric_residual/residual_task_graph_4.png b/scripts/examples/parametric_residual/residual_task_graph_4.png new file mode 100644 index 00000000..75c068aa Binary files /dev/null and b/scripts/examples/parametric_residual/residual_task_graph_4.png differ diff --git a/scripts/examples/spare_core/BIL_schedule.png b/scripts/examples/spare_core/BIL_schedule.png new file mode 100644 index 00000000..392642f1 Binary files /dev/null and b/scripts/examples/spare_core/BIL_schedule.png differ diff --git a/scripts/examples/spare_core/CPOP_schedule.png b/scripts/examples/spare_core/CPOP_schedule.png new file mode 100644 index 00000000..f2af1674 Binary files /dev/null and b/scripts/examples/spare_core/CPOP_schedule.png differ diff --git a/scripts/examples/spare_core/DPS_schedule.png b/scripts/examples/spare_core/DPS_schedule.png new file mode 100644 index 00000000..b26bf4c2 Binary files /dev/null and b/scripts/examples/spare_core/DPS_schedule.png differ diff --git a/scripts/examples/spare_core/Duplex_schedule.png b/scripts/examples/spare_core/Duplex_schedule.png new file mode 100644 index 00000000..1010b2d7 Binary files /dev/null and b/scripts/examples/spare_core/Duplex_schedule.png differ diff --git a/scripts/examples/spare_core/ETF_schedule.png b/scripts/examples/spare_core/ETF_schedule.png new file mode 100644 index 00000000..b26bf4c2 Binary files /dev/null and b/scripts/examples/spare_core/ETF_schedule.png differ diff --git a/scripts/examples/spare_core/FCP_schedule.png b/scripts/examples/spare_core/FCP_schedule.png new file mode 100644 index 00000000..f2af1674 Binary files /dev/null and b/scripts/examples/spare_core/FCP_schedule.png differ diff --git a/scripts/examples/spare_core/FLB_schedule.png b/scripts/examples/spare_core/FLB_schedule.png new file mode 100644 index 00000000..b26bf4c2 Binary files /dev/null and b/scripts/examples/spare_core/FLB_schedule.png differ diff --git a/scripts/examples/spare_core/FastestNode_schedule.png b/scripts/examples/spare_core/FastestNode_schedule.png new file mode 100644 index 00000000..f2af1674 Binary files /dev/null and b/scripts/examples/spare_core/FastestNode_schedule.png differ diff --git a/scripts/examples/spare_core/GDL_schedule.png b/scripts/examples/spare_core/GDL_schedule.png new file mode 100644 index 00000000..445e3953 Binary files /dev/null and b/scripts/examples/spare_core/GDL_schedule.png differ diff --git a/scripts/examples/spare_core/HBMCT_schedule.png b/scripts/examples/spare_core/HBMCT_schedule.png new file mode 100644 index 00000000..b26bf4c2 Binary files /dev/null and b/scripts/examples/spare_core/HBMCT_schedule.png differ diff --git a/scripts/examples/spare_core/HEFT_schedule.png b/scripts/examples/spare_core/HEFT_schedule.png new file mode 100644 index 00000000..f2af1674 Binary files /dev/null and b/scripts/examples/spare_core/HEFT_schedule.png differ diff --git a/scripts/examples/spare_core/MCT_schedule.png b/scripts/examples/spare_core/MCT_schedule.png new file mode 100644 index 00000000..f2af1674 Binary files /dev/null and b/scripts/examples/spare_core/MCT_schedule.png differ diff --git a/scripts/examples/spare_core/MET_schedule.png b/scripts/examples/spare_core/MET_schedule.png new file mode 100644 index 00000000..f2af1674 Binary files /dev/null and b/scripts/examples/spare_core/MET_schedule.png differ diff --git a/scripts/examples/spare_core/MSBC_schedule.png b/scripts/examples/spare_core/MSBC_schedule.png new file mode 100644 index 00000000..f2af1674 Binary files /dev/null and b/scripts/examples/spare_core/MSBC_schedule.png differ diff --git a/scripts/examples/spare_core/MaxMin_schedule.png b/scripts/examples/spare_core/MaxMin_schedule.png new file mode 100644 index 00000000..a99e3139 Binary files /dev/null and b/scripts/examples/spare_core/MaxMin_schedule.png differ diff --git a/scripts/examples/spare_core/MinMin_schedule.png b/scripts/examples/spare_core/MinMin_schedule.png new file mode 100644 index 00000000..1010b2d7 Binary files /dev/null and b/scripts/examples/spare_core/MinMin_schedule.png differ diff --git a/scripts/examples/spare_core/OLB_schedule.png b/scripts/examples/spare_core/OLB_schedule.png new file mode 100644 index 00000000..392642f1 Binary files /dev/null and b/scripts/examples/spare_core/OLB_schedule.png differ diff --git a/scripts/examples/spare_core/Sufferage_schedule.png b/scripts/examples/spare_core/Sufferage_schedule.png new file mode 100644 index 00000000..a99e3139 Binary files /dev/null and b/scripts/examples/spare_core/Sufferage_schedule.png differ diff --git a/scripts/examples/spare_core/WBA_schedule.png b/scripts/examples/spare_core/WBA_schedule.png new file mode 100644 index 00000000..30cd23fe Binary files /dev/null and b/scripts/examples/spare_core/WBA_schedule.png differ diff --git a/scripts/examples/spare_core/main.py b/scripts/examples/spare_core/main.py new file mode 100644 index 00000000..e569b9d2 --- /dev/null +++ b/scripts/examples/spare_core/main.py @@ -0,0 +1,404 @@ +import random +import numpy as np +from typing import Tuple, Dict, List, Hashable, Optional, Set +import networkx as nx +import pandas as pd +from saga.scheduler import Task, Scheduler +from saga.schedulers.cpop import CpopScheduler +from saga.schedulers.heft import HeftScheduler +from saga.schedulers.bil import BILScheduler +from saga.schedulers.brute_force import BruteForceScheduler +from saga.schedulers.dps import DPSScheduler +from saga.schedulers.minmin import MinMinScheduler +from saga.schedulers.maxmin import MaxMinScheduler +from saga.schedulers.duplex import DuplexScheduler +from saga.schedulers.etf import ETFScheduler +from saga.schedulers.fastest_node import FastestNodeScheduler +from saga.schedulers.fcp import FCPScheduler +from saga.schedulers.flb import FLBScheduler +from saga.schedulers.gdl import GDLScheduler +from saga.schedulers.hbmct import HbmctScheduler +from saga.schedulers.mct import MCTScheduler +from saga.schedulers.met import METScheduler +from saga.schedulers.msbc import MsbcScheduler +from saga.schedulers.olb import OLBScheduler +from saga.schedulers.sufferage import SufferageScheduler +from saga.schedulers.wba import WBAScheduler + +from saga.schedulers.dynamic_task_graph.residual.rheft import ResidualHeftScheduler +# from saga.schedulers.dynamic_task_graph.baseline.heft import BaselineHeftScheduler +from saga.schedulers.dynamic_task_graph.residual.rbil import ResidualBILScheduler +from saga.schedulers.dynamic_task_graph.residual.rbrute_force import ResidualBruteForceScheduler +from saga.schedulers.dynamic_task_graph.residual.rcpop import ResidualCpopScheduler +from saga.schedulers.dynamic_task_graph.residual.rdps import ResidualDPSScheduler +from saga.schedulers.dynamic_task_graph.residual.rminmin import ResidualMinMinScheduler +from saga.schedulers.dynamic_task_graph.residual.rmaxmin import ResidualMaxMinScheduler +from saga.schedulers.dynamic_task_graph.residual.rduplex import ResidualDuplexScheduler +from saga.schedulers.dynamic_task_graph.residual.retf import ResidualETFScheduler +from saga.schedulers.dynamic_task_graph.residual.rfastest_node import ResidualFastestNodeScheduler +from saga.schedulers.dynamic_task_graph.residual.rfcp import ResidualFCPScheduler +from saga.schedulers.dynamic_task_graph.residual.rflb import ResidualFLBScheduler +from saga.schedulers.dynamic_task_graph.residual.rgdl import ResidualGDLScheduler +from saga.schedulers.dynamic_task_graph.residual.rhbmct import ResidualHbmctScheduler +from saga.schedulers.dynamic_task_graph.residual.rmct import ResidualMCTScheduler +from saga.schedulers.dynamic_task_graph.residual.rmet import ResidualMETScheduler +from saga.schedulers.dynamic_task_graph.residual.rmsbc import ResidualMsbcScheduler +from saga.schedulers.dynamic_task_graph.residual.rolb import ResidualOLBScheduler +from saga.schedulers.dynamic_task_graph.residual.rsufferage import ResidualSufferageScheduler +from saga.schedulers.dynamic_task_graph.residual.rwba import ResidualWBAScheduler + +from saga.utils.draw import draw_gantt, draw_network, draw_task_graph +from saga.utils.tools import get_insert_loc +from saga.utils.random_graphs import ( + get_branching_dag, get_chain_dag, get_diamond_dag, get_fork_dag, + get_network, add_random_weights +) +import pathlib +import plotly.express as px +import matplotlib.pyplot as plt +import logging + + +from abc import ABC, abstractmethod + + +logging.basicConfig(level=logging.INFO) + + + +thisdir = pathlib.Path(__file__).parent.absolute() + + + +class ResidualWrapper(ABC): + def schedule( + self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + scheduler: Scheduler + ) -> Dict[str, List[Task]]: + comp_schedule: Dict[Hashable, List[Task]] + + for idx, task_graph_tupple in enumerate(task_graphs): + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + if idx <= 0: + comp_schedule = scheduler.schedule(network, task_graph) + else: + comp_schedule = scheduler.schedule(network, task_graph, comp_schedule, task_graph_arrival_time) + + return comp_schedule + + +# this wrapper is ignorant of the previous task graphs +class HEFTResidualWrapper(ABC): + def schedule( + self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + scheduler: Scheduler + ) -> Dict[str, List[Task]]: + final_schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + + # create a new list for schedule of each task graph + comp_schdeules: List[Dict[str, List[Task]]] = [] + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + # print(f'task_graph: {task_graph}') + comp_schdeule = scheduler.schedule(network, task_graph) + comp_schdeules.append(comp_schdeule) + # print(f'comp_schdeule: {comp_schdeule}') + + # print(f'Length of comp_schdeules: {len(comp_schdeules)}') + # print(f'comp_schdeules: {comp_schdeules}') + + # combine the schedules + for idx, task_graph_tupple in enumerate(task_graphs): + comp_schdeule = comp_schdeules[idx] + # creat a list of all tasks from comp_schedule values with type List[Task] + all_tasks = [task for task_list in comp_schdeule.values() for task in task_list] + for task in all_tasks: + # print(f'task: {task}') + _ , start_time = get_insert_loc( + final_schedule[task.node], + task.start + task_graph_tupple[1], + task.end - task.start + ) + final_schedule[task.node].append(Task(task.node, task.name, start_time, start_time + task.end - task.start)) + + return final_schedule + + + +class CumulativeWrapper(ABC): + + def schedule( + self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + scheduler: Scheduler + ) -> Dict[str, List[Task]]: + comp_schedule: Dict[str, List[Task]] = {} + for idx, task_graph_tupple in enumerate(task_graphs): + # print(f'idx: {idx}') + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + combined_task_graph = nx.DiGraph() + + for i in range(idx + 1): + combined_task_graph = nx.compose(combined_task_graph, task_graphs[i][0]) + + + # if task_graph_arrival_time > 0: + # for task_list in comp_schedule.values(): + # for task in task_list: + # print(f'task {task.name} with start: {task.start}, end: {task.end}') + # if task.end > task_graph_arrival_time: + # task_list.remove(task) + # print(f'task {task.name} with start: {task.start}, end: {task.end} needs to be resheduled') + + if task_graph_arrival_time > 0: + # Flatten all tasks into a single list + all_tasks = [(key, task) for key, task_list in comp_schedule.items() for task in task_list] + + # Create a new schedule after filtering + updated_schedule = {key: [] for key in comp_schedule} # Initialize an empty schedule + for key, task in all_tasks: + # print(f'Task {task.name} with start: {task.start}, end: {task.end}') + if task.end <= task_graph_arrival_time: + # Retain the task if it ends before the graph arrival time + updated_schedule[key].append(task) + if task.name in combined_task_graph: + combined_task_graph.nodes[task.name]['weight'] = 0 + else: + pass + # print(f'Task {task.name} with start: {task.start}, end: {task.end} needs to be rescheduled') + + # Update the original schedule with the filtered tasks + comp_schedule = updated_schedule + + + for node, attributes in combined_task_graph.nodes(data=True): + weight = attributes.get('weight', None) # Get the 'weight' attribute, default to None if not set + # print(f"Node: {node}, Weight: {weight}") + + + + if idx <= 0: + comp_schedule = scheduler.schedule(network, task_graph) + else: + comp_schedule = scheduler.schedule(network, combined_task_graph, comp_schedule, task_graph_arrival_time) + + # print(f'idx: {idx} comp_schedule: {comp_schedule}') + + # print(f'final comp_schedule: {comp_schedule}') + + return comp_schedule + + + + + + +def get_random_network() -> nx.Graph: + # network = get_network(num_nodes=random.randint(3, 8)) + # network = add_random_weights(network) + + network = get_network(num_nodes=3) + + """Adds random weights to the DAG.""" + for node in network.nodes: + network.nodes[node]["weight"] = 1 + for edge in network.edges: + if not network.is_directed() and edge[0] == edge[1]: + network.edges[edge]["weight"] = 1e9 * 1 + else: + network.edges[edge]["weight"] = 1 + + return network + +def get_random_task_graph() -> nx.DiGraph: + choice = random.choice(["chain", "fork", "diamond", "branching"]) + # choice = random.choice(["branching"]) + + if choice == "chain": + task_graph = get_chain_dag( + # num_nodes=random.randint(2, 2) + num_nodes=random.randint(5, 10) + ) + elif choice == "fork": + task_graph = get_fork_dag() + elif choice == "diamond": + task_graph = get_diamond_dag() + elif choice == "branching": + task_graph = get_branching_dag( + levels=random.randint(2,4), + branching_factor=random.randint(2,4) + ) + + add_random_weights(task_graph) + + return task_graph + +def draw_instance(network: nx.Graph, task_graph: nx.DiGraph): + logging.basicConfig(level=logging.INFO) + ax: plt.Axes = draw_task_graph(task_graph, use_latex=True) + ax.get_figure().savefig(str(thisdir / 'task_graph.png')) + + ax: plt.Figure = draw_network(network, draw_colors=False, use_latex=True) + ax.get_figure().savefig(str(thisdir / 'network.png')) + +def draw_my_network(network: nx.Graph, name: str): + ax: plt.Figure = draw_network(network, draw_colors=False, use_latex=True) + ax.get_figure().savefig(str(thisdir / f'{name}.png')) + +def draw_my_task_graph(task_graph: nx.DiGraph, name: str): + ax: plt.Axes = draw_task_graph(task_graph, use_latex=True) + ax.get_figure().savefig(str(thisdir / f'{name}.png')) + +def draw_schedule(schedule: Dict[str, List[Task]], name: str, xmax: float = None): + ax: plt.Axes = draw_gantt(schedule, use_latex=True, xmax=xmax) + ax.get_figure().savefig(str(thisdir / f'{name}.png')) + +def main(): + # forth try + + network = get_random_network() + task_graph = get_random_task_graph() + draw_my_task_graph(task_graph, f"task_graph") + draw_my_network(network, "network") + + + # non-residual schedulers + wbaScheduler = WBAScheduler() + sufferageScheduler = SufferageScheduler() + olbScheduler = OLBScheduler() + msbcScheduler = MsbcScheduler() + metScheduler = METScheduler() + mctScheduler = MCTScheduler() + hbmcScheduler = HbmctScheduler() + gdlscheduler = GDLScheduler() + flbScheduler = FLBScheduler() + heftScheduler = HeftScheduler() + bilScheduler = BILScheduler() + cpopScheduler = CpopScheduler() + dpScheduler = DPSScheduler() + minminScheduler = MinMinScheduler() + maxminScheduler = MaxMinScheduler() + duplexScheduler = DuplexScheduler() + etfScheduler = ETFScheduler() + fastestNodeScheduler = FastestNodeScheduler() + fcpScheduler = FCPScheduler() + + # bruteForceScheduler = BruteForceScheduler() + + + + # todo: implement baseline schedulers for comparison + # baselineHeftScheduler = BaselineHeftScheduler() + + + + + + # non-residual schedulers + wbaSchedule = wbaScheduler.schedule(network, task_graph) + sufferageSchedule = sufferageScheduler.schedule(network, task_graph) + olbSchedule = olbScheduler.schedule(network, task_graph) + msbcSchedule = msbcScheduler.schedule(network, task_graph) + metSchedule = metScheduler.schedule(network, task_graph) + mctSchedule = mctScheduler.schedule(network, task_graph) + hbmcSchedule = hbmcScheduler.schedule(network, task_graph) + gdlschedule = gdlscheduler.schedule(network, task_graph) + bilSchedule = bilScheduler.schedule(network, task_graph) + heftSchedule = heftScheduler.schedule(network, task_graph) + cpopSchedule = cpopScheduler.schedule(network, task_graph) + dpSchedule = dpScheduler.schedule(network, task_graph) + minminSchedule = minminScheduler.schedule(network, task_graph) + maxminSchedule = maxminScheduler.schedule(network, task_graph) + duplexSchedule = duplexScheduler.schedule(network, task_graph) + etfSchedule = etfScheduler.schedule(network, task_graph) + fastestNodeSchedule = fastestNodeScheduler.schedule(network, task_graph) + fcpSchedule = fcpScheduler.schedule(network, task_graph) + flbSchedule = flbScheduler.schedule(network, task_graph) + # bruteForceSchedule = bruteForceScheduler.schedule(network, task_graphs[0][0]) + + # count the number of idle cores in each schedule + wbaIdleCores = sum([1 for tasks in wbaSchedule.values() if not tasks]) + sufferageIdleCores = sum([1 for tasks in sufferageSchedule.values() if not tasks]) + olbIdleCores = sum([1 for tasks in olbSchedule.values() if not tasks]) + msbcIdleCores = sum([1 for tasks in msbcSchedule.values() if not tasks]) + metIdleCores = sum([1 for tasks in metSchedule.values() if not tasks]) + mctIdleCores = sum([1 for tasks in mctSchedule.values() if not tasks]) + hbmcIdleCores = sum([1 for tasks in hbmcSchedule.values() if not tasks]) + gdlIdleCores = sum([1 for tasks in gdlschedule.values() if not tasks]) + bilIdleCores = sum([1 for tasks in bilSchedule.values() if not tasks]) + heftIdleCores = sum([1 for tasks in heftSchedule.values() if not tasks]) + cpopIdleCores = sum([1 for tasks in cpopSchedule.values() if not tasks]) + dpIdleCores = sum([1 for tasks in dpSchedule.values() if not tasks]) + minminIdleCores = sum([1 for tasks in minminSchedule.values() if not tasks]) + maxminIdleCores = sum([1 for tasks in maxminSchedule.values() if not tasks]) + duplexIdleCores = sum([1 for tasks in duplexSchedule.values() if not tasks]) + etfIdleCores = sum([1 for tasks in etfSchedule.values() if not tasks]) + fastestNodeIdleCores = sum([1 for tasks in fastestNodeSchedule.values() if not tasks]) + fcpIdleCores = sum([1 for tasks in fcpSchedule.values() if not tasks]) + flbIdleCores = sum([1 for tasks in flbSchedule.values() if not tasks]) + # bruteForceIdleCores = sum([1 for tasks in bruteForceSchedule.values() if not tasks]) + + + + + # draw non-residual schedules + draw_schedule(wbaSchedule, "WBA_schedule") + draw_schedule(sufferageSchedule, "Sufferage_schedule") + draw_schedule(olbSchedule, "OLB_schedule") + draw_schedule(msbcSchedule, "MSBC_schedule") + draw_schedule(metSchedule, "MET_schedule") + draw_schedule(mctSchedule, "MCT_schedule") + draw_schedule(hbmcSchedule, "HBMCT_schedule") + draw_schedule(gdlschedule, "GDL_schedule") + draw_schedule(bilSchedule, "BIL_schedule") + draw_schedule(heftSchedule, "HEFT_schedule") + draw_schedule(cpopSchedule, "CPOP_schedule") + draw_schedule(dpSchedule, "DPS_schedule") + draw_schedule(minminSchedule, "MinMin_schedule") + draw_schedule(maxminSchedule, "MaxMin_schedule") + draw_schedule(duplexSchedule, "Duplex_schedule") + draw_schedule(etfSchedule, "ETF_schedule") + draw_schedule(fastestNodeSchedule, "FastestNode_schedule") + draw_schedule(fcpSchedule, "FCP_schedule") + draw_schedule(flbSchedule, "FLB_schedule") + # draw_schedule(bruteForceSchedule, "BF_schedule") + + # print makespan for each non-residual scheduler + print(f'HEFT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in heftSchedule.values()])}, idle cores: {heftIdleCores}') + print(f'BIL Makespan: {max([0 if not tasks else tasks[-1].end for tasks in bilSchedule.values()])}, idle cores: {bilIdleCores}') + print(f'CPOP Makespan: {max([0 if not tasks else tasks[-1].end for tasks in cpopSchedule.values()])}, idle cores: {cpopIdleCores}') + print(f'DPS Makespan: {max([0 if not tasks else tasks[-1].end for tasks in dpSchedule.values()])}, idle cores: {dpIdleCores}') + print(f'MinMin Makespan: {max([0 if not tasks else tasks[-1].end for tasks in minminSchedule.values()])}, idle cores: {minminIdleCores}') + print(f'MaxMin Makespan: {max([0 if not tasks else tasks[-1].end for tasks in maxminSchedule.values()])}, idle cores: {maxminIdleCores}') + print(f'Duplex Makespan: {max([0 if not tasks else tasks[-1].end for tasks in duplexSchedule.values()])}, idle cores: {duplexIdleCores}') + print(f'ETF Makespan: {max([0 if not tasks else tasks[-1].end for tasks in etfSchedule.values()])}, idle cores: {etfIdleCores}') + print(f'FastestNode Makespan: {max([0 if not tasks else tasks[-1].end for tasks in fastestNodeSchedule.values()])}, idle cores: {fastestNodeIdleCores}') + print(f'FCP Makespan: {max([0 if not tasks else tasks[-1].end for tasks in fcpSchedule.values()])}, idle cores: {fcpIdleCores}') + print(f'FLB Makespan: {max([0 if not tasks else tasks[-1].end for tasks in flbSchedule.values()])}, idle cores: {flbIdleCores}') + print(f'GDL Makespan: {max([0 if not tasks else tasks[-1].end for tasks in gdlschedule.values()])}, idle cores: {gdlIdleCores}') + print(f'HBMCT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in hbmcSchedule.values()])}, idle cores: {hbmcIdleCores}') + print(f'MCT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in mctSchedule.values()])}, idle cores: {mctIdleCores}') + print(f'MET Makespan: {max([0 if not tasks else tasks[-1].end for tasks in metSchedule.values()])}, idle cores: {metIdleCores}') + print(f'MSBC Makespan: {max([0 if not tasks else tasks[-1].end for tasks in msbcSchedule.values()])}, idle cores: {msbcIdleCores}') + print(f'OLB Makespan: {max([0 if not tasks else tasks[-1].end for tasks in olbSchedule.values()])}, idle cores: {olbIdleCores}') + print(f'Sufferage Makespan: {max([0 if not tasks else tasks[-1].end for tasks in sufferageSchedule.values()])}, idle cores: {sufferageIdleCores}') + print(f'WBA Makespan: {max([0 if not tasks else tasks[-1].end for tasks in wbaSchedule.values()])}, idle cores: {wbaIdleCores}') + + # baseline makespan + # print(f'Baseline HEFT Makespan: {max([0 if not tasks else tasks[-1].end for tasks in baselineHeftSchedule.values()])}') + + + +if __name__ == '__main__': + main() + diff --git a/scripts/examples/spare_core/network.png b/scripts/examples/spare_core/network.png new file mode 100644 index 00000000..1f296582 Binary files /dev/null and b/scripts/examples/spare_core/network.png differ diff --git a/scripts/examples/spare_core/task_graph.png b/scripts/examples/spare_core/task_graph.png new file mode 100644 index 00000000..21789c14 Binary files /dev/null and b/scripts/examples/spare_core/task_graph.png differ diff --git a/src/saga/scheduler.py b/src/saga/scheduler.py index 6a99f849..b7a6259b 100644 --- a/src/saga/scheduler.py +++ b/src/saga/scheduler.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Dict, Hashable, List, Optional +from typing import Dict, Hashable, List, Optional, Tuple import networkx as nx @@ -12,6 +12,33 @@ class Task: start: Optional[float] end: Optional[float] +class ResidualScheduler(ABC): + """An abstract class for a residual scheduler.""" + @abstractmethod + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Schedule the tasks on the network. + + Args: + network (nx.Graph): The network graph. + task_graphs (List[Tuple[nx.DiGraph, float]]): The task graphs. + + Returns: + Dict[Hashable, List[Task]]: The schedule. + """ + raise NotImplementedError + + @property + def __name__(self) -> str: + """Get the name of the scheduler. + + Returns: + str: The name of the scheduler. + """ + if hasattr(self, "name"): + return self.name + return self.__class__.__name__ + + class Scheduler(ABC): # pylint: disable=too-few-public-methods """An abstract class for a scheduler.""" diff --git a/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpcpop.py b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpcpop.py new file mode 100644 index 00000000..aa5a551c --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpcpop.py @@ -0,0 +1,198 @@ +import logging +import pathlib +from typing import Dict, Hashable, List, Tuple, Optional +import heapq + +import networkx as nx +import numpy as np +from saga.utils.draw import draw_gantt + +from ....scheduler import Task, DWScheduler +from ....utils.tools import get_insert_loc +from ...cpop import upward_rank +from ...cpop import cpop_ranks + +thisdir = pathlib.Path(__file__).resolve().parent + + +def heft_rank_sort(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + rank = upward_rank(network, task_graph) + topological_sort = {node: i for i, node in enumerate(reversed(list(nx.topological_sort(task_graph))))} + rank = {node: (rank[node] + topological_sort[node]) for node in rank} + return sorted(list(rank.keys()), key=rank.get, reverse=True) + +def cpop_ranking(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + ranks = cpop_ranks(network, task_graph) + start_tasks = [task for task in task_graph if task_graph.in_degree(task) == 0] + start_tasks_sorted = sorted(start_tasks, key=lambda t: ranks[t], reverse=True) + pq = [(-ranks[task], task) for task in start_tasks_sorted] + heapq.heapify(pq) + queue = [] + while pq: + _, task_name = heapq.heappop(pq) + queue.append(task_name) + ready_tasks = [ + succ for succ in task_graph.successors(task_name) + if all(pred in queue for pred in task_graph.predecessors(succ)) + ] + for ready_task in ready_tasks: + heapq.heappush(pq, (-ranks[ready_task], ready_task)) + + return queue + +class KPCpopScheduler(DWScheduler): + + # create constructor for the class + def __init__(self, k: int = 1) -> None: + super().__init__() + self.k = k + self.name = f"{self.k}PCPOPScheduler" + + + @staticmethod + def get_runtimes( + network: nx.Graph, task_graph: nx.DiGraph + ) -> Tuple[ + Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]],]: + + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug( + "Task %s on node %s has runtime %s", + task, + node, + runtimes[node][task], + ) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, + src, + dst_task, + dst, + commtimes[src, dst][src_task, dst_task], + ) + + return runtimes, commtimes + + def _schedule( + self, + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[ + Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float] + ], + schedule_order: List[Hashable], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: Optional[float] = 0 + ) -> Dict[Hashable, List[Task]]: + + + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + + # for every task that is in the comp_schedule, add it to the task_schedule + for task_list in comp_schedule.values(): + for task in task_list: + task_schedule[task.name] = task + + task_name: Hashable + # logging.debug("Schedule order: %s", schedule_order) + for task_name in schedule_order: + min_finish_time = np.inf + best_node = None + for node in network.nodes: # Find the best node to run the task + max_arrival_time: float = max( + [ + task_graph.nodes[task_name]["arrival_time"], + *[ + task_schedule[parent].end + + ( + commtimes[(task_schedule[parent].node, node)][ + (parent, task_name) + ] + ) + for parent in task_graph.predecessors(task_name) + ], + ] + ) + + runtime = runtimes[node][task_name] + idx, start_time = get_insert_loc( + comp_schedule[node], max_arrival_time, runtime + ) + + logging.debug( + "Testing task %s on node %s: start time %s, finish time %s", + task_name, + node, + start_time, + start_time + runtime, + ) + + finish_time = start_time + runtime + if finish_time < min_finish_time: + min_finish_time = finish_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task_name] + task = Task( + best_node[0], task_name, min_finish_time - new_runtime, min_finish_time + ) + comp_schedule[best_node[0]].insert(best_node[1], task) + task_schedule[task_name] = task + + return comp_schedule + + def schedule( + self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[str, List[Task]]: + + k = self.k + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + comp_schedule: Dict[Hashable, List[Task]] = None + for i in range(len(task_graphs)): + composed_graph = nx.compose_all([task_graphs[j][0] for j in range(max(0, i-k+1), i+1)]) + runtimes, commtimes = self.get_runtimes(network, composed_graph) + schedule_order = cpop_ranking(network, composed_graph) + + tasks_to_remove = [] + if comp_schedule is not None: + for task_name in schedule_order: + matching_task = next((task for tasks in comp_schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start < task_graphs[i][1]: + tasks_to_remove.append(task_name) + else: + comp_schedule[matching_task.node].remove(matching_task) + + for task_name in tasks_to_remove: + schedule_order.remove(task_name) + + + comp_schedule = self._schedule( + network, composed_graph, runtimes, commtimes, schedule_order, comp_schedule, task_graphs[i][1] + ) + + + return comp_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpflb.py b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpflb.py new file mode 100644 index 00000000..10b22ab7 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpflb.py @@ -0,0 +1,279 @@ +import json +import logging +from pprint import pformat +from queue import PriorityQueue +from typing import Dict, Hashable, List, Tuple + +import networkx as nx +from networkx import DiGraph, Graph + +from saga.scheduler import Task + +from ....scheduler import Scheduler, Task, DWScheduler + + +class PFLBScheduler(DWScheduler): + """The FLB (Fast Load Balancing) scheduler. + + Source: https://doi.org/10.1109/ICPP.1999.797442 + Note: They assume homogenous comp/comm speeds, so we will scale the weights in the task graph + by the average (but it will still perform poorly on heterogeneous networks). We also schedule + to the fastest node whenever the original algorithm schedules to an arbitrary node. + """ + def schedule(self, network: Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + network = network.copy() + task_graph = task_graph.copy() + + avg_comm_speed = sum( + network.edges[edge]['weight'] for edge in network.edges + if edge[0] != edge[1] or len(network.nodes) == 1 + ) / len(network.edges) + + fastest_node = max(network.nodes, key=lambda node: network.nodes[node]['weight']) + + def getEP(task: Hashable) -> Hashable: # pylint: disable=invalid-name + """Get Enabling Processor (EP) of a task. + + The enabling processor of a ready task t, EP (t) is the processor from which + the last message arrives. + + NOTE: Because this algorithm assumes homogenous comp/comm speeds, the EP is + *not* the processor from which the last message actually arrives, but rather + the processor from which the last message *would* arrive if the network was + homogenous (with comm speed equal to the average comm speed). + + Args: + task (Hashable): The task. + + Returns: + Hashable: The enabling processor of the task. + """ + enabling_task = max( + task_graph.predecessors(task), + key=lambda pred: scheduled_tasks[pred].end + task_graph.edges[pred, task]['weight'] / avg_comm_speed + ) + return scheduled_tasks[enabling_task].node + + + def getLMT(task: Hashable) -> float: # pylint: disable=invalid-name + """Get the Last Message Arrival Time (LMT) + + NOTE: Because the original algorithm assumes homogenous comp/comm speeds, we will + modify it to use the *actual* communication time between the tasks, rather than + assume they are all equal to the average communication time. + + Args: + task (Hashable): The task. + + Returns: + float: The LMT of the task. + """ + if task_graph.in_degree(task) == 0: + return task_graph_arrival_time + + return max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task]['weight'] / + avg_comm_speed + ) + for pred in task_graph.predecessors(task) + ) + + def getEMT(task: Hashable, node: Hashable) -> float: # pylint: disable=invalid-name + """Get the Effective Message Arrival Time (EMT) + + Since our networks have 0 comm delay between nodes and we extend for + heterogeneous networks, this is just getLMT(task). + + Args: + task (Hashable): The task. + node (Hashable): The node. + + Returns: + float: The EMT of the task. + """ + if task_graph.in_degree(task) == 0: + return task_graph_arrival_time + + return max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task]['weight'] / + network.edges[scheduled_tasks[pred].node, node]['weight'] + ) + for pred in task_graph.predecessors(task) + ) + + def getPRT(node: Hashable) -> float: # pylint: disable=invalid-name + return 0 if not schedule[node] else schedule[node][-1].end + + def getEST(task: Hashable, node: Hashable) -> float: # pylint: disable=invalid-name + return max(getPRT(node), getEMT(task, node)) + + non_ep_tasks = PriorityQueue() + emt_ep_tasks = { + node: PriorityQueue() + for node in network.nodes + } + lmt_ep_tasks = { + node: PriorityQueue() + for node in network.nodes + } + all_procs = PriorityQueue() + active_procs = PriorityQueue() + + for task in task_graph.nodes: + if task_graph.in_degree(task) == 0: + non_ep_tasks.put((task_graph_arrival_time, task)) + + for node in network.nodes: + all_procs.put((0, node)) + + def schedule_task() -> Tuple[Hashable, Hashable]: + # get head of active_procs without removing + _, proc1 = active_procs.queue[0] if active_procs.queue else (0, None) + task1 = None + if proc1 is not None: + _, task1 = emt_ep_tasks[proc1].queue[0] if emt_ep_tasks[proc1].queue else (0, None) + _, proc2 = all_procs.queue[0] if all_procs.queue else (0, None) + _, task2 = non_ep_tasks.queue[0] if non_ep_tasks.queue else (0, None) + + est_t1_p1 = getEST(task1, proc1) if proc1 is not None and task1 is not None else float('inf') + est_t2_p2 = getEST(task2, proc2) if proc2 is not None and task2 is not None else float('inf') + if est_t1_p1 == float('inf') and est_t2_p2 == float('inf'): + # NOTE: This should never happen. If it does, it means that there is a bug in the algorithm. + # log queue values + logging.debug("active_procs: %s", pformat(active_procs.queue)) + logging.debug("all_procs: %s", pformat(all_procs.queue)) + logging.debug("non_ep_tasks: %s", pformat(non_ep_tasks.queue)) + logging.debug("emt_ep_tasks: %s", pformat({node: pformat(emt_ep_tasks[node].queue) for node in emt_ep_tasks})) + logging.debug("schedule: %s", pformat(schedule)) + logging.debug("task_graph: %s", json.dumps(nx.readwrite.json_graph.node_link_data(task_graph))) + logging.debug("network: %s", json.dumps(nx.readwrite.json_graph.node_link_data(network))) + + + raise RuntimeError(f"No tasks to schedule. proc1={proc1}, task1={task1}, proc2={proc2}, task2={task2}") + if est_t1_p1 <= est_t2_p2: + new_task = Task( + node=proc1, + name=task1, + start=est_t1_p1, + end=est_t1_p1 + task_graph.nodes[task1]['weight'] / network.nodes[proc1]['weight'] + ) + schedule[proc1].append(new_task) + scheduled_tasks[task1] = new_task + + assert active_procs.get()[1] == proc1 + assert emt_ep_tasks[proc1].get()[1] == task1 # dequeue task from emt_ep_tasks[p1] + lmt_ep_tasks[proc1].queue = [ # remove task1 from lmt_ep_tasks[p1] + (priority, task) for priority, task in lmt_ep_tasks[proc1].queue + if task != task1 + ] + return task1, proc1 + else: + # schedule task t2 on processor p2 + new_task = Task( + node=proc2, + name=task2, + start=est_t2_p2, + end=est_t2_p2 + task_graph.nodes[task2]['weight'] / network.nodes[proc2]['weight'] + ) + schedule[proc2].append(new_task) + scheduled_tasks[task2] = new_task + + assert all_procs.get()[1] == proc2 # dequeue proc2 from all_procs + + # NOTE: this is not in the original algorithm, but it seems necessary + all_procs.put((getPRT(proc2), proc2)) # add proc2 with new priority PRT + + assert non_ep_tasks.get()[1] == task2 # dequeue task from non_ep_tasks + return task2, proc2 + + def update_task_lists(task: Hashable, proc: Hashable): + while True: + _, task = lmt_ep_tasks[proc].queue[0] if lmt_ep_tasks[proc].queue else (0, None) + if task is None: + break + if getLMT(task) >= getPRT(proc): # last message arrival time of t >= processor ready time + break + + assert lmt_ep_tasks[proc].get()[1] == task # dequeue task from lmt_ep_tasks[p] + emt_ep_tasks[proc].queue = [ # remove task from emt_ep_tasks[p] + (priority, _task) for priority, _task in emt_ep_tasks[proc].queue + if _task != task + ] + non_ep_tasks.put((getLMT(task), task)) # enqueue task in non_ep_tasks + + def update_proc_lists(task: Hashable, proc: Hashable): + _, task = emt_ep_tasks[proc].queue[0] if emt_ep_tasks[proc].queue else (0, None) + if task is None: + # remove proc from active_procs + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != proc + ] + else: + # remove proc from active_procs and add back with priority est + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != proc + ] + active_procs.put((getEST(task, proc), proc)) + + def update_ready_tasks(task: Hashable, proc: Hashable): + for succ in task_graph.successors(task): + if succ in scheduled_tasks: # not in original algorithm, is it necessary? + continue + + if not all(pred in scheduled_tasks for pred in task_graph.predecessors(succ)): + continue + + enabling_proc = getEP(succ) # get enabling processor of succ + lmt = getLMT(succ) # get last message arrival time of succ + emt = getEMT(succ, enabling_proc) # get effective message arrival time of succ on enabling processor + if lmt < getPRT(enabling_proc): # if lmt < processor ready time + # task perhaps should not execute on ep, since this indicates that the ep is not ready + # when the task is ready to execute + # enqueue succ with priority lmt in non_ep_tasks + non_ep_tasks.put((lmt, succ)) + else: + _, head_proc = emt_ep_tasks[enabling_proc].queue[0] if emt_ep_tasks[enabling_proc].queue else (0, None) + if head_proc is None: + # enqueue ep with priority est in active_procs + active_procs.put((getEST(succ, enabling_proc), enabling_proc)) + else: + # get next task to execute on ep (according to current priorities) + head_task = emt_ep_tasks[enabling_proc].queue[0][1] + # get the emt of this task + _emt = getEMT(head_task, enabling_proc) + if emt < _emt: + # succ should execute before head_task on ep because it becomes ready before head_task + # update ep with priority max(EMT, prt) in active_procs + # first, remove ep from active_procs + new_priority = max(emt, getPRT(enabling_proc)) + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != enabling_proc + ] + active_procs.put((new_priority, enabling_proc)) + # enqueue succ with priority emt in emt_ep_tasks[ep] + emt_ep_tasks[enabling_proc].put((emt, succ)) + # enqueue succ with priority lmt in lmt_ep_tasks[ep] + lmt_ep_tasks[enabling_proc].put((lmt, succ)) + + # while len(scheduled_tasks) < len(task_graph.nodes): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + task, proc = schedule_task() + update_task_lists(task, proc) + update_proc_lists(task, proc) + update_ready_tasks(task, proc) + + return schedule + diff --git a/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpheft.py b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpheft.py new file mode 100644 index 00000000..617f39db --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpheft.py @@ -0,0 +1,181 @@ +import logging +import pathlib +from typing import Dict, Hashable, List, Tuple, Optional + +import networkx as nx +import numpy as np +from saga.utils.draw import draw_gantt + +from ....scheduler import Task, DWScheduler +from ....utils.tools import get_insert_loc +from ...cpop import upward_rank + +thisdir = pathlib.Path(__file__).resolve().parent + + +def heft_rank_sort(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + rank = upward_rank(network, task_graph) + topological_sort = {node: i for i, node in enumerate(reversed(list(nx.topological_sort(task_graph))))} + rank = {node: (rank[node] + topological_sort[node]) for node in rank} + return sorted(list(rank.keys()), key=rank.get, reverse=True) + + +class KPHeftScheduler(DWScheduler): + + # create constructor for the class + def __init__(self, k: int = 1) -> None: + super().__init__() + self.k = k + self.name = f"{self.k}PHeftScheduler" + + + @staticmethod + def get_runtimes(network: nx.Graph, + task_graph: nx.DiGraph, + ) -> Tuple[ + Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]],]: + + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug( + "Task %s on node %s has runtime %s", + task, + node, + runtimes[node][task], + ) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, + src, + dst_task, + dst, + commtimes[src, dst][src_task, dst_task], + ) + + return runtimes, commtimes + + def _schedule( + self, + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[ + Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float] + ], + schedule_order: List[Hashable], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: Optional[float] = 0 + ) -> Dict[Hashable, List[Task]]: + + + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + + # for every task that is in the comp_schedule, add it to the task_schedule + for task_list in comp_schedule.values(): + for task in task_list: + task_schedule[task.name] = task + + task_name: Hashable + # logging.debug("Schedule order: %s", schedule_order) + for task_name in schedule_order: + min_finish_time = np.inf + best_node = None + for node in network.nodes: # Find the best node to run the task + max_arrival_time: float = max( + [ + task_graph.nodes[task_name]["arrival_time"], + *[ + task_schedule[parent].end + + ( + commtimes[(task_schedule[parent].node, node)][ + (parent, task_name) + ] + ) + for parent in task_graph.predecessors(task_name) + ], + ] + ) + + runtime = runtimes[node][task_name] + idx, start_time = get_insert_loc( + comp_schedule[node], max_arrival_time, runtime + ) + + logging.debug( + "Testing task %s on node %s: start time %s, finish time %s", + task_name, + node, + start_time, + start_time + runtime, + ) + + finish_time = start_time + runtime + if finish_time < min_finish_time: + min_finish_time = finish_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task_name] + task = Task( + best_node[0], task_name, min_finish_time - new_runtime, min_finish_time + ) + comp_schedule[best_node[0]].insert(best_node[1], task) + task_schedule[task_name] = task + + return comp_schedule + + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + ) -> Dict[str, List[Task]]: + + k = self.k + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + comp_schedule: Dict[Hashable, List[Task]] = None + for i in range(len(task_graphs)): + composed_graph = nx.compose_all([task_graphs[j][0] for j in range(max(0, i-k+1), i+1)]) + runtimes, commtimes = self.get_runtimes(network, composed_graph) + schedule_order = heft_rank_sort(network, composed_graph) + + tasks_to_remove = [] + if comp_schedule is not None: + for task_name in schedule_order: + matching_task = next((task for tasks in comp_schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start < task_graphs[i][1]: + tasks_to_remove.append(task_name) + else: + comp_schedule[matching_task.node].remove(matching_task) + + for task_name in tasks_to_remove: + schedule_order.remove(task_name) + + + comp_schedule = self._schedule( + network, composed_graph, runtimes, commtimes, schedule_order, comp_schedule, task_graphs[i][1] + ) + + + return comp_schedule + + diff --git a/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpmaxmin.py b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpmaxmin.py new file mode 100644 index 00000000..965e8156 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpmaxmin.py @@ -0,0 +1,123 @@ +from functools import lru_cache +from typing import Dict, Hashable, List, Tuple, Optional +import networkx as nx + +from ....scheduler import Scheduler, Task, DWScheduler +from saga.utils.draw import draw_gantt + + +class KPMaxMinScheduler(DWScheduler): # pylint: disable=too-few-public-methods + """Max-Min scheduler""" + + def __init__(self, k: int = 1) -> None: + super().__init__() + self.k = k + self.name = f"{self.k}PMaxMinScheduler" + + + + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + """Schedules the task graph on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule. + """ + + k = self.k + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + + + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for i in range(len(task_graphs)): + task_graph = nx.compose_all([task_graphs[j][0] for j in range(max(0, i-k+1), i+1)]) + + if i > 0: + for task_name in task_graph.nodes: + matching_task = next((task for tasks in schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start > task_graphs[i][1]: + schedule[matching_task.node].remove(matching_task) + scheduled_tasks.pop(task_name, None) + + + + + @lru_cache(maxsize=None) + def get_eet(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + @lru_cache(maxsize=None) + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_eat(node: Hashable, + task: Optional[Hashable] = None) -> float: + return max(schedule[node][-1].end if schedule.get(node) else 0, task_graph.nodes[task]["arrival_time"]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_fat(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]["arrival_time"] if task_graph.in_degree(task) <= 0 else max([ + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_ect(task: Hashable, node: Hashable) -> float: + return get_eet(task, node) + max(get_eat(node, task), get_fat(task, node)) + + def clear_caches(): + """Clear all caches.""" + get_eat.cache_clear() + get_fat.cache_clear() + get_ect.cache_clear() + + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + # get all tasks that are ready to be scheduled + available_tasks = [ + task for task in task_graph.nodes + if task not in scheduled_tasks and all(pred in scheduled_tasks for pred in task_graph.predecessors(task)) + ] + #slight change from MinMin logic + while available_tasks: + min_ects = {task: min(get_ect(task, node) for node in network.nodes) for task in available_tasks} + sched_task = max(min_ects, key=min_ects.get) + sched_node = min(network.nodes, key=lambda node: get_ect(sched_task, node)) + + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=max(get_eat(sched_node, sched_task), get_fat(sched_task, sched_node)), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + + available_tasks.remove(sched_task) + + clear_caches() + + # add empty list for nodes that have no tasks scheduled + for node in network.nodes: + schedule.setdefault(node, []) + + # sort schedule pairs by key name + schedule = dict(sorted(schedule.items())) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpminmin.py b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpminmin.py new file mode 100644 index 00000000..6665a3c3 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kpminmin.py @@ -0,0 +1,115 @@ +from functools import lru_cache +from itertools import product +from typing import Dict, Hashable, List, Tuple, Optional + +import networkx as nx + +from ....scheduler import Scheduler, Task + + +class KPMinMinScheduler(Scheduler): + """Minimum Completion Time scheduler""" + + def __init__(self, k: int = 1) -> None: + super().__init__() + self.k = k + self.name = f"{self.k}PMinMinScheduler" + + + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the schedule of the tasks on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule of the tasks on the network. + """ + k = self.k + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for i in range(len(task_graphs)): + task_graph = nx.compose_all([task_graphs[j][0] for j in range(max(0, i-k+1), i+1)]) + + if i > 0: + for task_name in task_graph.nodes: + matching_task = next((task for tasks in schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start > task_graphs[i][1]: + schedule[matching_task.node].remove(matching_task) + scheduled_tasks.pop(task_name, None) + + @lru_cache(maxsize=None) + def get_eet(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + @lru_cache(maxsize=None) + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_eat(node: Hashable, + task: Optional[Hashable] = None) -> float: + return max(schedule[node][-1].end if schedule.get(node) else 0, task_graph.nodes[task]["arrival_time"]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_fat(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]["arrival_time"] if task_graph.in_degree(task) <= 0 else max([ + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_ect(task: Hashable, node: Hashable) -> float: + return get_eet(task, node) + max(get_eat(node, task), get_fat(task, node)) + + def clear_caches(): + """Clear all caches.""" + get_eat.cache_clear() + get_fat.cache_clear() + get_ect.cache_clear() + + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + # get all tasks that are ready to be scheduled + available_tasks = [ + task for task in task_graph.nodes + if (task not in scheduled_tasks and set(task_graph.predecessors(task)).issubset(set(scheduled_tasks.keys()))) + ] + while available_tasks: + # Find the task and node that minimizes the ECT + sched_task, sched_node = min( + product(available_tasks, network.nodes), + key=lambda instance: get_ect(instance[0], instance[1]) + ) + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=max(get_eat(sched_node, sched_task), get_fat(sched_task, sched_node)), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + available_tasks.remove(sched_task) + + clear_caches() + + # add empty list for nodes that have no tasks scheduled + for node in network.nodes: + schedule.setdefault(node, []) + + # sort schedule pairs by key name + schedule = dict(sorted(schedule.items())) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/kpreemptive/kprandom.py b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kprandom.py new file mode 100644 index 00000000..19f8c537 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/kpreemptive/kprandom.py @@ -0,0 +1,72 @@ +from typing import Dict, Hashable, List, Tuple +import networkx as nx +from ....scheduler import Scheduler, Task, DWScheduler +import random + + +class KPRandomScheduler(DWScheduler): + """Schedules all tasks on the node with the highest processing speed""" + + def __init__(self, k: int = 1) -> None: + super().__init__() + self.k = k + self.name = f"{self.k}PRandomScheduler" + + + + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + + k = self.k + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + schedule = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + + for i in range(len(task_graphs)): + task_graph = nx.compose_all([task_graphs[j][0] for j in range(max(0, i-k+1), i+1)]) + + if i > 0: + for task_name in task_graph.nodes: + matching_task = next((task for tasks in schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start > task_graphs[i][1]: + schedule[matching_task.node].remove(matching_task) + scheduled_tasks.pop(task_name, None) + + + for task_name in nx.topological_sort(task_graph): + if task_name in scheduled_tasks: + continue + # Choose one of the network nodes at random uniformly + selected_node = random.choice(list(network.nodes)) + + # calculate the time needed for dependencies to arrive + if task_graph.in_degree(task_name) > 0: + data_arrival_time = max( + scheduled_tasks[pred].end + + (task_graph.edges[pred, task_name]["weight"] / network.edges[selected_node, selected_node]["weight"]) + for pred in task_graph.predecessors(task_name) + ) + else: + data_arrival_time = task_graph.nodes[task_name]["arrival_time"] + + # calculate the time needed for the task to execute + task_size = task_graph.nodes[task_name]["weight"] + exec_time = task_size / network.nodes[selected_node]["weight"] + start_time = max(data_arrival_time, task_graph.nodes[task_name]["arrival_time"], + schedule[selected_node][-1].end if schedule[selected_node] else 0) + # create the task and add it to the schedule + new_task = Task(selected_node, task_name, start_time, start_time + exec_time) + schedule[selected_node].append(new_task) + scheduled_tasks[task_name] = new_task + + # print the schedule + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npbil.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npbil.py new file mode 100644 index 00000000..a6ec6ccf --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npbil.py @@ -0,0 +1,174 @@ +import copy +from typing import Dict, Hashable, List, Tuple + +import networkx as nx + +from ....scheduler import Scheduler, Task, DWScheduler + + +class ResidualBILScheduler(DWScheduler): # pylint: disable=too-few-public-methods + """Best Imaginary Level Scheduler + + Source: https://doi.org/10.1007/BFb0024750 + Modifications: + - The original algorithm does not consider heterogenous communication strengths between + network nodes. This affects the ipc_overhead term in the BIL definition. We compute the + ipc_overhead between two tasks by scaling the cost by the average communication weight + in the network. + Original: BIL(task, node) = task_cost / node_speed + max(BIL(child, node) + ipc_overhead) + for child in children(task)) + """ + def schedule(self, # pylint: disable=too-many-locals + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + """Returns the schedule for the given task graph on the given network using the BIL algorithm. + + Args: + network: Network + task_graph: Task graph + + Returns: + A dictionary of the schedule + """ + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + + + # =========================================== + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + # =========================================== + scheduled_tasks: Dict[Hashable, Task] = {} + bils: Dict[Hashable, float] = {} + for task in reversed(list(nx.topological_sort(task_graph))): + for node in network.nodes: + exec_time = task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + bils[(task, node)] = exec_time + max( + ( + # Here we are trying find find the minimum if we execute child on the same node or execute it on + # another node and then communicate the result to the current node. We use minumum to find the best + # Then we take maximum of this computaion for all the children of the task to find the Best Imaginary Level + min( + bils[(child, node)], + *( + bils[(child, other_node)] + (task_graph.edges[task, child]['weight'] / + network.edges[other_node, node]['weight']) + for other_node in network.nodes + ) + ) + for child in task_graph.successors(task) + ), + # if there are no children, we take task_graph_arrival_time as the default value + default=task_graph_arrival_time + ) + + # we start with the tasks that have no dependencies (in_degree = 0) + ready_tasks = {task for task in task_graph.nodes if task_graph.in_degree(task) == 0} + while len(scheduled_tasks) < len(task_graph.nodes): + # We keep scheduling tasks until all tasks are scheduled + + # Section 3.1: Node (Task) Selection + # BIM is a dictionary of tasks and their BIM values for all nodes + # the list consists of tuples of (node, bim) sorted in descending order of BIM values + bims: Dict[Hashable, List[Tuple[Hashable, float]]] = { + # We try to find Best Imaginary Makespan BIM + task: sorted([ + # shcedule[node][-1].end is the end time of the last task scheduled on the node + # if the node is not scheduled, we take task_graph_arrival_time as the end time + # we add the BIL value of the task on the node to the end time to get the BIM value + (node, (schedule[node][-1].end if schedule[node] else task_graph_arrival_time) + bils[(task, node)]) + for node in network.nodes + # we just sort the BIM values in descending order by tuple[1] which is the BIM value + ], key=lambda x: x[1], reverse=True) + # We only consider tasks that are ready and not scheduled + for task in ready_tasks if task not in scheduled_tasks + } + + + # Tie breaker + # Here we are trying to find the task with the maximum BIM value. + # We define the priority of a node as the k-th smallest BIM value or the largest finite BIM value if the k-th smallest BIM value is undefined. In case more than one node have the same priority, we adopt a tie breaking policy in a recursive form: we compare the (k - 1)-th BIMs of nodes that have the same k-th BIM + # until we find a unique task with the maximum BIM value. + # and if there are still multiple tasks, we just use pop to select one of them + j = 0 + selected_tasks = copy.copy(ready_tasks) + while j < len(network.nodes): + max_bim = -1 + selected_tasks = set() + # iterate over all tasks and their BIM values + # add tasks with the maximum BIM value to the selected tasks + # if there is only one task with the maximum BIM value, we break the loop + for task, _bims in bims.items(): + # _bims is a list of tuples of (node, bim) sorted in descending order of BIM values + # So, we are consider the BIM value of the task on the jth processor + _, bim = _bims[j] + if bim == max_bim: + # if the BIM value is same as the max BIM value, we add the task to the selected tasks + selected_tasks.add(task) + elif bim > max_bim: + # if the BIM value is greater than the max BIM value, we update the max BIM value and the selected tasks + max_bim = bim + selected_tasks = {task} + if len(selected_tasks) == 1: + # if we have only one task with the maximum BIM value, we break the loop + break + j += 1 + + selected_task = selected_tasks.pop() + + # Section 3.2: Processor Selection + # compute revised bims for selected task and all nodes + revised_bims: Dict[Hashable, float] = { + node: ( + # schedule[node][-1].end is the end time of the last task scheduled on the node + (schedule[node][-1].end if schedule[node] else task_graph_arrival_time) + + # we add the BIL value of the task on the node to the end time to get the BIM value + bils[(selected_task, node)] + + ( + task_graph.nodes[selected_task]['weight'] / network.nodes[node]['weight'] * + max(len(ready_tasks)/len(task_graph.nodes)-1,0) + ) + ) + for node in network.nodes + } + + # select node with lowest revised bim + selected_node = min(revised_bims, key=revised_bims.get) + # If more than one processor have the same revised BIM value, we select the + # processor that makes the sum of the revised BIM values of other nodes on the + # processor maximum. + if len([node for node in network.nodes if revised_bims[node] == revised_bims[selected_node]]) > 1: + selected_node = max(network.nodes, key=lambda node: sum( + revised_bims[other_node] for other_node in network.nodes if other_node != node + )) + + # Schedule + start_time = max( + schedule[selected_node][-1].end if schedule[selected_node] else task_graph_arrival_time, + max(( + scheduled_tasks[child].end + ( + task_graph.edges[child, selected_task]['weight'] / + network.edges[selected_node, scheduled_tasks[child].node]['weight'] + ) + for child in task_graph.predecessors(selected_task) + ), default= task_graph_arrival_time) + ) + end_time = start_time + task_graph.nodes[selected_task]['weight'] / network.nodes[selected_node]['weight'] + new_task = Task( + node=selected_node, + name=selected_task, + start=start_time, + end=end_time + ) + schedule[selected_node].append(new_task) + scheduled_tasks[selected_task] = new_task + + # Update ready tasks + ready_tasks.update({ + child for child in task_graph.successors(selected_task) + if all(parent in scheduled_tasks for parent in task_graph.predecessors(child)) + }) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npbrute_force.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npbrute_force.py new file mode 100644 index 00000000..9332794f --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npbrute_force.py @@ -0,0 +1,83 @@ +import itertools +from typing import Dict, Hashable, List, Tuple +import copy + +import networkx as nx + +from ....scheduler import Scheduler, Task, DWScheduler + + +class ResidualBruteForceScheduler(DWScheduler): + """Brute force scheduler""" + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]], + ) -> Dict[Hashable, List[Task]]: + """Returns the best schedule (minimizing makespan) for a problem + instance using brute force + + Args: + network: Network + task_graph: Task graph + + Returns: + A dictionary of the schedule + """ + + # com_schedule: + # = {node: [] for node in network.nodes} + + + final_best_schedule = {} + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + + # get all topological sorts of the task graph + topological_sorts = list(nx.algorithms.dag.all_topological_sorts(task_graph)) + # get all valid mappings of the task graph nodes to the network nodes + mappings = [ + dict(zip(task_graph.nodes, mapping)) + for mapping in itertools.product(network.nodes, repeat=len(task_graph.nodes)) + ] + + best_schedule = None + best_makespan = float("inf") + for mapping in mappings: + for top_sort in topological_sorts: + tasks: Dict[int, Task] = {} + # schedule: Dict[int, List[Task]] = {} + # schedule is a copy of best_schedule + schedule = copy.deepcopy(final_best_schedule) + for task in top_sort: + node = mapping[task] + task_cost = task_graph.nodes[task]["weight"] + # get parents finish times + xfer time + ready_time = task_graph_arrival_time if task_graph.in_degree(task) == 0 else max([ + tasks[parent].end + ( + task_graph[parent][task]["weight"] / network[mapping[parent]][node]["weight"] + ) for parent in task_graph.predecessors(task) + ]) + # if node already has a task, get last tasks end time + if node in schedule: + ready_time = max(ready_time, schedule[node][-1].end, task_graph_arrival_time) + + node_speed = network.nodes[node]["weight"] + end_time = ready_time + task_cost / node_speed + tasks[task] = Task(node, task, ready_time, end_time) + schedule.setdefault(node, []).append(tasks[task]) + + makespan = max([max([task.end for task in tasks]) for tasks in schedule.values() if len(tasks) > 0]) + if makespan < best_makespan: + best_makespan = makespan + best_schedule = schedule + + # here + final_best_schedule = best_schedule + + + # fill empty nodes with empty lists + final_best_schedule = {node: final_best_schedule.get(node, []) for node in network.nodes} + + return final_best_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npcpop.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npcpop.py new file mode 100644 index 00000000..3307656c --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npcpop.py @@ -0,0 +1,169 @@ +import logging +import pathlib +from typing import Dict, Hashable, List, Tuple, Optional +import heapq + +import networkx as nx +import numpy as np +from saga.utils.draw import draw_gantt + +from ....scheduler import Task, DWScheduler +from ....utils.tools import get_insert_loc +from ...cpop import upward_rank +from ...cpop import cpop_ranks + +thisdir = pathlib.Path(__file__).resolve().parent + + +def heft_rank_sort(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + rank = upward_rank(network, task_graph) + topological_sort = {node: i for i, node in enumerate(reversed(list(nx.topological_sort(task_graph))))} + rank = {node: (rank[node] + topological_sort[node]) for node in rank} + return sorted(list(rank.keys()), key=rank.get, reverse=True) + +def cpop_ranking(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + ranks = cpop_ranks(network, task_graph) + start_tasks = [task for task in task_graph if task_graph.in_degree(task) == 0] + start_tasks_sorted = sorted(start_tasks, key=lambda t: ranks[t], reverse=True) + pq = [(-ranks[task], task) for task in start_tasks_sorted] + heapq.heapify(pq) + queue = [] + while pq: + _, task_name = heapq.heappop(pq) + queue.append(task_name) + ready_tasks = [ + succ for succ in task_graph.successors(task_name) + if all(pred in queue for pred in task_graph.predecessors(succ)) + ] + for ready_task in ready_tasks: + heapq.heappush(pq, (-ranks[ready_task], ready_task)) + + return queue + + +class NPCpopScheduler(DWScheduler): + + @staticmethod + def get_runtimes( + network: nx.Graph, task_graph: nx.DiGraph + ) -> Tuple[ + Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]],]: + + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug( + "Task %s on node %s has runtime %s", + task, + node, + runtimes[node][task], + ) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, + src, + dst_task, + dst, + commtimes[src, dst][src_task, dst_task], + ) + + return runtimes, commtimes + + def _schedule( + self, + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[ + Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float] + ], + schedule_order: List[Hashable], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: Optional[float] = 0 + ) -> Dict[Hashable, List[Task]]: + + + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + + task_name: Hashable + logging.debug("Schedule order: %s", schedule_order) + for task_name in schedule_order: + + min_finish_time = np.inf + best_node = None + for node in network.nodes: # Find the best node to run the task + max_arrival_time: float = max( + [ + task_graph_arrival_time, + *[ + task_schedule[parent].end + + ( + commtimes[(task_schedule[parent].node, node)][ + (parent, task_name) + ] + ) + for parent in task_graph.predecessors(task_name) + ], + ] + ) + + runtime = runtimes[node][task_name] + idx, start_time = get_insert_loc( + comp_schedule[node], max_arrival_time, runtime + ) + + logging.debug( + "Testing task %s on node %s: start time %s, finish time %s", + task_name, + node, + start_time, + start_time + runtime, + ) + + finish_time = start_time + runtime + if finish_time < min_finish_time: + min_finish_time = finish_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task_name] + task = Task( + best_node[0], task_name, min_finish_time - new_runtime, min_finish_time + ) + comp_schedule[best_node[0]].insert(best_node[1], task) + task_schedule[task_name] = task + + return comp_schedule + + def schedule( + self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[str, List[Task]]: + + comp_schedule: Dict[Hashable, List[Task]] = None + + for index, task_graph_tupple in enumerate(task_graphs): + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + runtimes, commtimes = NPCpopScheduler.get_runtimes(network, task_graph) + schedule_order = cpop_ranking(network, task_graph) + + comp_schedule = self._schedule( + network, task_graph, runtimes, commtimes, schedule_order, comp_schedule, task_graph_arrival_time + ) + + return comp_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npdps.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npdps.py new file mode 100644 index 00000000..9a2a5914 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npdps.py @@ -0,0 +1,200 @@ +import logging +import pathlib +from typing import Dict, Hashable, List, Tuple +import networkx as nx +import numpy as np + +from ....scheduler import Scheduler, Task +from ....utils.tools import check_instance_simple, get_insert_loc +thisdir = pathlib.Path(__file__).resolve().parent + + +# options for mode can be 'avg', 'median', 'max' or 'min' +mode = 'avg' + +def calc_TEC(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph) -> float: + match mode: + case 'avg': + return avg_TEC(task, network, task_graph) + case 'median': + return median_TEC(task, network, task_graph) + case 'max': + return max_TEC(task, network, task_graph) + case 'min': + return min_TEC(task, network, task_graph) + +#max Task Execution Cost +def max_TEC(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph) -> float: + return max(task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] for node in network.nodes) + +#avg Task Execution Cost +def avg_TEC(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph) -> float: + return sum(task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] for node in network.nodes) / len(network.nodes) + +#median Task Execution Cost +def median_TEC(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph) -> float: + return np.median(task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] for node in network.nodes) + +#min Task Execution Cost +def min_TEC(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph) -> float: + return min(task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] for node in network.nodes) + +def calc_TL(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph, assigned_tasks: dict) -> float: + if task_graph.predecessors(task) is None: + return 0 + max_TL = 0 + for pred in task_graph.predecessors(task): + #This is the first term of the equation + TL = calc_TL(pred, network, task_graph, assigned_tasks) + + #This is the second term of the equation + if assigned_tasks.get(pred) is None: + TL += calc_TEC(pred, network, task_graph) + else: + TL += task_graph.nodes[pred]['weight'] / network.nodes[assigned_tasks.get(pred)]['weight'] + + #This is the third term of the equation + if assigned_tasks.get(pred) is None or assigned_tasks.get(task) is None: + TL += task_graph.edges[pred, task]['weight'] + # elif assigned_tasks.get(pred) == assigned_tasks.get(task): + # TL += 0 + else: + TL += task_graph.edges[pred, task]['weight'] / network.edges[assigned_tasks.get(pred), assigned_tasks.get(task)]['weight'] + + if(TL > max_TL): + max_TL = TL + return max_TL + +def calc_BL(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph, assigned_tasks: dict) -> float: + if task_graph.successors(task) is None: + if assigned_tasks.get(task) is None: + return calc_TEC(task, network, task_graph) + else: + return task_graph.nodes[task]['weight'] / network.nodes[assigned_tasks.get(task)]['weight'] + max_BL = 0 + for succ in task_graph.successors(task): + #This is the first term of the equation + BL = calc_BL(succ, network, task_graph, assigned_tasks) + + #This is the second term of the equation + if assigned_tasks.get(succ) is None or assigned_tasks.get(task) is None: + BL += task_graph.edges[task, succ]['weight'] + # elif assigned_tasks.get(succ) == assigned_tasks.get(task): + # BL += 0 + else: + BL += task_graph.edges[task, succ]['weight'] / network.edges[assigned_tasks.get(task), assigned_tasks.get(succ)]['weight'] + + #This is the third term of the equation + if assigned_tasks.get(task) is None: + BL += calc_TEC(task, network, task_graph) + else: + BL += task_graph.nodes[task]['weight'] / network.nodes[assigned_tasks.get(task)]['weight'] + + if(BL > max_BL): + max_BL = BL + return max_BL + +def calc_priority(task: Hashable, network: nx.Graph, task_graph: nx.DiGraph, assigned_tasks: dict) -> float: + return calc_BL(task, network, task_graph, assigned_tasks) - calc_TL(task, network, task_graph, assigned_tasks) + +class ResidualDPSScheduler(Scheduler): + def __init__(self): + super().__init__() + + @staticmethod + def get_runtimes(network: nx.Graph, task_graph: nx.DiGraph) -> Tuple[Dict[Hashable, Dict[Hashable, float]], Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]]: + """Get the expected runtimes of all tasks on all nodes. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Tuple[Dict[Hashable, Dict[Hashable, float]], Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]]: A tuple of dictionaries mapping nodes to a dictionary of tasks and their runtimes and edges to a dictionary of tasks and their communication times. + The first dictionary maps nodes to a dictionary of tasks and their runtimes. + The second dictionary maps edges to a dictionary of task dependencies and their communication times. + """ + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug(f"Task {task} on node {node} has runtime {runtimes[node][task]}") + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug(f"Task {src_task} on node {src} to task {dst_task} on node {dst} has communication time {commtimes[src, dst][src_task, dst_task]}") + + return runtimes, commtimes + + def _schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[str, List[Task]]: + + + comp_schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + + task_list = list(nx.topological_sort(task_graph)) + assigned_tasks = {} + task_list.sort(key=lambda x: calc_priority(x, network, task_graph, assigned_tasks), reverse=True) + ready_list = task_list.copy() + + task_schedule: Dict[Hashable, Task] = {} + runtimes, commtimes = ResidualDPSScheduler.get_runtimes(network, task_graph) + + while len(ready_list) > 0: + task = ready_list.pop(0) + #Earliest Finish Time + min_finish_time = np.inf + best_node = None + for node in network.nodes: + logging.debug(f"Trying to assign task {task} to node {node}") + if(task_graph.predecessors(task) is not None): + max_arrival_time: float = max( + [ + task_graph_arrival_time, *[ + task_schedule[parent].end + ( + commtimes[(task_schedule[parent].node, node)][(parent, task)] + ) + for parent in task_graph.predecessors(task) + ] + ] + ) + else: + max_arrival_time = task_graph_arrival_time + runtime = runtimes[node][task] + idx, start_time = get_insert_loc(comp_schedule[node], max_arrival_time, runtime) + + finish_time = start_time + runtime + if finish_time < min_finish_time: + min_finish_time = finish_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task] + task_ob = Task(best_node[0], task, min_finish_time - new_runtime, min_finish_time) + comp_schedule[best_node[0]].insert(best_node[1], task_ob) + task_schedule[task] = task_ob + assigned_tasks[task] = best_node[0] + ready_list.sort(key=lambda x: calc_priority(x, network, task_graph, assigned_tasks), reverse=True) + + return comp_schedule + + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[str, List[Task]]: + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + check_instance_simple(network, task_graph) + return self._schedule(network, task_graphs) \ No newline at end of file diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npduplex.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npduplex.py new file mode 100644 index 00000000..6198bd9d --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npduplex.py @@ -0,0 +1,33 @@ +from typing import Dict, Hashable, List, Tuple + +import networkx as nx + +from ....scheduler import Scheduler, Task +from .npmaxmin import NPMaxMinScheduler +from .npminmin import NPMinMinScheduler + + +class ResidualDuplexScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Duplex scheduler""" + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the best schedule (minimizing makespan) for a problem instance using dupkex + + Args: + network: Network + task_graph: Task graph + + Returns: + A dictionary of the schedule + """ + minmin_schedule = NPMinMinScheduler().schedule(network, task_graphs) + maxmin_schedule = NPMaxMinScheduler().schedule(network, task_graphs) + + + minmin_makespan = max([0 if not tasks else tasks[-1].end for tasks in minmin_schedule.values()]) + + maxmin_makespan = max([0 if not tasks else tasks[-1].end for tasks in maxmin_schedule.values()]) + + + if minmin_makespan <= maxmin_makespan: + return minmin_schedule + return maxmin_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npetf.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npetf.py new file mode 100644 index 00000000..ed1ffd14 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npetf.py @@ -0,0 +1,128 @@ +from typing import Dict, Hashable, List, Tuple, Set + +import networkx as nx +import numpy as np + +from saga.scheduler import Task + +from ....scheduler import Task +from ....scheduler import Scheduler + + +class ResidualETFScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Earliest Task First scheduler""" + + def _get_start_times(self, + tasks: Dict[Hashable, Task], + ready_tasks: Set[Hashable], + ready_nodes: Set[Hashable], + task_graph: nx.DiGraph, + task_graph_arrival_time: float, + network: nx.Graph) -> Dict[Hashable, Tuple[Hashable, float]]: + """Returns the earliest possible start times of the ready tasks on the ready nodes + + + Args: + tasks (Dict[Hashable, Task]): The tasks. + ready_tasks (Set[Hashable]): The ready tasks. + ready_nodes (Set[Hashable]): The ready nodes. + task_graph (nx.DiGraph): The task graph. + network (nx.Graph): The network. + + Returns: + Dict[Hashable, Tuple[Hashable, float]]: The start times of the ready tasks on the ready nodes. + """ + start_times = {} + for task in ready_tasks: + min_start_time, min_node = np.inf, None + for node in ready_nodes: + max_arrival_time = max([ + task_graph_arrival_time, *[ + tasks[parent].end + ( + task_graph.edges[parent, task]["weight"] / + network.edges[tasks[parent].node, node]["weight"] + ) for parent in task_graph.predecessors(task) + ] + ]) + if max_arrival_time < min_start_time: + min_start_time = max_arrival_time + min_node = node + start_times[task] = min_node, min_start_time + return start_times + + def _get_ready_tasks(self, tasks: Dict[Hashable, Task], task_graph: nx.DiGraph) -> Set[Hashable]: + """Returns the ready tasks + + Args: + tasks (Dict[Hashable, Task]): The tasks. + task_graph (nx.DiGraph): The task graph. + + Returns: + Set[Hashable]: The ready tasks. + """ + return { + task for task in task_graph.nodes + if task not in tasks and all(pred in tasks for pred in task_graph.predecessors(task)) + } + + + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the best schedule (minimizing makespan) for a problem instance using ETF + + Args: + network: Network + task_graph: Task graph + + Returns: + A dictionary of the schedule + """ + current_moment = 0 + next_moment = np.inf + + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + tasks: Dict[Hashable, Task] = {} + + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + + # while len(tasks) < len(task_graph.nodes): + while set(task_graph.nodes).issubset(set(tasks.keys())) == False: + ready_tasks = self._get_ready_tasks(tasks, task_graph) + ready_nodes = { + node for node in network.nodes + if not schedule[node] or schedule[node][-1].end <= current_moment + } + while ready_tasks and ready_nodes: + start_times = self._get_start_times(tasks, ready_tasks, ready_nodes, task_graph, task_graph_arrival_time, network) + task_to_schedule = min(list(start_times.keys()), key=lambda task: start_times[task][1]) + node_to_schedule_on, start_time = start_times[task_to_schedule] + + start_time = max(start_time, current_moment) + + if start_time <= next_moment: + new_task = Task( + node=node_to_schedule_on, + name=task_to_schedule, + start=start_time, + end=start_time + ( + task_graph.nodes[task_to_schedule]["weight"] / + network.nodes[node_to_schedule_on]["weight"] + ) + ) + schedule[node_to_schedule_on].append(new_task) + tasks[task_to_schedule] = new_task + ready_tasks.remove(task_to_schedule) + ready_nodes.remove(node_to_schedule_on) + if new_task.end < next_moment: + next_moment = new_task.end + else: + break + + current_moment = next_moment + next_moment = min([np.inf, *[task.end for task in tasks.values() if task.end > current_moment]]) + + return schedule + \ No newline at end of file diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npfastest_node.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npfastest_node.py new file mode 100644 index 00000000..a2327a9f --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npfastest_node.py @@ -0,0 +1,56 @@ +from typing import Dict, Hashable, List, Tuple +import networkx as nx +from ....scheduler import Scheduler, Task, DWScheduler + + +class NPFastestNodeScheduler(DWScheduler): + """Schedules all tasks on the node with the highest processing speed""" + + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + """Schedules all tasks on the node with the highest processing speed + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: A schedule mapping nodes to a list of tasks. + + Raises: + ValueError: If the instance is not valid + """ + fastest_node = max(network.nodes, key=lambda node: network.nodes[node]["weight"]) + schedule = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + # add tasks to fastest node in order (topological sort) + free_time = 0 + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + for task_name in nx.topological_sort(task_graph): + task_size = task_graph.nodes[task_name]["weight"] + exec_time = task_size / network.nodes[fastest_node]["weight"] + + # For most instances, the data should probably arrive immediately + # since everything is executing on the same node. + data_arrival_time = task_graph_arrival_time + if task_graph.in_degree(task_name) > 0: + data_arrival_time = max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task_name]["weight"] / network.edges[fastest_node, fastest_node]["weight"] + ) + for pred in task_graph.predecessors(task_name) + ) + start_time = max(free_time, data_arrival_time) + + new_task = Task(fastest_node, task_name, start_time, start_time + exec_time) + schedule[fastest_node].append(new_task) + scheduled_tasks[task_name] = new_task + free_time = new_task.end + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npfcp.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npfcp.py new file mode 100644 index 00000000..39a57097 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npfcp.py @@ -0,0 +1,179 @@ +from queue import PriorityQueue +from typing import Dict, Hashable, List, Optional, Set, Tuple + +import networkx as nx + +from ....scheduler import Scheduler, Task + +def get_mcp_priorities(network: nx.Graph, task_graph: nx.DiGraph) -> Dict[Hashable, float]: + """Returns the priorities of the tasks on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, float]: The priorities of the tasks on the network. + """ + network = network.copy() + task_graph = task_graph.copy() + + avg_node_speed = sum( + network.nodes[node]['weight'] for node in network.nodes + ) / len(network.nodes) + avg_comm_speed = sum( + network.edges[edge]['weight'] for edge in network.edges + # if there is only one node, the avg is the self-loop edge weight + if edge[0] != edge[1] or len(network.nodes) == 1 + ) / len(network.edges) + + # scale task weights by average speeds + for task in task_graph.nodes: + task_graph.nodes[task]['weight'] /= avg_node_speed + for edge in task_graph.edges: + task_graph.edges[edge]['weight'] /= avg_comm_speed + + # add dummy src and sink tasks + src = '__mcp_src__' + sink = '__mcp_sink__' + task_graph.add_node(src, weight=1e-9) + task_graph.add_node(sink, weight=1e-9) + for task in task_graph.nodes: + if task not in (src, sink) and task_graph.in_degree(task) == 0: + task_graph.add_edge(src, task, weight=1e-9) + if task not in (src, sink) and task_graph.out_degree(task) == 0: + task_graph.add_edge(task, sink, weight=1e-9) + + longest_path_lengths = {} + for task in reversed(list(nx.topological_sort(task_graph))): + avg_exec_time = task_graph.nodes[task]['weight'] / avg_node_speed + if task == sink: + longest_path_lengths[task] = avg_exec_time + else: + longest_path_lengths[task] = avg_exec_time + max( + longest_path_lengths[succ] + task_graph.edges[task, succ]['weight'] / avg_comm_speed + for succ in task_graph.successors(task) + ) + + critical_path_length = max(longest_path_lengths.values()) + # paths with greatest priority have the least critical path length - longest path length + priorities = { + task: critical_path_length - longest_path_lengths[task] + for task in task_graph.nodes + if task not in (src, sink) + } + return priorities + + +class ResidualFCPScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Fast Critical Path Scheduler + + Source: https://doi.org/10.1145/305138.305162 + Note: This original algorithm assumes the network communication/computation speeds are the same for all nodes. + This implementation allows for different speeds by scaling the task weights by the average speeds (the algorithm + will still perform poorly for heterogeneous networks, but it will at least produce valid schedules). + """ + def __init__(self, priority_queue_size: Optional[int] = None): + super().__init__() + self.priority_queue_size = priority_queue_size + + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the best schedule (minimizing makespan) for a problem instance using FCP(Fastest Critical Path) + + Args: + network: Network + task_graph: Task graph + + Returns: + A dictionary of the schedule + """ + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + queue_priority = PriorityQueue(maxsize=self.priority_queue_size or len(network.nodes)) + queue_fifo = [] + queued_tasks: Set[Hashable] = set() + priorities = get_mcp_priorities(network, task_graph) + + def add_ready_task(task: Hashable): + if queue_priority.qsize() < queue_priority.maxsize: + queue_priority.put((priorities[task], task)) + else: + queue_fifo.append(task) + queued_tasks.add(task) + + def select_ready_task() -> Hashable: + task = queue_priority.get()[1] + if queue_fifo: + fifo_task = queue_fifo.pop(0) + queue_priority.put((priorities[fifo_task], fifo_task)) + return task + + def get_exec_time(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + def get_eat(node: Hashable) -> float: + eat = schedule[node][-1].end if schedule.get(node) else task_graph_arrival_time + return eat + + def get_fat(task: Hashable, node: Hashable) -> float: + fat = task_graph_arrival_time if task_graph.in_degree(task) <= 0 else max( + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ) + return fat + + def get_start_time(task: Hashable, node: Hashable) -> float: + return max(get_eat(node), get_fat(task, node)) + + def select_processor(task: Hashable) -> Hashable: + # processor that becomes idle first + p_start = min( + network.nodes, + key=lambda node: schedule[node][-1].end if schedule[node] else task_graph_arrival_time + ) + # processor with predecessor that last finishes + predecessors: List[Hashable] = list(task_graph.predecessors(task)) + if not predecessors: + return p_start + pred = max( + predecessors, + key=lambda task: scheduled_tasks[task].end + ) + p_arrive = scheduled_tasks[pred].node + + if get_start_time(task, p_start) <= get_start_time(task, p_arrive): + return p_start + return p_arrive + + for task in task_graph.nodes: + if task_graph.in_degree(task) == 0: + add_ready_task(task) + + # while len(scheduled_tasks) < len(task_graph.nodes): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + task = select_ready_task() + node = select_processor(task) + start_time = get_start_time(task, node) + exec_time = get_exec_time(task, node) + scheduled_tasks[task] = Task( + node=node, + name=task, + start=start_time, + end=start_time+exec_time + ) + schedule.setdefault(node, []).append(scheduled_tasks[task]) + for succ in task_graph.successors(task): + if all(pred in scheduled_tasks for pred in task_graph.predecessors(succ)): + if succ not in queued_tasks: + add_ready_task(succ) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npflb.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npflb.py new file mode 100644 index 00000000..2cb0f543 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npflb.py @@ -0,0 +1,279 @@ +import json +import logging +from pprint import pformat +from queue import PriorityQueue +from typing import Dict, Hashable, List, Tuple + +import networkx as nx +from networkx import DiGraph, Graph + +from saga.scheduler import Task + +from ....scheduler import Scheduler, Task, DWScheduler + + +class NPFLBScheduler(DWScheduler): + """The FLB (Fast Load Balancing) scheduler. + + Source: https://doi.org/10.1109/ICPP.1999.797442 + Note: They assume homogenous comp/comm speeds, so we will scale the weights in the task graph + by the average (but it will still perform poorly on heterogeneous networks). We also schedule + to the fastest node whenever the original algorithm schedules to an arbitrary node. + """ + def schedule(self, network: Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + network = network.copy() + task_graph = task_graph.copy() + + avg_comm_speed = sum( + network.edges[edge]['weight'] for edge in network.edges + if edge[0] != edge[1] or len(network.nodes) == 1 + ) / len(network.edges) + + fastest_node = max(network.nodes, key=lambda node: network.nodes[node]['weight']) + + def getEP(task: Hashable) -> Hashable: # pylint: disable=invalid-name + """Get Enabling Processor (EP) of a task. + + The enabling processor of a ready task t, EP (t) is the processor from which + the last message arrives. + + NOTE: Because this algorithm assumes homogenous comp/comm speeds, the EP is + *not* the processor from which the last message actually arrives, but rather + the processor from which the last message *would* arrive if the network was + homogenous (with comm speed equal to the average comm speed). + + Args: + task (Hashable): The task. + + Returns: + Hashable: The enabling processor of the task. + """ + enabling_task = max( + task_graph.predecessors(task), + key=lambda pred: scheduled_tasks[pred].end + task_graph.edges[pred, task]['weight'] / avg_comm_speed + ) + return scheduled_tasks[enabling_task].node + + + def getLMT(task: Hashable) -> float: # pylint: disable=invalid-name + """Get the Last Message Arrival Time (LMT) + + NOTE: Because the original algorithm assumes homogenous comp/comm speeds, we will + modify it to use the *actual* communication time between the tasks, rather than + assume they are all equal to the average communication time. + + Args: + task (Hashable): The task. + + Returns: + float: The LMT of the task. + """ + if task_graph.in_degree(task) == 0: + return task_graph_arrival_time + + return max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task]['weight'] / + avg_comm_speed + ) + for pred in task_graph.predecessors(task) + ) + + def getEMT(task: Hashable, node: Hashable) -> float: # pylint: disable=invalid-name + """Get the Effective Message Arrival Time (EMT) + + Since our networks have 0 comm delay between nodes and we extend for + heterogeneous networks, this is just getLMT(task). + + Args: + task (Hashable): The task. + node (Hashable): The node. + + Returns: + float: The EMT of the task. + """ + if task_graph.in_degree(task) == 0: + return task_graph_arrival_time + + return max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task]['weight'] / + network.edges[scheduled_tasks[pred].node, node]['weight'] + ) + for pred in task_graph.predecessors(task) + ) + + def getPRT(node: Hashable) -> float: # pylint: disable=invalid-name + return 0 if not schedule[node] else schedule[node][-1].end + + def getEST(task: Hashable, node: Hashable) -> float: # pylint: disable=invalid-name + return max(getPRT(node), getEMT(task, node)) + + non_ep_tasks = PriorityQueue() + emt_ep_tasks = { + node: PriorityQueue() + for node in network.nodes + } + lmt_ep_tasks = { + node: PriorityQueue() + for node in network.nodes + } + all_procs = PriorityQueue() + active_procs = PriorityQueue() + + for task in task_graph.nodes: + if task_graph.in_degree(task) == 0: + non_ep_tasks.put((task_graph_arrival_time, task)) + + for node in network.nodes: + all_procs.put((0, node)) + + def schedule_task() -> Tuple[Hashable, Hashable]: + # get head of active_procs without removing + _, proc1 = active_procs.queue[0] if active_procs.queue else (0, None) + task1 = None + if proc1 is not None: + _, task1 = emt_ep_tasks[proc1].queue[0] if emt_ep_tasks[proc1].queue else (0, None) + _, proc2 = all_procs.queue[0] if all_procs.queue else (0, None) + _, task2 = non_ep_tasks.queue[0] if non_ep_tasks.queue else (0, None) + + est_t1_p1 = getEST(task1, proc1) if proc1 is not None and task1 is not None else float('inf') + est_t2_p2 = getEST(task2, proc2) if proc2 is not None and task2 is not None else float('inf') + if est_t1_p1 == float('inf') and est_t2_p2 == float('inf'): + # NOTE: This should never happen. If it does, it means that there is a bug in the algorithm. + # log queue values + logging.debug("active_procs: %s", pformat(active_procs.queue)) + logging.debug("all_procs: %s", pformat(all_procs.queue)) + logging.debug("non_ep_tasks: %s", pformat(non_ep_tasks.queue)) + logging.debug("emt_ep_tasks: %s", pformat({node: pformat(emt_ep_tasks[node].queue) for node in emt_ep_tasks})) + logging.debug("schedule: %s", pformat(schedule)) + logging.debug("task_graph: %s", json.dumps(nx.readwrite.json_graph.node_link_data(task_graph))) + logging.debug("network: %s", json.dumps(nx.readwrite.json_graph.node_link_data(network))) + + + raise RuntimeError(f"No tasks to schedule. proc1={proc1}, task1={task1}, proc2={proc2}, task2={task2}") + if est_t1_p1 <= est_t2_p2: + new_task = Task( + node=proc1, + name=task1, + start=est_t1_p1, + end=est_t1_p1 + task_graph.nodes[task1]['weight'] / network.nodes[proc1]['weight'] + ) + schedule[proc1].append(new_task) + scheduled_tasks[task1] = new_task + + assert active_procs.get()[1] == proc1 + assert emt_ep_tasks[proc1].get()[1] == task1 # dequeue task from emt_ep_tasks[p1] + lmt_ep_tasks[proc1].queue = [ # remove task1 from lmt_ep_tasks[p1] + (priority, task) for priority, task in lmt_ep_tasks[proc1].queue + if task != task1 + ] + return task1, proc1 + else: + # schedule task t2 on processor p2 + new_task = Task( + node=proc2, + name=task2, + start=est_t2_p2, + end=est_t2_p2 + task_graph.nodes[task2]['weight'] / network.nodes[proc2]['weight'] + ) + schedule[proc2].append(new_task) + scheduled_tasks[task2] = new_task + + assert all_procs.get()[1] == proc2 # dequeue proc2 from all_procs + + # NOTE: this is not in the original algorithm, but it seems necessary + all_procs.put((getPRT(proc2), proc2)) # add proc2 with new priority PRT + + assert non_ep_tasks.get()[1] == task2 # dequeue task from non_ep_tasks + return task2, proc2 + + def update_task_lists(task: Hashable, proc: Hashable): + while True: + _, task = lmt_ep_tasks[proc].queue[0] if lmt_ep_tasks[proc].queue else (0, None) + if task is None: + break + if getLMT(task) >= getPRT(proc): # last message arrival time of t >= processor ready time + break + + assert lmt_ep_tasks[proc].get()[1] == task # dequeue task from lmt_ep_tasks[p] + emt_ep_tasks[proc].queue = [ # remove task from emt_ep_tasks[p] + (priority, _task) for priority, _task in emt_ep_tasks[proc].queue + if _task != task + ] + non_ep_tasks.put((getLMT(task), task)) # enqueue task in non_ep_tasks + + def update_proc_lists(task: Hashable, proc: Hashable): + _, task = emt_ep_tasks[proc].queue[0] if emt_ep_tasks[proc].queue else (0, None) + if task is None: + # remove proc from active_procs + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != proc + ] + else: + # remove proc from active_procs and add back with priority est + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != proc + ] + active_procs.put((getEST(task, proc), proc)) + + def update_ready_tasks(task: Hashable, proc: Hashable): + for succ in task_graph.successors(task): + if succ in scheduled_tasks: # not in original algorithm, is it necessary? + continue + + if not all(pred in scheduled_tasks for pred in task_graph.predecessors(succ)): + continue + + enabling_proc = getEP(succ) # get enabling processor of succ + lmt = getLMT(succ) # get last message arrival time of succ + emt = getEMT(succ, enabling_proc) # get effective message arrival time of succ on enabling processor + if lmt < getPRT(enabling_proc): # if lmt < processor ready time + # task perhaps should not execute on ep, since this indicates that the ep is not ready + # when the task is ready to execute + # enqueue succ with priority lmt in non_ep_tasks + non_ep_tasks.put((lmt, succ)) + else: + _, head_proc = emt_ep_tasks[enabling_proc].queue[0] if emt_ep_tasks[enabling_proc].queue else (0, None) + if head_proc is None: + # enqueue ep with priority est in active_procs + active_procs.put((getEST(succ, enabling_proc), enabling_proc)) + else: + # get next task to execute on ep (according to current priorities) + head_task = emt_ep_tasks[enabling_proc].queue[0][1] + # get the emt of this task + _emt = getEMT(head_task, enabling_proc) + if emt < _emt: + # succ should execute before head_task on ep because it becomes ready before head_task + # update ep with priority max(EMT, prt) in active_procs + # first, remove ep from active_procs + new_priority = max(emt, getPRT(enabling_proc)) + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != enabling_proc + ] + active_procs.put((new_priority, enabling_proc)) + # enqueue succ with priority emt in emt_ep_tasks[ep] + emt_ep_tasks[enabling_proc].put((emt, succ)) + # enqueue succ with priority lmt in lmt_ep_tasks[ep] + lmt_ep_tasks[enabling_proc].put((lmt, succ)) + + # while len(scheduled_tasks) < len(task_graph.nodes): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + task, proc = schedule_task() + update_task_lists(task, proc) + update_proc_lists(task, proc) + update_ready_tasks(task, proc) + + return schedule + diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npgdl.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npgdl.py new file mode 100644 index 00000000..86ca5a42 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npgdl.py @@ -0,0 +1,209 @@ +from functools import lru_cache, partial +from typing import Dict, Hashable, List, Tuple + +import networkx as nx +import numpy as np + +from ....scheduler import Scheduler, Task + + +class ResidualGDLScheduler(Scheduler): + """Generalized Dynamic Level Scheduler + + Source: https://doi.org/10.1109/71.207593 + Notes: Considers homogenous communication speeds (not homogenous compute speeds, though) + """ + def __init__(self, dynamic_level: int = 2): + super().__init__() + self.dynamic_level = dynamic_level + if dynamic_level not in (1, 2): + raise ValueError("dynamic_level must be 1 or 2") + + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + execution_time = {} + for task in task_graph.nodes: + for node in network.nodes: + execution_time[task, node] = ( + task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + ) + + communication_time = {} + for dep in task_graph.edges: + for link in network.edges: + communication_time[dep, tuple(sorted(link))] = ( + task_graph.edges[dep]['weight'] / network.edges[link]['weight'] + ) + + median_execution_time_per_task = { + task: np.median([ + execution_time[task, node] + for node in network.nodes + ]) + for task in task_graph.nodes + } + + median_execution_time = np.median([ + execution_time[task, node] + for task in task_graph.nodes + for node in network.nodes + ]) + + delta_execution_time = {} + for task in task_graph.nodes: + for node in network.nodes: + delta_execution_time[task, node] = ( + median_execution_time - execution_time[task, node] + ) + + # the static level of a task is the largest sum of execution times + # along any directed path from the task to a sink + static_level = {} + for task in reversed(list(nx.topological_sort(task_graph))): + if task_graph.out_degree(task) == 0: + static_level[task] = median_execution_time_per_task[task] + else: + static_level[task] = median_execution_time_per_task[task] + max( + static_level[succ] + task_graph.edges[task, succ]['weight'] + for succ in task_graph.successors(task) + ) + + @lru_cache(maxsize=None) + def data_available(task: str, node: str) -> float: + """returns time when data is available to execute task on node""" + if task_graph.in_degree(task) == 0: + return task_graph_arrival_time + return max( + scheduled_tasks[pred].end + ( # finish time + communication time + task_graph.edges[pred, task]['weight'] / + network.edges[scheduled_tasks[pred].node, node]['weight'] + ) + for pred in task_graph.predecessors(task) + ) + + @lru_cache(maxsize=None) + def node_available(node: str) -> float: + """returns time when node is available to execute task""" + if len(schedule[node]) == 0: + return 0 + return max(task.end for task in schedule[node]) + + @lru_cache(maxsize=None) + def dynamic_level_1(task: str, node: str) -> float: + return static_level[task] - max( + data_available(task, node), + node_available(node) + ) + delta_execution_time[task, node] + + largest_output_descendants = { + task: max( + task_graph.successors(task), + key=partial( + lambda t, s: task_graph.edges[t, s]['weight'], + task + ) + ) + for task in task_graph.nodes + if task_graph.out_degree(task) > 0 + } + + @lru_cache(maxsize=None) + def descendant_earliest_finish(task, child, node): + """returns earliest finish time of child's descendants on node + + NOTE: slight difference from paper, comm time is *inside* the + min function since we have heterogeneous comm speeds. This is + backwards-compatible with the paper's definition when the comm + speeds are homogeneous. + """ + if len(network.nodes) == 1: + return np.inf + return min( + communication_time[(task, child), tuple(sorted((node, other)))] + + execution_time[child, other] + for other in network.nodes + if node != other + ) + + @lru_cache(maxsize=None) + def descendant_consideration(task: str, node: str) -> float: + dc = largest_output_descendants.get(task) # pylint: disable=invalid-name + if dc is None: + return 0 + return median_execution_time_per_task[dc] - min( + execution_time[dc, node], + descendant_earliest_finish(task, dc, node) + ) + + @lru_cache(maxsize=None) + def dynamic_level_2(task: str, node: str) -> float: + return dynamic_level_1(task, node) + descendant_consideration(task, node) + + @lru_cache(maxsize=None) + def preferred_node(task: str) -> str: + return min( + network.nodes, + key=lambda node: dynamic_level(task, node) + ) + + dynamic_level = dynamic_level_1 if self.dynamic_level == 1 else dynamic_level_2 + @lru_cache(maxsize=None) + def cost(task: str) -> float: + return dynamic_level(task, preferred_node(task)) - max( + dynamic_level(task, other) + for other in network.nodes + ) + + @lru_cache(maxsize=None) + def global_dynamic_level(task: str) -> float: + return dynamic_level(task, preferred_node(task)) + cost(task) + + def clear_caches(): + """Clears the scheduler's caches""" + data_available.cache_clear() + node_available.cache_clear() + dynamic_level_1.cache_clear() + dynamic_level_2.cache_clear() + descendant_earliest_finish.cache_clear() + descendant_consideration.cache_clear() + dynamic_level.cache_clear() + cost.cache_clear() + global_dynamic_level.cache_clear() + + # while len(scheduled_tasks) < len(task_graph.nodes): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + candidate_tasks = [ + task for task in task_graph.nodes + if task not in scheduled_tasks and all( + pred in scheduled_tasks + for pred in task_graph.predecessors(task) + ) + ] + task = min(candidate_tasks, key=global_dynamic_level) + node = preferred_node(task) + start_time = max( + data_available(task, node), + node_available(node) + ) + exec_time = execution_time[task, node] + new_task = Task( + node=node, + name=task, + start=start_time, + end=start_time+exec_time + ) + scheduled_tasks[task] = new_task + schedule[node].append(new_task) + + clear_caches() + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/nphbmct.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/nphbmct.py new file mode 100644 index 00000000..92d4b351 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/nphbmct.py @@ -0,0 +1,374 @@ +import logging +from typing import Dict, Hashable, List, Tuple, Optional + +import networkx as nx +import numpy as np + +from ....scheduler import Scheduler, Task +from ...heft import heft_rank_sort + +def hbmct_rank_sort(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + """Sort tasks based on their rank (as defined in the HEFT paper). + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + List[Hashable]: The sorted list of tasks. + """ + + return heft_rank_sort(network, task_graph) + +def hbmct_create_groups(network: nx.Graph, task_graph: nx.DiGraph) -> List[List[Hashable]]: + """Create Independent Groups for scheduling. + Keep going down the sorted list and create a new group everytime there is a task depending on the current groups. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + List[List[Hashable]]: List of groups. + """ + rankings = hbmct_rank_sort(network, task_graph) + groups = [[rankings[0]]] + logging.debug("Upward: Rankings %s", rankings) + for task_name in rankings[1:]: + is_same_group = True + for predecessor in task_graph.predecessors(task_name): + if predecessor in groups[-1]: + groups.append([task_name]) + is_same_group = False + break + if is_same_group: + groups[-1].append(task_name) + return groups + +def calculate_est(network: nx.Graph, + task_graph: nx.DiGraph, + group:List[Hashable], + commtimes: Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]], + comp_schedule: Dict[Hashable, List[Task]], + task_schedule: Dict[Hashable, Task], + task_graph_arrival_time) -> Dict[Hashable, Dict[Hashable, float]]: + """ + Calculate the earliest start times for the given group on all nodes. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + group (List[Hashable]): The independent group of tasks + commtimes (Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]): Communication times for node-pair task-pair + comp_schedule (Dict[Hashable, List[Task]]): schedule of tasks for each node + task_schedule Dict[Hashable, Task]): Task objects associated with each task + Returns: + Dict[Hashable, Dict[Hashable, float]]: Earliest Start Time table for the given group + """ + est_table = {task_name:{node:None for node in network.nodes} for task_name in group} + for task_name in group: + for node in comp_schedule: + max_arrival_time: float = max( + [ + task_graph_arrival_time, *[ + task_schedule[parent].end + ( + commtimes[(task_schedule[parent].node, node)][(parent, task_name)] + ) + for parent in task_graph.predecessors(task_name) + ] + ] + ) + if comp_schedule[node]: + est_table[task_name][node] = max(max_arrival_time, comp_schedule[node][-1].end) + else: + est_table[task_name][node] = max_arrival_time + return est_table + +def get_initial_assignments(network: nx.Graph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + group: List[Hashable], + est_table: Dict[Hashable, Dict[Hashable, float]] + ) -> Dict[Hashable, List[Hashable]]: + """ + Get initial assignments of the tasks of an independent groups based on their execution times + + Args: + network (nx.Graph): The node network + runtimes (Dict[Hashable, Dict[Hashable, float]]): Runtimes of each node for given tasks + group (List[Hashable]): Current independent group under consideration + est_table (Dict[Hashable, Dict[Hashable, float]]): Earliest Start Time table for the given group + + Returns: + Dict[Hashable, List[Hashable]]: Initial Schedule of tasks + + """ + assignments = {node:[] for node in network.nodes} + for task_name in group: #Assign nodes based on execution times + assigned_node = min(runtimes, key= lambda node, task_name=task_name : runtimes[node][task_name]) + assignments[assigned_node].append(task_name) + for node in assignments: #sort based on earliest start times + assignments[node].sort(key= lambda x, node=node: est_table[x][node]) + return assignments + +def get_ft(node_schedule: List[Task]) -> float: + """ + Calculate finish time for a node in a given schedule + + Args: + node_schedule (List[Task]): Schedule of the node + + Returns: + float: Finish Time of the node + """ + if node_schedule: + return node_schedule[-1].end + return 0 + +def get_ft_after_insert(new_task_name: Hashable, + node: Hashable, + assignments: List[Hashable], + node_schedule: List[Task], + est_table: Dict[Hashable, Dict[Hashable, float]], + runtimes: Dict[Hashable, Dict[Hashable, float]], + insert_position:int) -> (float, List[Task], Task): + """ + Calculate the finish time after inserting task in the schedule of a node. + + Args: + new_task_name (Hashable) New task to be inserted. + node (nx.Graph): Node on which the task is to be run + assignments (List[Hashable]): Assignments of the tasks + node_schedule (List[Task]): Schedule of the node. + est_table (Dict[Hashable, Dict[Hashable, float]]): Earliest Start Time table for the given group + runtimes (Dict[Hashable, Dict[Hashable, float]]): Runtimes of each node for given tasks + insert_postiion (int): Position at which current group of tasks were inserted + Returns: + float: Finish time after insert + List[Task]: New schedule after insertion + Task: Task object of the task after inserting + """ + new_assignments = assignments.copy() + new_assignments.append(new_task_name) #Todo: O(n) time insertion instead of sorting + new_assignments.sort(key = lambda task: est_table[new_task_name][node]) + new_schedule = node_schedule.copy() + new_task = None + start_time = 0 + if new_schedule: + start_time = max(start_time, node_schedule[-1].end) + for task_id in reversed(range(insert_position, len(new_schedule))): + del new_schedule[task_id] + for task_name in new_assignments: + #Todo: Calculate where to start inserting new_assignments from instead of deleting + start_time = est_table[task_name][node] + if new_schedule: + start_time = max(start_time, new_schedule[-1].end) + task = Task(node, task_name, start_time, start_time + runtimes[node][task_name]) + new_schedule.append(task) + if task_name == new_task_name: + new_task = task + return new_schedule[-1].end, new_schedule, new_task + +def delete_task_from_schedule(task_name:Hashable, + node: Hashable, + node_schedule: List[Task], + est_table: Dict[Hashable, Dict[Hashable, float]], + runtimes: Dict[Hashable, Dict[Hashable, float]] + )-> (float, List[Task]): + """ + Calculate the new schedule after removing task from a node schedule. + + Args: + task_name (Hashable) New task to be inserted. + node (nx.Graph): Node on which the task is to be run + node_schedule (List[Task]): Schedule of the node. + est_table (Dict[Hashable, Dict[Hashable, float]]): Earliest Start Time table for the given group + runtimes (Dict[Hashable, Dict[Hashable, float]]): Runtimes of each node for given tasks + Returns: + float: Finish time after deletion + List[Task]: New schedule after insertion + """ + new_schedule = node_schedule.copy() + i = None + for i, task in enumerate(new_schedule): + if task.name == task_name: + del new_schedule[i] + logging.debug("Deleted: %s", new_schedule) + break + for j in range(i, len(new_schedule)): + task = new_schedule[j] + start_time = est_table[task.name][node] + if j!=0: + start_time = max(start_time, new_schedule[j-1].end) + new_schedule[j] = Task(node, task.name, start_time, start_time + runtimes[node][task.name]) + + new_ft = 0 + logging.debug("Schedule of %s after remoiving %s : %s",node, task_name, new_schedule) + if new_schedule: + new_ft = new_schedule[-1].end + return new_ft, new_schedule + +class ResidualHbmctScheduler(Scheduler): + """Schedules tasks using the HBMCT (Hybrid Minimum Completion Time) algorithm. + + Source: https://dx.doi.org/10.1137/0218016 + """ + @staticmethod + def get_runtimes(network: nx.Graph, + task_graph: nx.DiGraph) -> Tuple[Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], + Dict[Tuple[Hashable, Hashable], float]]]: + """Get the expected runtimes of all tasks on all nodes. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Tuple[Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]]: + A tuple of dictionaries mapping nodes to a dictionary of tasks and their runtimes + and edges to a dictionary of tasks and their communication times. The first dictionary + maps nodes to a dictionary of tasks and their runtimes. The second dictionary maps edges + to a dictionary of task dependencies and their communication times. + """ + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task_name in task_graph.nodes: + cost: float = task_graph.nodes[task_name]["weight"] + runtimes[node][task_name] = cost / speed + logging.debug("Task %s on node %s has runtime %s", task_name, node, runtimes[node][task_name]) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, src, dst_task, dst, commtimes[src, dst][src_task, dst_task] + ) + + return runtimes, commtimes + + @staticmethod + def schedule_groups(network: nx.Graph, + task_graph: nx.DiGraph, + groups:List[List[Hashable]], + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: float = 0.0 + ) -> Dict[Hashable, List[Task]]: + """ + Schedule all the groups independently + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + groups (List[List[Hashable]]): List of groups + runtimes (Dict[Hashable, Dict[Hashable, float]]): Runtimes of each node for given tasks + commtimes (Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]): Communication times for node-pair task-pair + + Returns: + Dict[Hashable, List[Task]]: The schedule for each node + """ + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + for group in groups: + comp_group_start_positions = {node: len(comp_schedule[node]) for node in comp_schedule} + est_table = calculate_est(network, task_graph, group, commtimes, comp_schedule, task_schedule, task_graph_arrival_time) + average_est = {task_name: np.mean([ + est for est in est_table[task_name] + ]) for task_name in est_table} + assignments = get_initial_assignments(network, runtimes, group, est_table) + for node in assignments: + for task_name in assignments[node]: + start_time = est_table[task_name][node] + if comp_schedule[node]: + start_time = max(start_time, comp_schedule[node][-1].end) + task = Task(node, task_name, start_time, start_time + runtimes[node][task_name]) + comp_schedule[node].append(task) + task_schedule[task_name] = task + logging.debug("Initial assignment for group %s: %s", group, comp_schedule) + assignment_changed = True + while assignment_changed: + assignment_changed = False + max_ft_node = max(comp_schedule, key = lambda x: get_ft(comp_schedule[x])) + max_ft = comp_schedule[max_ft_node][-1].end + avg_est_assignments = sorted(assignments[max_ft_node], key=lambda task_name, average_est = average_est: average_est[task_name]) + logging.debug("current MFT %s with finish time %s", max_ft_node, max_ft) + + for task_name in avg_est_assignments: + logging.debug("Trying to move around task %s", task_name) + new_max_ft, max_ft_node_new_schedule = delete_task_from_schedule( + task_name, + max_ft_node, + comp_schedule[max_ft_node], + est_table, + runtimes + ) + if new_max_ft Dict[str, List[Task]]: + """Computes the schedule for the task graph using the CPoP algorithm. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[str, List[Task]]: The schedule for the task graph. + """ + comp_schedule: Dict[Hashable, List[Task]] = None + # {node: [] for node in network.nodes} + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + runtimes, commtimes = ResidualHbmctScheduler.get_runtimes(network, task_graph) + groups = hbmct_create_groups(network, task_graph) + comp_schedule = self.schedule_groups(network, task_graph, groups, runtimes, commtimes, comp_schedule, task_graph_arrival_time) + + return comp_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npheft.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npheft.py new file mode 100644 index 00000000..3e83ef9a --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npheft.py @@ -0,0 +1,147 @@ +import logging +import pathlib +from typing import Dict, Hashable, List, Tuple, Optional + +import networkx as nx +import numpy as np +from saga.utils.draw import draw_gantt + +from ....scheduler import Task, DWScheduler +from ....utils.tools import get_insert_loc +from ...cpop import upward_rank + +thisdir = pathlib.Path(__file__).resolve().parent + + +def heft_rank_sort(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + rank = upward_rank(network, task_graph) + topological_sort = {node: i for i, node in enumerate(reversed(list(nx.topological_sort(task_graph))))} + rank = {node: (rank[node] + topological_sort[node]) for node in rank} + return sorted(list(rank.keys()), key=rank.get, reverse=True) + + +class NPHeftScheduler(DWScheduler): + + @staticmethod + def get_runtimes( + network: nx.Graph, task_graph: nx.DiGraph + ) -> Tuple[ + Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]],]: + + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug( + "Task %s on node %s has runtime %s", + task, + node, + runtimes[node][task], + ) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, + src, + dst_task, + dst, + commtimes[src, dst][src_task, dst_task], + ) + + return runtimes, commtimes + + def _schedule( + self, + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[ + Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float] + ], + schedule_order: List[Hashable], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: Optional[float] = 0 + ) -> Dict[Hashable, List[Task]]: + + + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + + task_name: Hashable + logging.debug("Schedule order: %s", schedule_order) + for task_name in schedule_order: + + min_finish_time = np.inf + best_node = None + for node in network.nodes: # Find the best node to run the task + max_arrival_time: float = max( + [ + task_graph_arrival_time, + *[ + task_schedule[parent].end + + ( + commtimes[(task_schedule[parent].node, node)][ + (parent, task_name) + ] + ) + for parent in task_graph.predecessors(task_name) + ], + ] + ) + + runtime = runtimes[node][task_name] + idx, start_time = get_insert_loc( + comp_schedule[node], max_arrival_time, runtime + ) + + logging.debug( + "Testing task %s on node %s: start time %s, finish time %s", + task_name, + node, + start_time, + start_time + runtime, + ) + + finish_time = start_time + runtime + if finish_time < min_finish_time: + min_finish_time = finish_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task_name] + task = Task( + best_node[0], task_name, min_finish_time - new_runtime, min_finish_time + ) + comp_schedule[best_node[0]].insert(best_node[1], task) + task_schedule[task_name] = task + + return comp_schedule + + def schedule( + self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[str, List[Task]]: + + comp_schedule: Dict[Hashable, List[Task]] = None + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + runtimes, commtimes = NPHeftScheduler.get_runtimes(network, task_graph) + schedule_order = heft_rank_sort(network, task_graph) + comp_schedule = self._schedule( + network, task_graph, runtimes, commtimes, schedule_order, comp_schedule, task_graph_arrival_time + ) + + return comp_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/nphybrid.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/nphybrid.py new file mode 100644 index 00000000..a2b6e0ff --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/nphybrid.py @@ -0,0 +1,39 @@ + +import logging +from typing import Dict, Iterable, List, Tuple + +import networkx as nx +import numpy as np + +from saga.scheduler import Scheduler, Task + + +class HybridScheduler(Scheduler): + """A hybrid scheduler.""" + def __init__(self, schedulers: Iterable[Scheduler]) -> None: + """Initializes the hybrid scheduler. + + Args: + schedulers (Iterable[Scheduler]): An iterable of schedulers. + """ + self.schedulers = schedulers + + def schedule(self, network: nx.Graph, task_graph: nx.DiGraph) -> Dict[str, List[Task]]: + """Returns the best schedule of the given schedule functions. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[str, List[Task]]: The best schedule. + """ + pass + # best_scheduler, best_schedule, best_makespan = None, None, np.inf + # for scheduler in self.schedulers: + # schedule = scheduler.schedule(network, task_graph) + # makespan = max(tasks[-1].end if tasks else 0 for tasks in schedule.values()) + # if makespan < best_makespan: + # best_scheduler, best_schedule, best_makespan = scheduler, schedule, makespan + # logging.debug("Best Scheduler: %s", best_scheduler.__class__.__name__) + # return best_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npmaxmin.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmaxmin.py new file mode 100644 index 00000000..cec1d547 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmaxmin.py @@ -0,0 +1,93 @@ +from functools import lru_cache +from typing import Dict, Hashable, List, Tuple +import networkx as nx + +from ....scheduler import Scheduler, Task + + +class NPMaxMinScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Max-Min scheduler""" + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Schedules the task graph on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule. + """ + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + @lru_cache(maxsize=None) + def get_eet(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + @lru_cache(maxsize=None) + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_eat(node: Hashable) -> float: + return schedule[node][-1].end if schedule.get(node) else task_graph_arrival_time + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_fat(task: Hashable, node: Hashable) -> float: + return task_graph_arrival_time if task_graph.in_degree(task) <= 0 else max([ + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_ect(task: Hashable, node: Hashable) -> float: + return get_eet(task, node) + max(get_eat(node), get_fat(task, node)) + + def clear_caches(): + """Clear all caches.""" + get_eat.cache_clear() + get_fat.cache_clear() + get_ect.cache_clear() + + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + available_tasks = [ + task for task in task_graph.nodes + if task not in scheduled_tasks and all(pred in scheduled_tasks + for pred in task_graph.predecessors(task)) + ] + #slight change from MinMin logic + while available_tasks: + min_ects = {task: min(get_ect(task, node) for node in network.nodes) for task in available_tasks} + sched_task = max(min_ects, key=min_ects.get) + sched_node = min(network.nodes, key=lambda node: get_ect(sched_task, node)) + + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=max(get_eat(sched_node), get_fat(sched_task, sched_node)), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + + available_tasks.remove(sched_task) + + clear_caches() + + # add empty list for nodes that have no tasks scheduled + for node in network.nodes: + schedule.setdefault(node, []) + + # sort schedule pairs by key name + schedule = dict(sorted(schedule.items())) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npmct.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmct.py new file mode 100644 index 00000000..85c1fcb2 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmct.py @@ -0,0 +1,64 @@ +from functools import partial +from typing import Dict, Hashable, List, Tuple + +import networkx as nx + +from ....scheduler import Scheduler, Task + + +class ResidualMCTScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Minimum Completion Time scheduler + + Source: https://doi.org/10.1006/jpdc.2000.1714 + """ + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the schedule of the tasks on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule of the tasks on the network. + """ + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} # Initialize list for each node + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + def get_exec_time(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + def get_eat(node: Hashable) -> float: + eat = schedule[node][-1].end if schedule.get(node) else 0 + return eat + + def get_fat(task: Hashable, node: Hashable) -> float: + fat = task_graph_arrival_time if task_graph.in_degree(task) <= 0 else max( + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ) + return fat + + def get_completion_time(task: Hashable, node: Hashable) -> float: + start_time = max(get_eat(node), get_fat(task, node)) + return start_time + get_exec_time(task, node) + + for task in nx.topological_sort(task_graph): + # Find node with minimum execution time for the task + sched_node = min(network.nodes, key=partial(get_completion_time, task)) + + start_time = max(get_eat(sched_node), get_fat(task, sched_node)) + end_time = start_time + get_exec_time(task, sched_node) + + # Add task to the schedule + new_task = Task(node=sched_node, name=task, start=start_time, end=end_time) + schedule[sched_node].append(new_task) + scheduled_tasks[task] = new_task + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npmet.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmet.py new file mode 100644 index 00000000..dee6a33f --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmet.py @@ -0,0 +1,57 @@ +from functools import partial +from typing import Dict, List, Hashable, Tuple +import networkx as nx + +from ....scheduler import Scheduler, Task + + +class ResidualMETScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Minimum Execution Time scheduler""" + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the schedule of the tasks on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule of the tasks on the network. + """ + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} # Initialize list for each node + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + def get_exec_time(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + def get_eat(node: Hashable) -> float: + eat = schedule[node][-1].end if schedule.get(node) else 0 + return eat + + def get_fat(task: Hashable, node: Hashable) -> float: + fat = task_graph_arrival_time if task_graph.in_degree(task) <= 0 else max( + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ) + return fat + + for task in nx.topological_sort(task_graph): + # Find node with minimum execution time for the task + sched_node = min(network.nodes, key=partial(get_exec_time, task)) + + start_time = max(get_eat(sched_node), get_fat(task, sched_node)) + end_time = start_time + get_exec_time(task, sched_node) + + # Add task to the schedule + new_task = Task(node=sched_node, name=task, start=start_time, end=end_time) + schedule[sched_node].append(new_task) + scheduled_tasks[task] = new_task + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npminmin.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npminmin.py new file mode 100644 index 00000000..fb06bdf6 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npminmin.py @@ -0,0 +1,101 @@ +from functools import lru_cache +from itertools import product +from typing import Dict, Hashable, List, Tuple + +import networkx as nx + +from ....scheduler import Scheduler, Task +from saga.utils.draw import draw_gantt + + +class NPMinMinScheduler(Scheduler): + """Minimum Completion Time scheduler""" + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the schedule of the tasks on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule of the tasks on the network. + """ + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + @lru_cache(maxsize=None) + def get_eet(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + @lru_cache(maxsize=None) + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + + # this function gives the earliest available time of a node which is the end time of the last task scheduled on that node or 0 if no task is scheduled on that node + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_eat(node: Hashable) -> float: + eat = schedule[node][-1].end if schedule.get(node) else task_graph_arrival_time + return eat + + + # this function gives the latest available time of a task on a node which is the maximum of the end time of the tasks that are predecessors of the task and the communication time between the predecessor task and the task + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_fat(task: Hashable, node: Hashable) -> float: + fat = task_graph_arrival_time if task_graph.in_degree(task) <= 0 else max([ + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ]) + return fat + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_ect(task: Hashable, node: Hashable) -> float: + return get_eet(task, node) + max(get_eat(node), get_fat(task, node)) + + def clear_caches(): + """Clear all caches.""" + get_eat.cache_clear() + get_fat.cache_clear() + get_ect.cache_clear() + + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + available_tasks = [ + task for task in task_graph.nodes + if (task not in scheduled_tasks and + # Check if all predecessors are scheduled + set(task_graph.predecessors(task)).issubset(set(scheduled_tasks.keys()))) + ] + while available_tasks: + # Find the task and node that minimizes the ECT + sched_task, sched_node = min( + product(available_tasks, network.nodes), + key=lambda instance: get_ect(instance[0], instance[1]) + ) + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=max(get_eat(sched_node), get_fat(sched_task, sched_node)), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + available_tasks.remove(sched_task) + + clear_caches() + + + # add empty list for nodes that have no tasks scheduled + for node in network.nodes: + schedule.setdefault(node, []) + + # sort schedule pairs by key name + schedule = dict(sorted(schedule.items())) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npmsbc.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmsbc.py new file mode 100644 index 00000000..bb0c918e --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npmsbc.py @@ -0,0 +1,348 @@ +import logging +from typing import Dict, Hashable, List, Tuple, Optional + +import networkx as nx +import numpy as np + +from ....scheduler import Scheduler, Task +from ....utils.tools import get_insert_loc + + +def calulate_sbct( + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]], +) -> (Dict[Hashable, float], Dict[Hashable, Hashable]): + """ + Computes the strict bound completion time of the tasks in the task graph. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + runtimes (Dict[Hashable, Dict[Hashable, float]]): A dictionary mapping nodes to a + dictionary of tasks and their runtimes. + commtimes (Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]): A + dictionary mapping edges to a dictionary of task dependencies and their communication times. + + Returns: + Dict[Hashable, float]: the strict bound completion time for each task + Dict[Hashable, float]: the favourite node achieving the sbl time for each task + """ + sbct = {} + ifav = {} + + def get_drt(task_name, node): + """ + Calculate the data ready time + """ + return max( + (sbct[pred] + commtimes[ifav[pred], node][pred, task_name]) + for pred in task_graph.predecessors(task_name) + ) + + def get_sbct(task_name): + """ + Calculate the strict bound completion time for a specific task + """ + min_val = float("inf") + min_node = None + degree = task_graph.in_degree(task_name) + for node in network.nodes: + if degree <= 0: + temp_val = runtimes[node][task_name] + else: + temp_val = get_drt(task_name, node) + runtimes[node][task_name] + + if temp_val < min_val: + min_val = temp_val + min_node = node + return min_val, min_node + + for task_name in nx.topological_sort(task_graph): + sbct[task_name], ifav[task_name] = get_sbct(task_name) + + return sbct, ifav + + +def get_sbl(network: nx.Graph, task_graph: nx.DiGraph) -> Dict[Hashable, float]: + """ + Computes the static b-level of the tasks in the task graph. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, float]: the static b-level for each task + """ + sbl = {} + + is_comp_zero = all( + np.isclose(network.nodes[_node]["weight"], 0) for _node in network.nodes + ) + + def avg_comp_time(task: Hashable) -> float: + """Get the average compute time for a task""" + if is_comp_zero: + return 1e-9 + return np.mean( + [ + task_graph.nodes[task]["weight"] / network.nodes[node]["weight"] + for node in network.nodes + if not np.isclose(network.nodes[node]["weight"], 0) + ] + ) + + for task_name in nx.topological_sort(task_graph): + sbl[task_name] = ( + 0 + if task_graph.in_degree(task_name) <= 0 + else max( + (avg_comp_time(pred) + sbl[pred]) + for pred in task_graph.predecessors(task_name) + ) + ) + + return sbl + + +def calculate_st( + task_graph: nx.DiGraph, + ifav: Dict[Hashable, float], + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]], +) -> Dict[Hashable, float]: + """ + Calculate the start-times on ifav nodes + + Args: + task_graph (nx.DiGraph): The task graph. + ifav (Dict[Hashable, float]): Favourite node for each task to run on. + runtimes (Dict[Hashable, Dict[Hashable, float]]): A dictionary mapping nodes to a + dictionary of tasks and their runtimes. + commtimes (Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]): A + dictionary mapping edges to a dictionary of task dependencies and their communication times. + + Returns: + Dict[Hashable, float]: start-timed for each tasks + """ + st = {} + for task_name in nx.topological_sort(task_graph): + st[task_name] = ( + 0 + if task_graph.in_degree(task_name) <= 0 + else max( + ( + st[pred] + + runtimes[ifav[pred]][pred] + + commtimes[ifav[pred], ifav[task_name]][pred, task_name] + ) + for pred in task_graph.predecessors(task_name) + ) + ) + return st + + +def get_priority( + task_graph: nx.Graph, + sbct: Dict[Hashable, float], + sbl: Dict[Hashable, float], + st: Dict[Hashable, float], +) -> Dict[Hashable, float]: + """ + Calculate the priority values + + Args: + task_graph (nx.DiGraph): The task graph. + sbct (Dict[Hashable, float]): strict bound completion time. + sbl (Dict[Hashable, float]): static b-level. + st (Dict[Hashable, float]): start-time on ifav. + + Returns: + Dict[Hashable, float]: Priority values + """ + return { + task_name: sbct[task_name] + sbl[task_name] + st[task_name] + for task_name in task_graph.nodes + } + + +class ResidualMsbcScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Implements the Multiple Strict Bounds Constraints (MSBC) scheduling algorithm + + Source: https://dx.doi.org/10.1109/PACRIM.2005.1517309 + """ + + @staticmethod + def get_runtimes( + network: nx.Graph, task_graph: nx.DiGraph + ) -> Tuple[ + Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]], + ]: + """Get the expected runtimes of all tasks on all nodes. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Tuple[Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]]: + A tuple of dictionaries mapping nodes to a dictionary of tasks and their runtimes + and edges to a dictionary of tasks and their communication times. The first dictionary + maps nodes to a dictionary of tasks and their runtimes. The second dictionary maps edges + to a dictionary of task dependencies and their communication times. + """ + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug( + "Task %s on node %s has runtime %s", + task, + node, + runtimes[node][task], + ) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, + src, + dst_task, + dst, + commtimes[src, dst][src_task, dst_task], + ) + + return runtimes, commtimes + + def _schedule( + self, + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[ + Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float] + ], + priorities: Dict[Hashable, float], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: float = 0.0, + ) -> Dict[Hashable, List[Task]]: + """Computes the schedule for the task graph using the MSBC algorithm. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + runtimes (Dict[Hashable, Dict[Hashable, float]]): A dictionary mapping nodes to a + dictionary of tasks and their runtimes. + commtimes (Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]]): A + dictionary mapping edges to a dictionary of task dependencies and their communication times. + priorities (Dict[Hashable, float]): Priority values for tasks + + Returns: + Dict[Hashable, List[Task]]: The schedule for the task graph. + """ + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + ready_set = set( + [ + task_name + for task_name in task_graph.nodes + if task_graph.in_degree(task_name) <= 0 + ] + ) + + scheduled_set = set() + sorted_nodes = sorted( + network.nodes, key=lambda node: network.nodes[node]["weight"], reverse=True + ) + while len(ready_set) > 0: + task_name = max(ready_set, key=lambda x: priorities[x]) + ready_set.remove(task_name) + min_start_time = np.inf + best_node = None + for node in sorted_nodes: # Find the best node to run the task + max_arrival_time: float = max( # + [ + task_graph_arrival_time, + *[ + task_schedule[parent].end + + ( + commtimes[(task_schedule[parent].node, node)][ + (parent, task_name) + ] + ) + for parent in task_graph.predecessors(task_name) + ], + ] + ) + + runtime = runtimes[node][task_name] + idx, start_time = get_insert_loc( + comp_schedule[node], max_arrival_time, runtime + ) + if start_time < min_start_time: + min_start_time = start_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task_name] + task = Task( + best_node[0], task_name, min_start_time, min_start_time + new_runtime + ) + comp_schedule[best_node[0]].insert(best_node[1], task) + task_schedule[task_name] = task + scheduled_set.add(task_name) + for succ in task_graph.successors(task_name): + is_ready = True + for pred in task_graph.predecessors(succ): + if pred not in scheduled_set: + is_ready = False + break + if is_ready: + ready_set.add(succ) + return comp_schedule + + def schedule( + self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]], + ) -> Dict[Hashable, List[Task]]: + """Computes the schedule for the task graph using the MSBC algorithm. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule for the task graph. + + Raises: + ValueError: If instance is invalid. + """ + + comp_schedule: Dict[Hashable, List[Task]] = None + # {node: [] for node in network.nodes} + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + runtimes, commtimes = ResidualMsbcScheduler.get_runtimes(network, task_graph) + sbl = get_sbl(network, task_graph) + sbct, ifav = calulate_sbct(network, task_graph, runtimes, commtimes) + st = calculate_st(task_graph, ifav, runtimes, commtimes) + priorities = get_priority(task_graph, sbct, sbl, st) + comp_schedule = self._schedule(network, task_graph, runtimes, commtimes, priorities, comp_schedule, task_graph_arrival_time) + + return comp_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npolb.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npolb.py new file mode 100644 index 00000000..3eb10ce2 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npolb.py @@ -0,0 +1,61 @@ +from typing import Dict, Hashable, List, Tuple +import networkx as nx + +from ....scheduler import Scheduler, Task + +class ResidualOLBScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Opportunistic Load Balancing scheduler + + Source: https://doi.org/10.1006/jpdc.2000.1714 + Summary: "(OLB) assigns each task, in arbitrary order, to the next machine that is expected + to be available, regardless of the task's expected execution time on that machine" + (from source). + """ + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]],) -> Dict[Hashable, List[Task]]: + """Schedule tasks on nodes using the OLB algorithm. + + Args: + network (nx.Graph): Network topology. + task_graph (nx.DiGraph): Task graph. + + Returns: + Dict[Hashable, List[Task]]: Schedule of the tasks on the network. + """ + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + + for task in nx.topological_sort(task_graph): + next_available_node = min( + network.nodes, + key=lambda node: schedule[node][-1].end if schedule[node] else 0 + ) + times = [ + # time node is available + schedule[next_available_node][-1].end if schedule[next_available_node] else 0, + *[ + scheduled_tasks[predecessor].end + ( + task_graph.edges[predecessor, task]['weight'] / + network.edges[scheduled_tasks[predecessor].node, next_available_node]['weight'] + ) + for predecessor in task_graph.predecessors(task) + ], task_graph_arrival_time + ] + start_time = max(times) + exec_time = task_graph.nodes[task]['weight'] / network.nodes[next_available_node]['weight'] + new_task = Task( + name=task, + node=next_available_node, + start=start_time, + end=start_time + exec_time + ) + + schedule[next_available_node].append(new_task) + scheduled_tasks[task] = new_task + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/nprandom.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/nprandom.py new file mode 100644 index 00000000..490a0924 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/nprandom.py @@ -0,0 +1,49 @@ +from typing import Dict, Hashable, List, Tuple +import networkx as nx +from ....scheduler import Scheduler, Task, DWScheduler +import random + + +class NPRandomScheduler(DWScheduler): + """Schedules all tasks on the node with the highest processing speed""" + + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + + schedule = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + + for i in range(len(task_graphs)): + task_graph = task_graphs[i][0] + task_graph_arrival_time = task_graphs[i][1] + + for task_name in nx.topological_sort(task_graph): + # Choose one of the network nodes at random uniformly + selected_node = random.choice(list(network.nodes)) + + # calculate the time needed for dependencies to arrive + if task_graph.in_degree(task_name) > 0: + data_arrival_time = max( + scheduled_tasks[pred].end + + (task_graph.edges[pred, task_name]["weight"] / network.edges[selected_node, selected_node]["weight"]) + for pred in task_graph.predecessors(task_name) + ) + else: + data_arrival_time = task_graph_arrival_time + + # calculate the time needed for the task to execute + task_size = task_graph.nodes[task_name]["weight"] + exec_time = task_size / network.nodes[selected_node]["weight"] + start_time = max(data_arrival_time, task_graph_arrival_time, + schedule[selected_node][-1].end if schedule[selected_node] else 0) + # create the task and add it to the schedule + new_task = Task(selected_node, task_name, start_time, start_time + exec_time) + schedule[selected_node].append(new_task) + scheduled_tasks[task_name] = new_task + + # print the schedule + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npsmt.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npsmt.py new file mode 100644 index 00000000..36928be2 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npsmt.py @@ -0,0 +1,194 @@ +from itertools import combinations, product +from typing import Dict, Hashable, List, Optional, Tuple + +import networkx as nx +from pysmt.shortcuts import (GE, LE, And, Div, ExactlyOne, Implies, Or, Plus, + Real, Symbol, get_model) +from pysmt.typing import REAL + +from saga.scheduler import Task + +from ....scheduler import Scheduler, Task + + +class ResidualSMTScheduler(Scheduler): + """SMT-based scheduler""" + def __init__(self, epsilon: float = 1e-3, solver_name: Optional[str] = None) -> None: + """Initializes the scheduler + + Args: + epsilon (float, optional): The epsilon value. Defaults to 1e-3. + solver_name (Optional[str], optional): The name of the solver. Defaults to None. + """ + super(ResidualSMTScheduler, self).__init__() + self.epsilon = epsilon + self.solver_name = solver_name + + @classmethod + def get_assignment_symbols(cls, network: nx.Graph, task_graph: nx.DiGraph) -> Dict[int, Dict[int, Symbol]]: + """Returns the assignment symbols for the network and task graph + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[int, Dict[int, Symbol]]: The assignment symbols for the network and task graph. + """ + assignments = {} + for node_name in network.nodes: + assignments[node_name] = {} + for task_name in task_graph.nodes: + assignments[node_name][task_name] = Symbol(f"Node{node_name}_Task{task_name}", REAL) + return assignments + + def _schedule(self, + network: nx.Graph, + task_graph: nx.DiGraph, + makespan: float) -> Optional[Dict[Hashable, List[Task]]]: + """Returns the schedule of the tasks on the network if one exists within the makespan + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + makespan (float): The makespan. + + Returns: + Optional[Dict[Hashable, List[Task]]]: The schedule of the tasks on the network. + """ + start_time = self.get_assignment_symbols(network, task_graph) + + constraints = [] + # Each task is assigned to exactly one node + for task in task_graph.nodes: + constraints.append( + ExactlyOne( + GE(start_time[node][task], Real(0)) for node in network.nodes + ) + ) + + # Each node can only execute one task at a time + for node in network.nodes: + for task1, task2 in combinations(task_graph.nodes, r=2): + constraints.append( + Implies( + And( + GE(start_time[node][task1], Real(0)), + GE(start_time[node][task2], Real(0)) + ), + Or( + # task1 ends before task2 starts + LE( + Plus(start_time[node][task1], Div(Real(task_graph.nodes[task1]['weight']), + Real(network.nodes[node]['weight']))), + start_time[node][task2] + ), + # task2 ends before task1 starts + LE( + Plus(start_time[node][task2], Div(Real(task_graph.nodes[task2]['weight']), + Real(network.nodes[node]['weight']))), + start_time[node][task1] + ) + ) + ) + ) + + # communication time constraints + for task1, task2 in task_graph.edges: + for node1, node2 in product(network.nodes, repeat=2): + constraints.append( + Implies( + # task1 and task2 are assigned to node1 and node2 respectively + And( + GE(start_time[node1][task1], Real(0)), + GE(start_time[node2][task2], Real(0)) + ), + # task2 start time is at least task1 end time + communication time + LE( + Plus( + start_time[node1][task1], # start time of task1 + Div(Real(task_graph.nodes[task1]['weight']), + Real(network.nodes[node1]['weight'])), # execution time of task1 + Div(Real(task_graph.edges[task1, task2]['weight']), + Real(network.edges[node1, node2]['weight'])) # communication time + ), + start_time[node2][task2] + ) + ) + ) + + # makespan constraint + for node, task in product(network.nodes, task_graph.nodes): + constraints.append( + Implies( + GE(start_time[node][task], Real(0)), + LE( + Plus(start_time[node][task], Div(Real(task_graph.nodes[task]['weight']), + Real(network.nodes[node]['weight']))), + Real(makespan) + ) + ) + ) + + model = get_model(And(constraints), solver_name=self.solver_name) + schedule: Dict[Hashable, List[Task]] = {} + if model: + for node in network.nodes: + schedule[node] = [] + for task in task_graph.nodes: + if model.get_py_value(start_time[node][task]) >= 0: + schedule[node].append( + Task( + node=node, + name=task, + start=float(model.get_py_value(start_time[node][task])), + end=(float(model.get_py_value(start_time[node][task])) + + task_graph.nodes[task]['weight'] / network.nodes[node]['weight']) + ) + ) + # sort tasks by start time + for _, tasks in schedule.items(): + tasks.sort(key=lambda task: task.start) + return schedule + else: + return None + + + def schedule(self, network: nx.Graph, task_graph: nx.DiGraph) -> Dict[Hashable, List[Task]]: + """Returns an epsilon-optimal schedule of the tasks on the network. + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule of the tasks on the network. + """ + # binary search for the makespan + lower_bound = 0 + # upper_bound is if we execute all tasks on the fastest node + fastest_node = max(network.nodes, key=lambda node: network.nodes[node]['weight']) + + upper_bound = 0 + for task in task_graph.nodes: + exec_time = task_graph.nodes[task]['weight'] / network.nodes[fastest_node]['weight'] + comm_time = max([ + task_graph.edges[pred_task, task]['weight'] / network.edges[pred_node, fastest_node]['weight'] + for pred_task, pred_node in product(task_graph.predecessors(task), network.nodes) + ], default=0) + upper_bound += exec_time + comm_time + + schedule = self._schedule(network, task_graph, upper_bound) + if schedule is None: + raise ValueError("Error in SMT solver, a schedule should always exist") + makespan = (lower_bound + upper_bound) / 2 + while upper_bound - lower_bound > self.epsilon: + _schedule = self._schedule(network, task_graph, makespan) + if _schedule is not None: + upper_bound = makespan + schedule = _schedule + else: + lower_bound = makespan + makespan = (lower_bound + upper_bound) / 2 + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npsufferage.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npsufferage.py new file mode 100644 index 00000000..c76f61ed --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npsufferage.py @@ -0,0 +1,82 @@ +from functools import lru_cache +from typing import Dict, Hashable, List, Tuple +import networkx as nx + +from ....scheduler import Scheduler, Task + +class ResidualSufferageScheduler(Scheduler): + """Implements a sufferage scheduler. + + Source: https://dx.doi.org/10.1007/978-3-540-69277-5_7 + """ + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Schedules the task graph on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule. + """ + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + def get_eet(task: Hashable, node: Hashable) -> float: + """Estimated execution time of a task on a node""" + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + """Communication time to send task1's output from node1 to task2's input on node2""" + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + def get_eat(node: Hashable) -> float: + """Earliest available time on a node""" + return schedule[node][-1].end if schedule.get(node) else 0 + + def get_fat(task: Hashable, node: Hashable) -> float: + """Get file availability time of a task on a node""" + return task_graph_arrival_time if task_graph.in_degree(task) <= 0 else max([ + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ]) + + def get_ect(task: Hashable, node: Hashable) -> float: + """Get estimated completion time of a task on a node""" + return get_eet(task, node) + max(get_eat(node), get_fat(task, node)) + + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + available_tasks = [ + task for task in task_graph.nodes + if task not in scheduled_tasks and all(pred in scheduled_tasks + for pred in task_graph.predecessors(task)) + ] + + sufferages = {} + for task in available_tasks: + ect_values = [get_ect(task, node) for node in network.nodes] + first_ect = min(ect_values) + ect_values.remove(first_ect) + second_ect = min(ect_values) if ect_values else first_ect + sufferages[task] = second_ect - first_ect + + sched_task = max(sufferages, key=sufferages.get) + sched_node = min(network.nodes, key=lambda node: get_ect(sched_task, node)) + + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=max(get_eat(sched_node), get_fat(sched_task, sched_node)), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/npreemptive/npwba.py b/src/saga/schedulers/dynamic_task_graph/npreemptive/npwba.py new file mode 100644 index 00000000..f7dce023 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/npreemptive/npwba.py @@ -0,0 +1,128 @@ +from functools import lru_cache +import random +from typing import Dict, Hashable, List, Tuple + +import networkx as nx + +from ....scheduler import Scheduler, Task + + +class ResidualWBAScheduler(Scheduler): # pylint: disable=too-few-public-methods + """Workflow-Based Allocation (WBA) scheduler. + + Source: http://dx.doi.org/10.1109/CCGRID.2005.1558639 + """ + def __init__(self, alpha: float = 0.5) -> None: + """Initializes the WBA scheduler. + + Args: + alpha (float, optional): The alpha parameter. Defaults to 0.5. + """ + super(ResidualWBAScheduler, self).__init__() + self.alpha = alpha + + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the schedule of the given task graph on the given network. + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule. + """ + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + + @lru_cache(maxsize=None) + def get_eet(task: Hashable, node: Hashable) -> float: + """Estimated execution time of task on node""" + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + @lru_cache(maxsize=None) + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + """Communication time between task1 and task2 on node1 and node2""" + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_eat(node: Hashable) -> float: + """Earliest available time of node""" + return schedule[node][-1].end if schedule.get(node) else 0 + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_fat(task: Hashable, node: Hashable) -> float: + """Latest available time of task on node""" + if task_graph.in_degree(task) <= 0: + return task_graph_arrival_time + return max( + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_est(task: Hashable, node: Hashable) -> float: + """Earliest start time of task on node""" + return max(get_eat(node), get_fat(task, node)) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_ect(task: Hashable, node: Hashable) -> float: + """Earliest completion time of task on node""" + return get_eet(task, node) + get_est(task, node) + + def clear_caches(): + """Clear all caches.""" + get_eat.cache_clear() + get_fat.cache_clear() + get_est.cache_clear() + get_ect.cache_clear() + + cur_makespan = 0 + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + available_tasks = [ + task for task in task_graph.nodes + if (task not in scheduled_tasks and + set(task_graph.predecessors(task)).issubset(set(scheduled_tasks.keys()))) + ] + + while available_tasks: + i_min = float('inf') + i_max = -float('inf') + + makespan_increases: Dict[Hashable, Tuple[Hashable, Hashable]] = {} + for task in available_tasks: + for node in network.nodes: + makespan_increases[task, node] = max(get_ect(task, node) - cur_makespan, 0) # makespan increase + + i_min = min(makespan_increases.values()) + i_max = max(makespan_increases.values()) + avail_pairs = [ + key for key, value in makespan_increases.items() + if value <= i_min + self.alpha * (i_max - i_min) + ] + + sched_task, sched_node = random.choice(avail_pairs) + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=get_est(sched_task, sched_node), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + cur_makespan = max(cur_makespan, new_task.end) + available_tasks.remove(sched_task) + + clear_caches() + # fill empty nodes with empty list + for node in network.nodes: + if node not in schedule: + schedule[node] = [] + return schedule \ No newline at end of file diff --git a/src/saga/schedulers/dynamic_task_graph/preemptive/pcpop.py b/src/saga/schedulers/dynamic_task_graph/preemptive/pcpop.py new file mode 100644 index 00000000..308047de --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/preemptive/pcpop.py @@ -0,0 +1,189 @@ +import logging +import pathlib +from typing import Dict, Hashable, List, Tuple, Optional +import heapq + +import networkx as nx +import numpy as np +from saga.utils.draw import draw_gantt + +from ....scheduler import Task, DWScheduler +from ....utils.tools import get_insert_loc +from ...cpop import upward_rank +from ...cpop import cpop_ranks + +thisdir = pathlib.Path(__file__).resolve().parent + + +def heft_rank_sort(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + rank = upward_rank(network, task_graph) + topological_sort = {node: i for i, node in enumerate(reversed(list(nx.topological_sort(task_graph))))} + rank = {node: (rank[node] + topological_sort[node]) for node in rank} + return sorted(list(rank.keys()), key=rank.get, reverse=True) + +def cpop_ranking(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + ranks = cpop_ranks(network, task_graph) + start_tasks = [task for task in task_graph if task_graph.in_degree(task) == 0] + start_tasks_sorted = sorted(start_tasks, key=lambda t: ranks[t], reverse=True) + pq = [(-ranks[task], task) for task in start_tasks_sorted] + heapq.heapify(pq) + queue = [] + while pq: + _, task_name = heapq.heappop(pq) + queue.append(task_name) + ready_tasks = [ + succ for succ in task_graph.successors(task_name) + if all(pred in queue for pred in task_graph.predecessors(succ)) + ] + for ready_task in ready_tasks: + heapq.heappush(pq, (-ranks[ready_task], ready_task)) + + return queue + +class PCpopScheduler(DWScheduler): + + @staticmethod + def get_runtimes( + network: nx.Graph, task_graph: nx.DiGraph + ) -> Tuple[ + Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]],]: + + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug( + "Task %s on node %s has runtime %s", + task, + node, + runtimes[node][task], + ) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, + src, + dst_task, + dst, + commtimes[src, dst][src_task, dst_task], + ) + + return runtimes, commtimes + + def _schedule( + self, + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[ + Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float] + ], + schedule_order: List[Hashable], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: Optional[float] = 0 + ) -> Dict[Hashable, List[Task]]: + + + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + + # for every task that is in the comp_schedule, add it to the task_schedule + for task_list in comp_schedule.values(): + for task in task_list: + task_schedule[task.name] = task + + task_name: Hashable + # logging.debug("Schedule order: %s", schedule_order) + for task_name in schedule_order: + min_finish_time = np.inf + best_node = None + for node in network.nodes: # Find the best node to run the task + max_arrival_time: float = max( + [ + task_graph.nodes[task_name]["arrival_time"], + *[ + task_schedule[parent].end + + ( + commtimes[(task_schedule[parent].node, node)][ + (parent, task_name) + ] + ) + for parent in task_graph.predecessors(task_name) + ], + ] + ) + + runtime = runtimes[node][task_name] + idx, start_time = get_insert_loc( + comp_schedule[node], max_arrival_time, runtime + ) + + logging.debug( + "Testing task %s on node %s: start time %s, finish time %s", + task_name, + node, + start_time, + start_time + runtime, + ) + + finish_time = start_time + runtime + if finish_time < min_finish_time: + min_finish_time = finish_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task_name] + task = Task( + best_node[0], task_name, min_finish_time - new_runtime, min_finish_time + ) + comp_schedule[best_node[0]].insert(best_node[1], task) + task_schedule[task_name] = task + + return comp_schedule + + def schedule( + self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[str, List[Task]]: + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + comp_schedule: Dict[Hashable, List[Task]] = None + for i in range(len(task_graphs)): + composed_graph = nx.compose_all([task_graphs[j][0] for j in range(i + 1)]) + runtimes, commtimes = self.get_runtimes(network, composed_graph) + schedule_order = cpop_ranking(network, composed_graph) + + tasks_to_remove = [] + if comp_schedule is not None: + for task_name in schedule_order: + matching_task = next((task for tasks in comp_schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start < task_graphs[i][1]: + tasks_to_remove.append(task_name) + else: + comp_schedule[matching_task.node].remove(matching_task) + + for task_name in tasks_to_remove: + schedule_order.remove(task_name) + + + comp_schedule = self._schedule( + network, composed_graph, runtimes, commtimes, schedule_order, comp_schedule, task_graphs[i][1] + ) + + + return comp_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/preemptive/pfastest_node.py b/src/saga/schedulers/dynamic_task_graph/preemptive/pfastest_node.py new file mode 100644 index 00000000..2a99a1db --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/preemptive/pfastest_node.py @@ -0,0 +1,66 @@ +from typing import Dict, Hashable, List, Tuple +import networkx as nx +from ....scheduler import Scheduler, Task, DWScheduler + + +class PFastestNodeScheduler(DWScheduler): + """Schedules all tasks on the node with the highest processing speed""" + + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + """Schedules all tasks on the node with the highest processing speed + + Args: + network (nx.Graph): The network graph. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: A schedule mapping nodes to a list of tasks. + + Raises: + ValueError: If the instance is not valid + """ + fastest_node = max(network.nodes, key=lambda node: network.nodes[node]["weight"]) + schedule = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + # add tasks to fastest node in order (topological sort) + free_time = 0 + + for i in range(len(task_graphs)): + task_graph = nx.compose_all([task_graphs[j][0] for j in range(i + 1)]) + task_graph_arrival_time = task_graphs[i][1] + + if i > 0: + for task_name in task_graph.nodes: + matching_task = next((task for tasks in schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start > task_graphs[i][1]: + schedule[matching_task.node].remove(matching_task) + scheduled_tasks.pop(task_name, None) + + + + for task_name in nx.topological_sort(task_graph): + task_size = task_graph.nodes[task_name]["weight"] + exec_time = task_size / network.nodes[fastest_node]["weight"] + + # For most instances, the data should probably arrive immediately + # since everything is executing on the same node. + data_arrival_time = task_graph_arrival_time + if task_graph.in_degree(task_name) > 0: + data_arrival_time = max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task_name]["weight"] / network.edges[fastest_node, fastest_node]["weight"] + ) + for pred in task_graph.predecessors(task_name) + ) + start_time = max(free_time, data_arrival_time) + + new_task = Task(fastest_node, task_name, start_time, start_time + exec_time) + schedule[fastest_node].append(new_task) + scheduled_tasks[task_name] = new_task + free_time = new_task.end + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/preemptive/pflb.py b/src/saga/schedulers/dynamic_task_graph/preemptive/pflb.py new file mode 100644 index 00000000..10b22ab7 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/preemptive/pflb.py @@ -0,0 +1,279 @@ +import json +import logging +from pprint import pformat +from queue import PriorityQueue +from typing import Dict, Hashable, List, Tuple + +import networkx as nx +from networkx import DiGraph, Graph + +from saga.scheduler import Task + +from ....scheduler import Scheduler, Task, DWScheduler + + +class PFLBScheduler(DWScheduler): + """The FLB (Fast Load Balancing) scheduler. + + Source: https://doi.org/10.1109/ICPP.1999.797442 + Note: They assume homogenous comp/comm speeds, so we will scale the weights in the task graph + by the average (but it will still perform poorly on heterogeneous networks). We also schedule + to the fastest node whenever the original algorithm schedules to an arbitrary node. + """ + def schedule(self, network: Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + + schedule: Dict[Hashable, List[Task]] = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + for task_graph_tupple in task_graphs: + task_graph = task_graph_tupple[0] + task_graph_arrival_time = task_graph_tupple[1] + + network = network.copy() + task_graph = task_graph.copy() + + avg_comm_speed = sum( + network.edges[edge]['weight'] for edge in network.edges + if edge[0] != edge[1] or len(network.nodes) == 1 + ) / len(network.edges) + + fastest_node = max(network.nodes, key=lambda node: network.nodes[node]['weight']) + + def getEP(task: Hashable) -> Hashable: # pylint: disable=invalid-name + """Get Enabling Processor (EP) of a task. + + The enabling processor of a ready task t, EP (t) is the processor from which + the last message arrives. + + NOTE: Because this algorithm assumes homogenous comp/comm speeds, the EP is + *not* the processor from which the last message actually arrives, but rather + the processor from which the last message *would* arrive if the network was + homogenous (with comm speed equal to the average comm speed). + + Args: + task (Hashable): The task. + + Returns: + Hashable: The enabling processor of the task. + """ + enabling_task = max( + task_graph.predecessors(task), + key=lambda pred: scheduled_tasks[pred].end + task_graph.edges[pred, task]['weight'] / avg_comm_speed + ) + return scheduled_tasks[enabling_task].node + + + def getLMT(task: Hashable) -> float: # pylint: disable=invalid-name + """Get the Last Message Arrival Time (LMT) + + NOTE: Because the original algorithm assumes homogenous comp/comm speeds, we will + modify it to use the *actual* communication time between the tasks, rather than + assume they are all equal to the average communication time. + + Args: + task (Hashable): The task. + + Returns: + float: The LMT of the task. + """ + if task_graph.in_degree(task) == 0: + return task_graph_arrival_time + + return max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task]['weight'] / + avg_comm_speed + ) + for pred in task_graph.predecessors(task) + ) + + def getEMT(task: Hashable, node: Hashable) -> float: # pylint: disable=invalid-name + """Get the Effective Message Arrival Time (EMT) + + Since our networks have 0 comm delay between nodes and we extend for + heterogeneous networks, this is just getLMT(task). + + Args: + task (Hashable): The task. + node (Hashable): The node. + + Returns: + float: The EMT of the task. + """ + if task_graph.in_degree(task) == 0: + return task_graph_arrival_time + + return max( + scheduled_tasks[pred].end + ( + task_graph.edges[pred, task]['weight'] / + network.edges[scheduled_tasks[pred].node, node]['weight'] + ) + for pred in task_graph.predecessors(task) + ) + + def getPRT(node: Hashable) -> float: # pylint: disable=invalid-name + return 0 if not schedule[node] else schedule[node][-1].end + + def getEST(task: Hashable, node: Hashable) -> float: # pylint: disable=invalid-name + return max(getPRT(node), getEMT(task, node)) + + non_ep_tasks = PriorityQueue() + emt_ep_tasks = { + node: PriorityQueue() + for node in network.nodes + } + lmt_ep_tasks = { + node: PriorityQueue() + for node in network.nodes + } + all_procs = PriorityQueue() + active_procs = PriorityQueue() + + for task in task_graph.nodes: + if task_graph.in_degree(task) == 0: + non_ep_tasks.put((task_graph_arrival_time, task)) + + for node in network.nodes: + all_procs.put((0, node)) + + def schedule_task() -> Tuple[Hashable, Hashable]: + # get head of active_procs without removing + _, proc1 = active_procs.queue[0] if active_procs.queue else (0, None) + task1 = None + if proc1 is not None: + _, task1 = emt_ep_tasks[proc1].queue[0] if emt_ep_tasks[proc1].queue else (0, None) + _, proc2 = all_procs.queue[0] if all_procs.queue else (0, None) + _, task2 = non_ep_tasks.queue[0] if non_ep_tasks.queue else (0, None) + + est_t1_p1 = getEST(task1, proc1) if proc1 is not None and task1 is not None else float('inf') + est_t2_p2 = getEST(task2, proc2) if proc2 is not None and task2 is not None else float('inf') + if est_t1_p1 == float('inf') and est_t2_p2 == float('inf'): + # NOTE: This should never happen. If it does, it means that there is a bug in the algorithm. + # log queue values + logging.debug("active_procs: %s", pformat(active_procs.queue)) + logging.debug("all_procs: %s", pformat(all_procs.queue)) + logging.debug("non_ep_tasks: %s", pformat(non_ep_tasks.queue)) + logging.debug("emt_ep_tasks: %s", pformat({node: pformat(emt_ep_tasks[node].queue) for node in emt_ep_tasks})) + logging.debug("schedule: %s", pformat(schedule)) + logging.debug("task_graph: %s", json.dumps(nx.readwrite.json_graph.node_link_data(task_graph))) + logging.debug("network: %s", json.dumps(nx.readwrite.json_graph.node_link_data(network))) + + + raise RuntimeError(f"No tasks to schedule. proc1={proc1}, task1={task1}, proc2={proc2}, task2={task2}") + if est_t1_p1 <= est_t2_p2: + new_task = Task( + node=proc1, + name=task1, + start=est_t1_p1, + end=est_t1_p1 + task_graph.nodes[task1]['weight'] / network.nodes[proc1]['weight'] + ) + schedule[proc1].append(new_task) + scheduled_tasks[task1] = new_task + + assert active_procs.get()[1] == proc1 + assert emt_ep_tasks[proc1].get()[1] == task1 # dequeue task from emt_ep_tasks[p1] + lmt_ep_tasks[proc1].queue = [ # remove task1 from lmt_ep_tasks[p1] + (priority, task) for priority, task in lmt_ep_tasks[proc1].queue + if task != task1 + ] + return task1, proc1 + else: + # schedule task t2 on processor p2 + new_task = Task( + node=proc2, + name=task2, + start=est_t2_p2, + end=est_t2_p2 + task_graph.nodes[task2]['weight'] / network.nodes[proc2]['weight'] + ) + schedule[proc2].append(new_task) + scheduled_tasks[task2] = new_task + + assert all_procs.get()[1] == proc2 # dequeue proc2 from all_procs + + # NOTE: this is not in the original algorithm, but it seems necessary + all_procs.put((getPRT(proc2), proc2)) # add proc2 with new priority PRT + + assert non_ep_tasks.get()[1] == task2 # dequeue task from non_ep_tasks + return task2, proc2 + + def update_task_lists(task: Hashable, proc: Hashable): + while True: + _, task = lmt_ep_tasks[proc].queue[0] if lmt_ep_tasks[proc].queue else (0, None) + if task is None: + break + if getLMT(task) >= getPRT(proc): # last message arrival time of t >= processor ready time + break + + assert lmt_ep_tasks[proc].get()[1] == task # dequeue task from lmt_ep_tasks[p] + emt_ep_tasks[proc].queue = [ # remove task from emt_ep_tasks[p] + (priority, _task) for priority, _task in emt_ep_tasks[proc].queue + if _task != task + ] + non_ep_tasks.put((getLMT(task), task)) # enqueue task in non_ep_tasks + + def update_proc_lists(task: Hashable, proc: Hashable): + _, task = emt_ep_tasks[proc].queue[0] if emt_ep_tasks[proc].queue else (0, None) + if task is None: + # remove proc from active_procs + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != proc + ] + else: + # remove proc from active_procs and add back with priority est + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != proc + ] + active_procs.put((getEST(task, proc), proc)) + + def update_ready_tasks(task: Hashable, proc: Hashable): + for succ in task_graph.successors(task): + if succ in scheduled_tasks: # not in original algorithm, is it necessary? + continue + + if not all(pred in scheduled_tasks for pred in task_graph.predecessors(succ)): + continue + + enabling_proc = getEP(succ) # get enabling processor of succ + lmt = getLMT(succ) # get last message arrival time of succ + emt = getEMT(succ, enabling_proc) # get effective message arrival time of succ on enabling processor + if lmt < getPRT(enabling_proc): # if lmt < processor ready time + # task perhaps should not execute on ep, since this indicates that the ep is not ready + # when the task is ready to execute + # enqueue succ with priority lmt in non_ep_tasks + non_ep_tasks.put((lmt, succ)) + else: + _, head_proc = emt_ep_tasks[enabling_proc].queue[0] if emt_ep_tasks[enabling_proc].queue else (0, None) + if head_proc is None: + # enqueue ep with priority est in active_procs + active_procs.put((getEST(succ, enabling_proc), enabling_proc)) + else: + # get next task to execute on ep (according to current priorities) + head_task = emt_ep_tasks[enabling_proc].queue[0][1] + # get the emt of this task + _emt = getEMT(head_task, enabling_proc) + if emt < _emt: + # succ should execute before head_task on ep because it becomes ready before head_task + # update ep with priority max(EMT, prt) in active_procs + # first, remove ep from active_procs + new_priority = max(emt, getPRT(enabling_proc)) + active_procs.queue = [ + (priority, _proc) for priority, _proc in active_procs.queue + if _proc != enabling_proc + ] + active_procs.put((new_priority, enabling_proc)) + # enqueue succ with priority emt in emt_ep_tasks[ep] + emt_ep_tasks[enabling_proc].put((emt, succ)) + # enqueue succ with priority lmt in lmt_ep_tasks[ep] + lmt_ep_tasks[enabling_proc].put((lmt, succ)) + + # while len(scheduled_tasks) < len(task_graph.nodes): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + task, proc = schedule_task() + update_task_lists(task, proc) + update_proc_lists(task, proc) + update_ready_tasks(task, proc) + + return schedule + diff --git a/src/saga/schedulers/dynamic_task_graph/preemptive/pheft.py b/src/saga/schedulers/dynamic_task_graph/preemptive/pheft.py new file mode 100644 index 00000000..440dcec4 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/preemptive/pheft.py @@ -0,0 +1,169 @@ +import logging +import pathlib +from typing import Dict, Hashable, List, Tuple, Optional + +import networkx as nx +import numpy as np +from saga.utils.draw import draw_gantt + +from ....scheduler import Task, DWScheduler +from ....utils.tools import get_insert_loc +from ...cpop import upward_rank + +thisdir = pathlib.Path(__file__).resolve().parent + + +def heft_rank_sort(network: nx.Graph, task_graph: nx.DiGraph) -> List[Hashable]: + rank = upward_rank(network, task_graph) + topological_sort = {node: i for i, node in enumerate(reversed(list(nx.topological_sort(task_graph))))} + rank = {node: (rank[node] + topological_sort[node]) for node in rank} + return sorted(list(rank.keys()), key=rank.get, reverse=True) + + +class PHeftScheduler(DWScheduler): + + @staticmethod + def get_runtimes( + network: nx.Graph, task_graph: nx.DiGraph + ) -> Tuple[ + Dict[Hashable, Dict[Hashable, float]], + Dict[Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float]],]: + + runtimes = {} + for node in network.nodes: + runtimes[node] = {} + speed: float = network.nodes[node]["weight"] + for task in task_graph.nodes: + cost: float = task_graph.nodes[task]["weight"] + runtimes[node][task] = cost / speed + logging.debug( + "Task %s on node %s has runtime %s", + task, + node, + runtimes[node][task], + ) + + commtimes = {} + for src, dst in network.edges: + commtimes[src, dst] = {} + commtimes[dst, src] = {} + speed: float = network.edges[src, dst]["weight"] + for src_task, dst_task in task_graph.edges: + cost = task_graph.edges[src_task, dst_task]["weight"] + commtimes[src, dst][src_task, dst_task] = cost / speed + commtimes[dst, src][src_task, dst_task] = cost / speed + logging.debug( + "Task %s on node %s to task %s on node %s has communication time %s", + src_task, + src, + dst_task, + dst, + commtimes[src, dst][src_task, dst_task], + ) + + return runtimes, commtimes + + def _schedule( + self, + network: nx.Graph, + task_graph: nx.DiGraph, + runtimes: Dict[Hashable, Dict[Hashable, float]], + commtimes: Dict[ + Tuple[Hashable, Hashable], Dict[Tuple[Hashable, Hashable], float] + ], + schedule_order: List[Hashable], + current_schedule: Optional[Dict[str, List[Task]]] = None, + task_graph_arrival_time: Optional[float] = 0 + ) -> Dict[Hashable, List[Task]]: + + + comp_schedule: Dict[Hashable, List[Task]] = current_schedule or {node: [] for node in network.nodes} + task_schedule: Dict[Hashable, Task] = {} + + # for every task that is in the comp_schedule, add it to the task_schedule + for task_list in comp_schedule.values(): + for task in task_list: + task_schedule[task.name] = task + + task_name: Hashable + # logging.debug("Schedule order: %s", schedule_order) + for task_name in schedule_order: + min_finish_time = np.inf + best_node = None + for node in network.nodes: # Find the best node to run the task + max_arrival_time: float = max( + [ + task_graph.nodes[task_name]["arrival_time"], + *[ + task_schedule[parent].end + + ( + commtimes[(task_schedule[parent].node, node)][ + (parent, task_name) + ] + ) + for parent in task_graph.predecessors(task_name) + ], + ] + ) + + runtime = runtimes[node][task_name] + idx, start_time = get_insert_loc( + comp_schedule[node], max_arrival_time, runtime + ) + + logging.debug( + "Testing task %s on node %s: start time %s, finish time %s", + task_name, + node, + start_time, + start_time + runtime, + ) + + finish_time = start_time + runtime + if finish_time < min_finish_time: + min_finish_time = finish_time + best_node = node, idx + + new_runtime = runtimes[best_node[0]][task_name] + task = Task( + best_node[0], task_name, min_finish_time - new_runtime, min_finish_time + ) + comp_schedule[best_node[0]].insert(best_node[1], task) + task_schedule[task_name] = task + + return comp_schedule + + def schedule( + self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[str, List[Task]]: + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + comp_schedule: Dict[Hashable, List[Task]] = None + for i in range(len(task_graphs)): + composed_graph = nx.compose_all([task_graphs[j][0] for j in range(i + 1)]) + runtimes, commtimes = self.get_runtimes(network, composed_graph) + schedule_order = heft_rank_sort(network, composed_graph) + + tasks_to_remove = [] + if comp_schedule is not None: + for task_name in schedule_order: + matching_task = next((task for tasks in comp_schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start < task_graphs[i][1]: + tasks_to_remove.append(task_name) + else: + comp_schedule[matching_task.node].remove(matching_task) + + for task_name in tasks_to_remove: + schedule_order.remove(task_name) + + + comp_schedule = self._schedule( + network, composed_graph, runtimes, commtimes, schedule_order, comp_schedule, task_graphs[i][1] + ) + + + return comp_schedule diff --git a/src/saga/schedulers/dynamic_task_graph/preemptive/pmaxmin.py b/src/saga/schedulers/dynamic_task_graph/preemptive/pmaxmin.py new file mode 100644 index 00000000..01c99304 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/preemptive/pmaxmin.py @@ -0,0 +1,114 @@ +from functools import lru_cache +from typing import Dict, Hashable, List, Tuple, Optional +import networkx as nx + +from ....scheduler import Scheduler, Task, DWScheduler +from saga.utils.draw import draw_gantt + + +class PMaxMinScheduler(DWScheduler): # pylint: disable=too-few-public-methods + """Max-Min scheduler""" + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + """Schedules the task graph on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule. + """ + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + + + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for i in range(len(task_graphs)): + task_graph = nx.compose_all([task_graphs[j][0] for j in range(i + 1)]) + + + if i > 0: + for task_name in task_graph.nodes: + matching_task = next((task for tasks in schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start > task_graphs[i][1]: + schedule[matching_task.node].remove(matching_task) + scheduled_tasks.pop(task_name, None) + + + + + @lru_cache(maxsize=None) + def get_eet(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + @lru_cache(maxsize=None) + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_eat(node: Hashable, + task: Optional[Hashable] = None) -> float: + return max(schedule[node][-1].end if schedule.get(node) else 0, task_graph.nodes[task]["arrival_time"]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_fat(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]["arrival_time"] if task_graph.in_degree(task) <= 0 else max([ + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_ect(task: Hashable, node: Hashable) -> float: + return get_eet(task, node) + max(get_eat(node, task), get_fat(task, node)) + + def clear_caches(): + """Clear all caches.""" + get_eat.cache_clear() + get_fat.cache_clear() + get_ect.cache_clear() + + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + # get all tasks that are ready to be scheduled + available_tasks = [ + task for task in task_graph.nodes + if task not in scheduled_tasks and all(pred in scheduled_tasks for pred in task_graph.predecessors(task)) + ] + #slight change from MinMin logic + while available_tasks: + min_ects = {task: min(get_ect(task, node) for node in network.nodes) for task in available_tasks} + sched_task = max(min_ects, key=min_ects.get) + sched_node = min(network.nodes, key=lambda node: get_ect(sched_task, node)) + + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=max(get_eat(sched_node, sched_task), get_fat(sched_task, sched_node)), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + + available_tasks.remove(sched_task) + + clear_caches() + + # add empty list for nodes that have no tasks scheduled + for node in network.nodes: + schedule.setdefault(node, []) + + # sort schedule pairs by key name + schedule = dict(sorted(schedule.items())) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/preemptive/pminmin.py b/src/saga/schedulers/dynamic_task_graph/preemptive/pminmin.py new file mode 100644 index 00000000..28dced60 --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/preemptive/pminmin.py @@ -0,0 +1,108 @@ +from functools import lru_cache +from itertools import product +from typing import Dict, Hashable, List, Tuple, Optional + +import networkx as nx + +from ....scheduler import Scheduler, Task + + +class PMinMinScheduler(Scheduler): + """Minimum Completion Time scheduler""" + def schedule(self, network: nx.Graph, task_graphs: List[Tuple[nx.DiGraph, float]]) -> Dict[Hashable, List[Task]]: + """Returns the schedule of the tasks on the network + + Args: + network (nx.Graph): The network. + task_graph (nx.DiGraph): The task graph. + + Returns: + Dict[Hashable, List[Task]]: The schedule of the tasks on the network. + """ + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + + + schedule: Dict[Hashable, List[Task]] = {} + scheduled_tasks: Dict[Hashable, Task] = {} # Map from task_name to Task + + for i in range(len(task_graphs)): + task_graph = nx.compose_all([task_graphs[j][0] for j in range(i + 1)]) + + + if i > 0: + for task_name in task_graph.nodes: + matching_task = next((task for tasks in schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start > task_graphs[i][1]: + schedule[matching_task.node].remove(matching_task) + scheduled_tasks.pop(task_name, None) + + @lru_cache(maxsize=None) + def get_eet(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]['weight'] / network.nodes[node]['weight'] + + @lru_cache(maxsize=None) + def get_commtime(task1: Hashable, task2: Hashable, node1: Hashable, node2: Hashable) -> float: + return task_graph.edges[task1, task2]['weight'] / network.edges[node1, node2]['weight'] + + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_eat(node: Hashable, + task: Optional[Hashable] = None) -> float: + return max(schedule[node][-1].end if schedule.get(node) else 0, task_graph.nodes[task]["arrival_time"]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_fat(task: Hashable, node: Hashable) -> float: + return task_graph.nodes[task]["arrival_time"] if task_graph.in_degree(task) <= 0 else max([ + scheduled_tasks[pred_task].end + + get_commtime(pred_task, task, scheduled_tasks[pred_task].node, node) + for pred_task in task_graph.predecessors(task) + ]) + + @lru_cache(maxsize=None) # Must clear cache after each iteration since schedule changes + def get_ect(task: Hashable, node: Hashable) -> float: + return get_eet(task, node) + max(get_eat(node, task), get_fat(task, node)) + + def clear_caches(): + """Clear all caches.""" + get_eat.cache_clear() + get_fat.cache_clear() + get_ect.cache_clear() + + # while len(scheduled_tasks) < task_graph.order(): + while set(task_graph.nodes).issubset(set(scheduled_tasks.keys())) == False: + # get all tasks that are ready to be scheduled + available_tasks = [ + task for task in task_graph.nodes + if (task not in scheduled_tasks and set(task_graph.predecessors(task)).issubset(set(scheduled_tasks.keys()))) + ] + while available_tasks: + # Find the task and node that minimizes the ECT + sched_task, sched_node = min( + product(available_tasks, network.nodes), + key=lambda instance: get_ect(instance[0], instance[1]) + ) + schedule.setdefault(sched_node, []) + new_task = Task( + node=sched_node, + name=sched_task, + start=max(get_eat(sched_node, sched_task), get_fat(sched_task, sched_node)), + end=get_ect(sched_task, sched_node) + ) + schedule[sched_node].append(new_task) + scheduled_tasks[sched_task] = new_task + available_tasks.remove(sched_task) + + clear_caches() + + # add empty list for nodes that have no tasks scheduled + for node in network.nodes: + schedule.setdefault(node, []) + + # sort schedule pairs by key name + schedule = dict(sorted(schedule.items())) + + return schedule diff --git a/src/saga/schedulers/dynamic_task_graph/preemptive/prandom.py b/src/saga/schedulers/dynamic_task_graph/preemptive/prandom.py new file mode 100644 index 00000000..4714364b --- /dev/null +++ b/src/saga/schedulers/dynamic_task_graph/preemptive/prandom.py @@ -0,0 +1,64 @@ +from typing import Dict, Hashable, List, Tuple +import networkx as nx +from ....scheduler import Scheduler, Task, DWScheduler +import random + + +class PRandomScheduler(DWScheduler): + """Schedules all tasks on the node with the highest processing speed""" + + def schedule(self, + network: nx.Graph, + task_graphs: List[Tuple[nx.DiGraph, float]] + ) -> Dict[Hashable, List[Task]]: + + + for task_graph_tupple in task_graphs: + for node in task_graph_tupple[0].nodes: + task_graph_tupple[0].nodes[node]["arrival_time"] = task_graph_tupple[1] + + schedule = {node: [] for node in network.nodes} + scheduled_tasks: Dict[Hashable, Task] = {} + + + for i in range(len(task_graphs)): + task_graph = nx.compose_all([task_graphs[j][0] for j in range(i + 1)]) + + if i > 0: + for task_name in task_graph.nodes: + matching_task = next((task for tasks in schedule.values() for task in tasks if task.name == task_name), None) + if matching_task: + if matching_task.start > task_graphs[i][1]: + schedule[matching_task.node].remove(matching_task) + scheduled_tasks.pop(task_name, None) + + + for task_name in nx.topological_sort(task_graph): + if task_name in scheduled_tasks: + continue + # Choose one of the network nodes at random uniformly + selected_node = random.choice(list(network.nodes)) + + # calculate the time needed for dependencies to arrive + if task_graph.in_degree(task_name) > 0: + data_arrival_time = max( + scheduled_tasks[pred].end + + (task_graph.edges[pred, task_name]["weight"] / network.edges[selected_node, selected_node]["weight"]) + for pred in task_graph.predecessors(task_name) + ) + else: + data_arrival_time = task_graph.nodes[task_name]["arrival_time"] + + # calculate the time needed for the task to execute + task_size = task_graph.nodes[task_name]["weight"] + exec_time = task_size / network.nodes[selected_node]["weight"] + start_time = max(data_arrival_time, task_graph.nodes[task_name]["arrival_time"], + schedule[selected_node][-1].end if schedule[selected_node] else 0) + # create the task and add it to the schedule + new_task = Task(selected_node, task_name, start_time, start_time + exec_time) + schedule[selected_node].append(new_task) + scheduled_tasks[task_name] = new_task + + # print the schedule + + return schedule diff --git a/src/saga/schedulers/flb.py b/src/saga/schedulers/flb.py index 60d465c5..d97d5606 100644 --- a/src/saga/schedulers/flb.py +++ b/src/saga/schedulers/flb.py @@ -61,7 +61,7 @@ def getEP(task: Hashable) -> Hashable: # pylint: disable=invalid-name def getLMT(task: Hashable) -> float: # pylint: disable=invalid-name """Get the Last Message Arrival Time (LMT) - NOTE: Because this original algorithm assumes homogenous comp/comm speeds, we will + NOTE: Because the original algorithm assumes homogenous comp/comm speeds, we will modify it to use the *actual* communication time between the tasks, rather than assume they are all equal to the average communication time. diff --git a/src/saga/schedulers/parametric/__init__.py b/src/saga/schedulers/parametric/__init__.py index 7287d490..c33f6019 100644 --- a/src/saga/schedulers/parametric/__init__.py +++ b/src/saga/schedulers/parametric/__init__.py @@ -92,29 +92,113 @@ def deserialize(self, data: Dict[str, Any]) -> "InsertTask": class ParametricScheduler(Scheduler): def __init__(self, initial_priority: IntialPriority, - insert_task: InsertTask) -> None: + insert_task: InsertTask, + cumulative: bool = False + ) -> None: super().__init__() self.initial_priority = initial_priority self.insert_task = insert_task + self.cumulative = cumulative def schedule(self, network: nx.Graph, task_graph: nx.DiGraph, - schedule: Optional[ScheduleType] = None) -> ScheduleType: + schedule: Optional[ScheduleType] = None, + min_start_time: float = 0.0) -> ScheduleType: """Schedule the tasks on the network. Args: network (nx.Graph): The network graph. task_graph (nx.DiGraph): The task graph. schedule (Optional[ScheduleType]): The current schedule. + min_start_time: float Returns: Dict[Hashable, List[Task]]: A dictionary mapping nodes to a list of tasks executed on the node. """ + if schedule is not None: + schedule = { + node: [ + Task(task.node, task.name, task.start - min_start_time, task.end - min_start_time) + for task in tasks + ] + for node, tasks in schedule.items() + } + + + queue = self.initial_priority(network, task_graph) + + # print("\n\nschedule before pruning:") + # for node, tasks in schedule.items(): + # print(f"Node {node}:") + # for task in tasks: + # print(f"Task {task.name} starts at {task.start} and ends at {task.end}") + # print() + + # print("\nqueue before pruning:") + # print(queue) + + + + # the code for comulative scheduling (don't need it for the residual scheduling) + # ============================================= + # the following two loops are for cumulative scheduling (scheduling all graphs together with non-preemptive approach) + + if self.cumulative: + # remove tasks that have already been scheduled and their start time is less than min_start_time + for _, tasks in schedule.items(): + for task in tasks: + # if task.start < min_start_time: + if task.start < 0: + queue.remove(task.name) + + # remove tasks that have already been scheduled but their start time is greater than min_start_time from the schedule + + removed_tasks = [] + for _, tasks in schedule.items(): + for task in tasks[:]: # Iterate over a copy of the list + # if task.start > min_start_time: + if task.start > 0: + tasks.remove(task) + removed_tasks.append(task.name) + + for removed in removed_tasks: + # check if it has any scheduled parents + for parent in task_graph.predecessors(removed): + # check if the parent is in the schedule + for _, tasks in schedule.items(): + for task in tasks: + if task.name == parent: + task_graph.nodes[parent]["scheduled_task"] = task + # ============================================= + schedule = {node: [] for node in network.nodes} if schedule is None else deepcopy(schedule) + + + # print("\n\nschedule after pruning:") + # for node, tasks in schedule.items(): + # print(f"Node {node}:") + # for task in tasks: + # print(f"Task {task.name} starts at {task.start} and ends at {task.end}") + # print() + # print("\nqueue after pruning:") + # print(queue) + while queue: + # print queue wihout line break + # print(queue) self.insert_task(network, task_graph, schedule, queue.pop(0)) + + # print() + + schedule = { + node: [ + Task(task.node, task.name, task.start + min_start_time, task.end + min_start_time) + for task in tasks + ] + for node, tasks in schedule.items() + } return schedule def serialize(self) -> Dict[str, Any]: diff --git a/src/saga/schedulers/parametric/components.py b/src/saga/schedulers/parametric/components.py index 915e7618..f7945e6f 100644 --- a/src/saga/schedulers/parametric/components.py +++ b/src/saga/schedulers/parametric/components.py @@ -139,7 +139,7 @@ def __call__(self, if self.append_only: start_time = max( min_start_time, - 0.0 if not schedule[node] else schedule[node][-1].end + 0.0 if not schedule[node] else max(schedule[node][-1].end, 0.0) ) insert_loc = len(schedule[node]) else: diff --git a/src/saga/schedulers/wba.py b/src/saga/schedulers/wba.py index bf0f89b8..d3a6195d 100644 --- a/src/saga/schedulers/wba.py +++ b/src/saga/schedulers/wba.py @@ -115,5 +115,8 @@ def clear_caches(): available_tasks.remove(sched_task) clear_caches() - + # fill empty nodes with empty list + for node in network.nodes: + if node not in schedule: + schedule[node] = [] return schedule diff --git a/src/saga/utils/random_graphs.py b/src/saga/utils/random_graphs.py index e6d269ba..c135b882 100644 --- a/src/saga/utils/random_graphs.py +++ b/src/saga/utils/random_graphs.py @@ -1,3 +1,4 @@ +from copy import deepcopy from itertools import product import random from typing import Tuple, TypeVar @@ -9,6 +10,44 @@ from saga.utils.random_variable import RandomVariable + +def add_ccr_weights(task_graph: nx.DiGraph, + network: nx.Graph, + ccr: float) -> nx.Graph: + """Get the network with weights for a given CCR + + Args: + task_graph (nx.DiGraph): The task graph. + network (nx.Graph): The network graph. + ccr (float): The communication to computation ratio. + + Returns: + nx.Graph: The network graph. + """ + network = deepcopy(network) + mean_network_weight = np.mean([ + network.nodes[node]["weight"] + for node in network.nodes + ]) + mean_task_cost = np.mean([ + task_graph.nodes[node]["weight"] + for node in task_graph.nodes + ]) + mean_dependency_weight = np.mean([ + task_graph.edges[edge]["weight"] + for edge in task_graph.edges + ]) + + link_strength = (mean_dependency_weight * mean_network_weight) / (ccr * mean_task_cost) + + for edge in network.edges: + if edge[0] == edge[1]: + network.edges[edge]["weight"] = 1e9 + else: + network.edges[edge]["weight"] = link_strength + + return network + def get_diamond_dag() -> nx.DiGraph: """Returns a diamond DAG.""" dag = nx.DiGraph()