diff --git a/.gitignore b/.gitignore index 0d20b64..85351ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,10 @@ +.ipynb_checkpoints + +*.json + +*.pdf + *.pyc + + + diff --git a/README.md b/README.md index 5f89a07..5e0ee39 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ Architecture][rackscale]. ## Tools -- `bin/scorsa-sched`: Simulate the execution of a workload; requires a system +- `bin/scorsa_sched.py`: Simulate the execution of a workload; requires a system configuration, a layout, and a workload description. -- `bin/scorsa-plot`: Plot a workload schedule; requires a system +- `bin/scorsa_plot.py`: Plot a workload schedule; requires a system configuration, a workload description, a schedule file, and a stats file. -- `bin/swf2workload`: Convert SWF log to scorsa's workload file format. -- `bin/gen-layout`: Generate layout files. -- `bin/test-distance`: Generate distance tables between sockets for a given +- `bin/swf2workload.py`: Convert SWF log to scorsa's workload file format. +- `bin/gen_layout.py`: Generate layout files. +- `bin/test_distance.py`: Generate distance tables between sockets for a given layout. ## Data @@ -21,24 +21,34 @@ Architecture][rackscale]. ## Usage -Executing the simulator involves running `bin/scorsa-sched` as follows: +Executing the simulator involves running `bin/scorsa_sched.py` as follows: ``` -./bin/scorsa-sched -c etc/sample-config.ini -w data/ricc-1272889970+1d.json \ +./bin/scorsa_sched.py -c etc/sample-config.ini -w data/ricc-1272889970+1d.json \ -l etc/layout-2r-064.csv ``` -Which ganerates two JSON files in the current directory: `schedule.json` and -`stats.json`. The former contains the result of the scheduled simulation -itself, while the latter contains additional stats of the system collected -during the execution. These files can be used to visualize the simulation as -follows: +For convenience, you can achieve the same outcome of the above command line by just running the bash script `run.sh`: ``` -./bin/scorsa-plot -c etc/sample-config.ini -w data/ricc-1272889970+1d.json \ +source run.sh +``` + +Which ganerates three JSON files in the current directory: `metrics.json`, `schedule.json` and `stats.json`. The file `metrics.json` contains the distance and fragmentation summary of the simulation (the same information is printed on the console), `schedule.json` contains the result of the scheduled simulation itself, while the `stats.json` contains additional statistics of the system collected during the execution. These files can be used to visualize the simulation as follows: + +``` +./bin/scorsa_plot.py -c etc/sample-config.ini -w data/ricc-1272889970+1d.json \ -s schedule.json -t stats.json ``` +For convenience, you can achieve the same outcome of the above command line by just running the bash script `plot.sh`: + +``` +source plot.sh +``` + + + ## Formats ### Layout File @@ -56,7 +66,7 @@ denoted by columns separated by «`|`». Empty slots can be identified with a 04,05,|,08,|,-1 ``` -Generation of layouts can be automated using `bin/gen-layout`. A layout of 32 +Generation of layouts can be automated using `bin/gen_layout.py`. A layout of 32 sockets in 2 racks, with 4 drawers per rack, and 2 sockets per sled: ``` @@ -138,6 +148,16 @@ Where: created from scracth, and `true` when the nodes already existed and have been reused. +## Environment + +SCORSA uses Python 3.6. [Conda](https://docs.conda.io/en/latest/) allows you to have different environments installed on your computer to access different versions of Python and different libraries. Sometimes libraries conflict which causes errors and packages not to work. To avoid conflicts, we created an environment specifically for this simulator that contains all of the libraries that you will need. + +To install the SCORSA environment, you will need to follow these steps: + +1. Fork and clone the Github repository. This repository contains a file called [environment.yml](environment.yml) that is needed for the installation. +2. Open the Terminal on your computer (e.g. Git Bash for Windows or Terminal on a Mac/Linux). +3. In the Terminal, navigate to the SCORSA directory, then, type in the Terminal: ```conda env create -f environment.yml``` + ## Maintainers Jordà Polo ``, 2016. diff --git a/SCORSA.ipynb b/SCORSA.ipynb new file mode 100644 index 0000000..966d8cb --- /dev/null +++ b/SCORSA.ipynb @@ -0,0 +1,191 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a notebook to help the execution of the SCORSA without entering the VM. Open another jupyter window to the file [bin/policies.py](http://localhost:8888/edit/scorsa/bin/policies.py), edit it there, return to this notebook and run the cell below to see your new results." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2019-08-01T10:03:17.355778Z", + "start_time": "2019-08-01T10:03:17.005778Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " \"distance\": {\n", + " \"#records\": 834,\n", + " \"90th\": 0.9360000000000002,\n", + " \"95th\": 0.99,\n", + " \"99th\": 0.99,\n", + " \"max\": 0.99,\n", + " \"mean\": 0.43226229675600114,\n", + " \"stdev\": 0.35123351936085256,\n", + " \"totaltime\": 99960.0\n", + " },\n", + " \"fragmentation\": {\n", + " \"#records\": 834,\n", + " \"90th\": 0.6383444393382353,\n", + " \"95th\": 0.6586881635137292,\n", + " \"99th\": 0.6908828712406014,\n", + " \"max\": 0.7440365301492303,\n", + " \"mean\": 0.3685515767337823,\n", + " \"stdev\": 0.23448180584656506,\n", + " \"totaltime\": 99960.0\n", + " }\n", + "}\n", + "Files 'metrics.json', schedule.json' and 'stats.json' generated.\n", + "\n", + "Remember to submit the file 'metrics.json' to http://bscdc-login.bsc.es:7777 before the end of the lecture! Multiple submissions are accepted.\n", + "\n" + ] + } + ], + "source": [ + "#IPython extension to reload modules before executing user code\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "import sys\n", + "sys.path.insert(0, 'bin')\n", + "import scorsa_sched as scorsa\n", + "\n", + "#add the line below to the point in the policy.py where you want to debug\n", + "#import pdb; pdb.set_trace()\n", + "\n", + "class Args:\n", + " c = \"etc/sample-config.ini\"\n", + " w = \"data/ricc-1272889970+1d.json\"\n", + " l = \"etc/layout-64n-2r-4d-1s.csv\"\n", + " nhid = 200\n", + "\n", + "scorsa.main(Args())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Plot Results" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "ExecuteTime": { + "end_time": "2019-07-26T17:22:37.317069Z", + "start_time": "2019-07-26T17:22:37.195952Z" + } + }, + "source": [ + "Visualizing the PDF on jupyter" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "ExecuteTime": { + "end_time": "2019-08-01T10:03:17.367349Z", + "start_time": "2019-08-01T10:03:17.357582Z" + } + }, + "outputs": [], + "source": [ + "class PDF(object):\n", + " def __init__(self, pdf, size=(200,200)):\n", + " self.pdf = pdf\n", + " self.size = size\n", + "\n", + " def _repr_html_(self):\n", + " return ''.format(self.pdf, self.size)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Generating the PDF" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "ExecuteTime": { + "end_time": "2019-08-01T10:03:27.538523Z", + "start_time": "2019-08-01T10:03:17.369393Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + "<__main__.PDF at 0x7fe5107119b0>" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "!/bin/bash plot.sh\n", + "PDF('plot.pdf',size=(800,600))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.8" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/bin/gen-layout b/bin/gen_layout.py similarity index 97% rename from bin/gen-layout rename to bin/gen_layout.py index 9752cc8..76d96ca 100755 --- a/bin/gen-layout +++ b/bin/gen_layout.py @@ -1,5 +1,6 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- + # # gen-layout -- Generate a layout file # @@ -46,7 +47,7 @@ if j + 1 < r: row += ",|," - print row + print(row) if i + 1 == dpr: break @@ -57,4 +58,4 @@ if j + 1 < r: row += ",|," - print row + print(row) diff --git a/bin/plot.py b/bin/plot.py index 8d09742..c527350 100644 --- a/bin/plot.py +++ b/bin/plot.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import numpy as np import numpy.ma as ma diff --git a/bin/policies.py b/bin/policies.py index 5cba903..cb648ec 100644 --- a/bin/policies.py +++ b/bin/policies.py @@ -1,16 +1,17 @@ import math +import random from collections import defaultdict import scorsa -def allocate_nodes(config, free, family, num_nodes, node_size): +def resource_selection_composition_and_placement(config, free: dict, family: str, num_nodes:int , node_size: int): reuse = config.getboolean("composition", "reuse") ff = free[family] num_sockets = num_nodes * node_size - # 1. Reuse node if available - if reuse and node_size in ff.keys() and len(ff[node_size]) >= num_nodes: + # 1. Reuse pre-composed node if available + if reuse and node_size in list(ff.keys()) and len(ff[node_size]) >= num_nodes: nodes = ff[node_size][0:num_nodes] ff[node_size] = ff[node_size][num_nodes:] return False, nodes @@ -41,56 +42,104 @@ def allocate_nodes(config, free, family, num_nodes, node_size): ff[1] = ff[1][num_sockets:] return True, nodes -def free_nodes(free, family, nodes): - for node in nodes: - free[family][len(node)].append(node) -def sched_fcfs(config, curr, jobs, pending, free): + +#inputs: +# config: simulator configuration, can be ignored +# curr: current simulation step +# jobs: dictionary of ALL job descriptions as listed in the workload file (by ID) +# pending: dictionary of job IDs that haven't been scheduled yet. Sorted by Arrival Time +# free: dictionary of currently available resources indexed by processor family + + +#output: +# schedule: proposed job schedule and placement + +def schedule(config, curr, jobs, pending, free): period = config.getfloat("simulator", "period") digits = config.getint("simulator", "digits") compose = config.getfloat("composition", "time") - backscale = config.getboolean("scheduler", "backscale") + downscale = config.getboolean("scheduler", "backscale") schedule = {} - for jid in pending: +###################### +# START EDITING HERE +###################### + + +# visiting_jobs [[u'1387', 8], [u'1373', 16]] +# visiting_jids [u'1387', u'1373'] + + visiting_jobs = [] # array of jobid,num_requested_sockets + visiting_jids = [] # array of jobid only + + if len(pending) == 0: + return schedule + + + #VISITNG NODES IN RANDOM ORDER + random.seed(12345) + visiting_indexes = random.sample(range(len(pending)), len(pending)) + + for index in visiting_indexes: + visiting_jobs.append([pending[index],jobs[pending[index]]["tasks"]]) + + for job in visiting_jobs: + visiting_jids.append(job[0]) + + #print 'pending',pending + #print 'visiting_jobs',visiting_jobs + #print 'visiting_jids',visiting_jids + + for jid in visiting_jids: job = jobs[jid] - family = free.keys()[0] + family = list(free.keys())[0] num_sockets = job["tasks"] num_nodes = 1 if job["scale"] == "up" else num_sockets - node_size = num_sockets / num_nodes + node_size = int(num_sockets / num_nodes) + assert num_sockets % num_nodes == 0, "This was not expected! nodesize=%s".format(node_size) time = job["time"] - backscaled = False + downscaled = False - alloc = allocate_nodes(config, free, family, num_nodes, node_size) - if alloc == None and not backscale: - break - - num_free = len(scorsa.list_free_sockets(free)) - if alloc == None and num_free == 0: + ## 1. TRY TO PLACE ALL TASKS + alloc = resource_selection_composition_and_placement(config, free, family, num_nodes, node_size) + if alloc == None and not downscale: break + ## IF NOT ALL TASKS CAN BE PLACED, + ## 2. TRY TO PLACE AT LEAST AS MANY TASKS AS POSSIBLE (DOWNSCALE THE JOB) IF THE JOB + ## is scaleout (expected to scale across nodes, as opposed to scale up - SMP designed) if alloc == None: + num_free = len(scorsa.list_free_sockets(free)) + if num_free == 0: + break num_sockets = num_free num_nodes = 1 if job["scale"] == "up" else num_sockets - node_size = num_sockets / num_nodes + node_size = int(num_sockets / num_nodes) + assert num_sockets % num_nodes == 0, "This was not expected! nodesize=%s".format(node_size) time = time * (1 / (float(num_sockets) / job["tasks"])) - alloc = allocate_nodes(config, free, family, num_nodes, node_size) - backscaled = True + alloc = resource_selection_composition_and_placement(config, free, family, num_nodes, node_size) + downscaled = True recomposed, nodes = alloc if recomposed: time = time + compose + +###################### +# If the job can't be placed, you should break before this point +###################### + schedule[jid] = {} schedule[jid]["family"] = family schedule[jid]["nodes"] = nodes schedule[jid]["start"] = curr schedule[jid]["end"] = curr + scorsa.step(time, period, digits) schedule[jid]["reused"] = not recomposed - schedule[jid]["backscaled"] = backscaled + schedule[jid]["backscaled"] = downscaled - for jid in schedule.keys(): + for jid in list(schedule.keys()): pending.remove(jid) return schedule diff --git a/bin/scorsa-sched b/bin/scorsa-sched deleted file mode 100755 index 18eff43..0000000 --- a/bin/scorsa-sched +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# scorsa-sched -- Simulate the execution of a workload -# -# Given a system configuration and layout, and a workload description, -# simulate the execution of all the jobs in the workload generating a schedule -# that includes when and where the jobs are executed, along with additional -# stats such as fragmentation. -# -# Copyright © 2016 Jordà Polo - -from __future__ import division - -import logging -import argparse -import configparser -import json -import numpy as np - -from collections import defaultdict - -import scorsa -import policies - -logging.basicConfig(format="%(message)s", level=logging.ERROR) - -ap = argparse.ArgumentParser() -ap.add_argument("-c", "--config", dest="c", required=True, - help="System configuration file") -ap.add_argument("-l", "--layout", dest="l", required=True, - help="System layout file") -ap.add_argument("-w", "--workload", dest="w", required=True, - help="JSON workload file") -args = ap.parse_args() - -config = configparser.ConfigParser(delimiters=("=")) -config.read(args.c) - -length = config.getfloat("simulator", "length") -period = config.getfloat("simulator", "period") -digits = config.getint("simulator", "digits") -families = json.loads(config.get("system", "families")) - -layout = scorsa.map_layout(np.genfromtxt(args.l, delimiter=',', dtype=None)) -max_dist = scorsa.distance(layout, 0, len(layout) - 1) - -with open(args.w) as w: - workload = json.load(w) - -jobs = {job["id"]: job for job in workload} - -arrivals = defaultdict(list) # job IDs indexed by arrival time -completions = defaultdict(list) # job IDs indexed by completion time - -schedule = {} # scheduling decisions indexed by job ID -stats = [] - -running = [] # current job IDs being executed -pending = [] # current submitted job IDs that haven't been scheduled yet -free = {} # current available resources indexed by family - -for f in families: - r = json.loads(config.get("system.%s" % f, "range")) - free[f] = defaultdict(list) - free[f][1] = [[n] for n in range(r[0], r[1])] - -for jid, job in jobs.iteritems(): - i = scorsa.step(float(job["arrival"]), period, digits) - arrivals[i].append(jid) - -for i in scorsa.steps(0.0, length, period, digits): - if i in completions: - for jid in completions[i]: - family = schedule[jid]["family"] - nodes = schedule[jid]["nodes"] - policies.free_nodes(free, family, nodes) - running.remove(jid) - - if i in arrivals: - pending = pending + arrivals[i] - - new = policies.sched_fcfs(config, i, jobs, pending, free) - - for jid, sched in new.iteritems(): - running.append(jid) - completions[sched["end"]].append(jid) - - schedule.update(new) - - # measure fragmentation of free slots, and for each running job, - # normalized by number of sockets - sockets = scorsa.list_free_sockets(free) - f = scorsa.fragmentation(layout, sockets) * len(sockets) / len(layout) - r = d = b = 0.0 - for jid in running: - sockets = scorsa.list_sockets(schedule[jid]["nodes"]) - job_dist = scorsa.distance(layout, min(sockets), max(sockets)) - min_dist = scorsa.distance(layout, 0, len(sockets) - 1) - ratio = len(sockets) / len(layout) - f += scorsa.fragmentation(layout, sockets) * len(sockets) / len(layout) - d += job_dist - min_dist - r += ratio if schedule[jid]["reused"] else 0.0 - b += ratio if schedule[jid]["backscaled"] else 0.0 - d = d / len(running) / max_dist if len(running) > 0 else d - - stats.append({"time": i, "frag": f, "dist": d, "reuse": r, "backscale": b}) - -with open("schedule.json", "w") as out: - json.dump(schedule, out) - -with open("stats.json", "w") as out: - json.dump(stats, out) diff --git a/bin/scorsa.py b/bin/scorsa.py index 542de57..fa48c37 100644 --- a/bin/scorsa.py +++ b/bin/scorsa.py @@ -1,4 +1,4 @@ -from __future__ import division + import numpy as np @@ -7,6 +7,8 @@ from operator import itemgetter # Convert standard time in float format to discrete time in "step" format. + + def step(time, period, digits): s = time - (time % period) s = s + period if time % period != 0 else s @@ -14,19 +16,22 @@ def step(time, period, digits): # Generate list of time steps between the specified interval. Similar to # Python's range(), with float support. + + def steps(start, stop, period, digits): r = start while r < stop: yield round(r, digits) r += period + def map_layout(data): m = {} sled_id = 0 draw_id = 0 rack_id = 0 - for (x,y), value in np.ndenumerate(data): + for (x, y), value in np.ndenumerate(data): if y == 0: rack_id = 0 @@ -52,10 +57,11 @@ def map_layout(data): return m + def distance(layout, a, b): d = 0 if a != b: - d = 1 + d = 1 if layout[a]["sled_id"] != layout[b]["sled_id"]: d = 10 if layout[a]["draw_id"] != layout[b]["draw_id"]: @@ -64,6 +70,7 @@ def distance(layout, a, b): d = 1000 return d + def fragmentation(layout, subset): f = 0.0 by_rack = defaultdict(list) @@ -74,21 +81,23 @@ def fragmentation(layout, subset): for sid in subset: by_rack[layout[sid]['rack_id']].append(sid) - for _, sids in by_rack.iteritems(): + for _, sids in by_rack.items(): sids = sorted(sids) fragments = [] - for k, group in groupby(enumerate(sids), lambda (i, x): i - x): - fragments.append(len(map(itemgetter(1), group))) + for k, group in groupby(enumerate(sids), lambda i_x: i_x[0] - i_x[1]): + fragments.append(len(list(map(itemgetter(1), group)))) f += 1 - max(fragments) / sum(fragments) return f / len(by_rack) + def list_sockets(nodes): return list(chain.from_iterable(nodes)) + def list_free_sockets(free): nodes = [] - for f in free.values(): - for l, n in f.iteritems(): + for f in list(free.values()): + for l, n in f.items(): nodes.append(n) return list(chain.from_iterable(chain.from_iterable(nodes))) diff --git a/bin/scorsa-plot b/bin/scorsa_plot.py similarity index 80% rename from bin/scorsa-plot rename to bin/scorsa_plot.py index 5410119..9c7a874 100755 --- a/bin/scorsa-plot +++ b/bin/scorsa_plot.py @@ -1,7 +1,8 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- + # -# scorsa-plot -- Plot a workload schedule +# scorsa_plot -- Plot a workload schedule # # Given a system configuration, a workload description, and a schedule as # generated by `scorsa-sched', this program generates a plot displaying the @@ -10,20 +11,21 @@ # # Copyright © 2016 Jordà Polo -import sys -import logging + import argparse import configparser -import json import copy +import json +import logging import math - -import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt +import numpy as np +import sys -import scorsa import plot +import scorsa + logging.basicConfig(format="%(message)s", level=logging.ERROR) @@ -62,7 +64,7 @@ jobs = {job["id"]: job for job in workload} -for k, v in schedule.iteritems(): +for k, v in schedule.items(): schedule[k]["id"] = k mpl.rcParams['axes.titlesize'] = 'medium' @@ -71,16 +73,16 @@ mpl.rcParams['ytick.labelsize'] = 'small' _, ax = plt.subplots(len(families)+1, sharex=True, sharey=False, squeeze=False) -ax[-1,0].set_xlabel('Time (s)') +ax[-1, 0].set_xlabel('Time (s)') for a, f in enumerate(families): - ax[a,0].set_xlim(0.0, window_max - window_min) + ax[a, 0].set_xlim(0.0, window_max - window_min) r = json.loads(config.get("system.%s" % f, "range")) - ax[a,0].set_ylabel('Number of sockets\n(%s)' % (f)) - ax[a,0].set_ylim(0, len(range(r[0], r[1]))) + ax[a, 0].set_ylabel('Number of sockets\n(%s)' % (f)) + ax[a, 0].set_ylim(0, len(list(range(r[0], r[1])))) - subs = [s for s in schedule.values() if s["family"] == f] + subs = [s for s in list(schedule.values()) if s["family"] == f] order = [] for i in sorted(subs, key=lambda k: k["end"], reverse=True): if i["start"] <= window_max and i["end"] >= window_min: @@ -116,13 +118,13 @@ if submissions: for jid in order: arrival = jobs[jid]['arrival'] - ax[a,0].axvline(arrival - arrival % period, linewidth=0.1, - color='#666666', linestyle='dotted') + ax[a, 0].axvline(arrival - arrival % period, linewidth=0.1, + color='#666666', linestyle='dotted') for jid in reversed(order): y = np.array(arunning[jid]) - ax[a,0].step(x, y, '-', linewidth=0.01, color='black', where='post') - plot.fill_steps(ax[a,0], x, y, 0, step_where="post", + ax[a, 0].step(x, y, '-', linewidth=0.01, color='black', where='post') + plot.fill_steps(ax[a, 0], x, y, 0, step_where="post", facecolor=jobs[jid]['color'], linewidth=0.0) a += 1 @@ -131,13 +133,13 @@ d = [s["dist"] for s in stats][offset:] r = [s["reuse"] for s in stats][offset:] b = [s["backscale"] for s in stats][offset:] -ax[a,0].set_ylabel('Metrics') -ax[a,0].set_ylim(0.0, 1.0) -ax[a,0].step(x, f, '-', where='post', label='Fragmentation') -ax[a,0].step(x, d, '-', where='post', label='Distance') -ax[a,0].step(x, r, '-', where='post', label='Reuse') -ax[a,0].step(x, b, '-', where='post', label='Backscale') -ax[a,0].legend(prop={'size': 8}) +ax[a, 0].set_ylabel('Metrics') +ax[a, 0].set_ylim(0.0, 1.0) +ax[a, 0].step(x, f, '-', where='post', label='Fragmentation') +ax[a, 0].step(x, d, '-', where='post', label='Distance') +ax[a, 0].step(x, r, '-', where='post', label='Reuse') +ax[a, 0].step(x, b, '-', where='post', label='Backscale') +ax[a, 0].legend(prop={'size': 8}) plt.tight_layout() plt.savefig('plot.pdf', bbox_inches='tight') diff --git a/bin/scorsa_sched.py b/bin/scorsa_sched.py new file mode 100755 index 0000000..cd9d6a8 --- /dev/null +++ b/bin/scorsa_sched.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# scorsa-sched -- Simulate the execution of a workload +# +# Given a system configuration and layout, and a workload description, +# simulate the execution of all the jobs in the workload generating a schedule +# that includes when and where the jobs are executed, along with additional +# stats such as fragmentation. +# +# Copyright © 2016 Jordà Polo + + +import logging +import argparse +import configparser +import json +import numpy as np + +from collections import defaultdict + +import scorsa +import policies + + +def free_nodes(free, family, nodes): + for node in nodes: + free[family][len(node)].append(node) + + +def main(args): + + logging.basicConfig(format="%(message)s", level=logging.ERROR) + config = configparser.ConfigParser(delimiters=("=")) + config.read(args.c) + + length = config.getfloat("simulator", "length") + period = config.getfloat("simulator", "period") + digits = config.getint("simulator", "digits") + families = json.loads(config.get("system", "families")) + + layout = scorsa.map_layout(np.genfromtxt( + args.l, delimiter=',', dtype=str, encoding=None)) + max_dist = scorsa.distance(layout, 0, len(layout) - 1) + + with open(args.w) as w: + workload = json.load(w) + + jobs = {job["id"]: job for job in workload} + + arrivals = defaultdict(list) # job IDs indexed by arrival time + completions = defaultdict(list) # job IDs indexed by completion time + + schedule = {} # scheduling decisions indexed by job ID + stats = [] + internal_stats = [] + + running = [] # current job IDs being executed + pending = [] # current submitted job IDs that haven't been scheduled yet + free = {} # current available resources indexed by family + + for f in families: + r = json.loads(config.get("system.%s" % f, "range")) + free[f] = defaultdict(list) + free[f][1] = [[n] for n in range(r[0], r[1])] + + for jid, job in jobs.items(): + i = scorsa.step(float(job["arrival"]), period, digits) + arrivals[i].append(jid) + + for i in scorsa.steps(0.0, length, period, digits): + if i in completions: + #  release resources for jobs that complete in this step + for jid in completions[i]: + family = schedule[jid]["family"] + nodes = schedule[jid]["nodes"] + free_nodes(free, family, nodes) + running.remove(jid) + + if i in arrivals: + pending = pending + arrivals[i] + + new = policies.schedule(config, i, jobs, pending, free) + + for jid, sched in new.items(): + running.append(jid) + completions[sched["end"]].append(jid) + + schedule.update(new) + + # measure fragmentation of free slots, and for each running job, + # normalized by number of sockets + sockets = scorsa.list_free_sockets(free) + f = scorsa.fragmentation(layout, sockets) * len(sockets) / len(layout) + r = d = b = 0.0 + for jid in running: + sockets = scorsa.list_sockets(schedule[jid]["nodes"]) + job_dist = scorsa.distance(layout, min(sockets), max(sockets)) + min_dist = scorsa.distance(layout, 0, len(sockets) - 1) + ratio = len(sockets) / len(layout) + f += scorsa.fragmentation(layout, sockets) * \ + len(sockets) / len(layout) + d += job_dist - min_dist + r += ratio if schedule[jid]["reused"] else 0.0 + b += ratio if schedule[jid]["backscaled"] else 0.0 + d = d / len(running) / max_dist if len(running) > 0 else d + + stats.append({"time": i, "frag": f, "dist": d, + "reuse": r, "backscale": b}) + internal_stats.append([f, d]) + + metrics = {} + x = 0 #  fragmentation + metrics = { + "fragmentation": { + "mean": np.mean(internal_stats, axis=0)[x], + "stdev": np.std(internal_stats, axis=0)[x], + "max": np.max(internal_stats, axis=0)[x], + "90th": np.percentile(internal_stats, axis=0, q=90)[x], + "95th": np.percentile(internal_stats, axis=0, q=95)[x], + "99th": np.percentile(internal_stats, axis=0, q=99)[x], + "#records": len(internal_stats), + "totaltime": stats[len(stats)-1]["time"] + } + } + x = 1 #  distance + metrics.update({ + "distance": { + "mean": np.mean(internal_stats, axis=0)[x], + "stdev": np.std(internal_stats, axis=0)[x], + "max": np.max(internal_stats, axis=0)[x], + "90th": np.percentile(internal_stats, axis=0, q=90)[x], + "95th": np.percentile(internal_stats, axis=0, q=95)[x], + "99th": np.percentile(internal_stats, axis=0, q=99)[x], + "#records": len(internal_stats), + "totaltime": stats[len(stats)-1]["time"] + } + }) + print(json.dumps(metrics, indent=4, sort_keys=True)) + + with open("metrics.json", "w") as out: + json.dump(metrics, out) + + with open("schedule.json", "w") as out: + json.dump(schedule, out) + + with open("stats.json", "w") as out: + json.dump(stats, out) + + print("Files 'metrics.json', schedule.json' and 'stats.json' generated.") + + print( + "\nRemember to submit the file 'metrics.json' to " + "http://bscdc-login.bsc.es:7777 before the end " + "of the lecture! Multiple submissions are accepted.\n" + ) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("-c", "--config", dest="c", required=True, + help="System configuration file") + ap.add_argument("-l", "--layout", dest="l", required=True, + help="System layout file") + ap.add_argument("-w", "--workload", dest="w", required=True, + help="JSON workload file") + args = ap.parse_args() + main(args) diff --git a/bin/swf2workload b/bin/swf2workload.py similarity index 90% rename from bin/swf2workload rename to bin/swf2workload.py index cc6312d..1feac92 100755 --- a/bin/swf2workload +++ b/bin/swf2workload.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # # swf2workload -- Convert SWF log to scorsa's workload @@ -32,7 +32,7 @@ "no": 0.25 } -assigned = {k: 0 for k in target.keys()} +assigned = {k: 0 for k in list(target.keys())} jobs = [] first = None @@ -47,7 +47,7 @@ continue entry = {} - for k, v in fields.iteritems(): + for k, v in fields.items(): entry[k] = int(line[v]) if entry["status"] != 0: @@ -57,7 +57,7 @@ first = entry scale = "no" - for k, v in assigned.iteritems(): + for k, v in assigned.items(): if len(jobs) and float(v) / len(jobs) < target[k]: scale = k assigned[k] = v + 1 @@ -77,4 +77,4 @@ jobs.append(job) -print json.dumps(jobs) +print(json.dumps(jobs)) diff --git a/bin/test-distance b/bin/test_distance.py similarity index 79% rename from bin/test-distance rename to bin/test_distance.py index 87bef56..e14f587 100755 --- a/bin/test-distance +++ b/bin/test_distance.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # # test-distance -- Generate distance table between sockets @@ -21,9 +21,9 @@ layout = scorsa.map_layout(np.genfromtxt(args.l, delimiter=',', dtype=None)) pids = sorted(layout.keys()) -print "-", " ".join(str(pid) for pid in pids) +print("-", " ".join(str(pid) for pid in pids)) for i in pids: - print i, + print(i, end=' ') for j in pids: - print scorsa.distance(layout, i, j), - print "" + print(scorsa.distance(layout, i, j), end=' ') + print("") diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..616b775 --- /dev/null +++ b/environment.yml @@ -0,0 +1,12 @@ +# To create the environment for the SCORSA simulator just run: +# conda env create --file environment.yml +# conda env update --file environment.yml +name: scorsa +channels: + - conda-forge + - defaults +dependencies: + - python=3.6 + - numpy=1.16 + - configparser=3.7 + - matplotlib=3.0 diff --git a/etc/sample-config.ini b/etc/sample-config.ini index 3e6d6ec..a5762cc 100644 --- a/etc/sample-config.ini +++ b/etc/sample-config.ini @@ -13,7 +13,7 @@ time = 0.0 reuse = False [scheduler] -backscale = False +backscale = True [api] url = http://0.0.0.0:5000/redfish/v1 diff --git a/plot.sh b/plot.sh new file mode 100755 index 0000000..7917856 --- /dev/null +++ b/plot.sh @@ -0,0 +1 @@ +./bin/scorsa_plot.py -c etc/sample-config.ini -w data/ricc-1272889970+1d.json -s schedule.json -t stats.json diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..9132b33 --- /dev/null +++ b/run.sh @@ -0,0 +1 @@ +./bin/scorsa_sched.py -c etc/sample-config.ini -w data/ricc-1272889970+1d.json -l etc/layout-64n-2r-4d-1s.csv