stdlib: Add Workload to the stdlib

This commit adds the concept of a "Workload" to the stdlib. Workloads
specify the details needed to run a particular gem5 workload on an
stdlib board. These are specified as part of gem5-resources and loaded
via the `Workload` class though can be specified locally via the
`CustomWorkload` class.

Tests are included in this commit to verify the functionality of these
Workloads.

Change-Id: I8840d281eb01ee4138f01ee499cae96bf7e0579d
Reviewed-on: https://gem5-review.googlesource.com/c/public/gem5/+/62532
Maintainer: Jason Lowe-Power <power.jg@gmail.com>
Reviewed-by: Jason Lowe-Power <power.jg@gmail.com>
Tested-by: kokoro <noreply+kokoro@google.com>
This commit is contained in:
Bobby R. Bruce
2022-08-18 11:51:11 -07:00
committed by Bobby Bruce
parent eb1242d96a
commit 329a917c71
5 changed files with 559 additions and 13 deletions

View File

@@ -218,6 +218,7 @@ PySource('gem5.resources', 'gem5/resources/__init__.py')
PySource('gem5.resources', 'gem5/resources/downloader.py')
PySource('gem5.resources', 'gem5/resources/md5_utils.py')
PySource('gem5.resources', 'gem5/resources/resource.py')
PySource('gem5.resources', 'gem5/resources/workload.py')
PySource('gem5.utils', 'gem5/utils/__init__.py')
PySource('gem5.utils', 'gem5/utils/filelock.py')
PySource('gem5.utils', 'gem5/utils/override.py')

View File

@@ -25,8 +25,10 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from abc import ABCMeta, abstractmethod
import inspect
from .mem_mode import MemMode, mem_mode_to_string
from ...resources.workload import AbstractWorkload
from m5.objects import (
System,
@@ -177,6 +179,37 @@ class AbstractBoard:
)
return self._is_fs
def set_workload(self, workload: AbstractWorkload) -> None:
"""
Set the workload for this board to run.
This function will take the workload specified and run the correct
workload function (e.g., `set_kernel_disk_workload`) with the correct
parameters
:params workload: The workload to be set to this board.
"""
try:
func = getattr(self, workload.get_function_str())
except AttributeError:
raise Exception(
"This board does not support this workload type. "
f"This board does not contain the necessary "
f"`{workload.get_function_str()}` function"
)
func_signature = inspect.signature(func)
for param_name in workload.get_parameters().keys():
if param_name not in func_signature.parameters.keys():
raise Exception(
"Workload specifies non-existent parameter "
f"`{param_name}` for function "
f"`{workload.get_function_str()}` "
)
func(**workload.get_parameters())
@abstractmethod
def _setup_board(self) -> None:
"""

View File

@@ -39,7 +39,7 @@ from pathlib import Path
import tarfile
from tempfile import gettempdir
from urllib.error import HTTPError
from typing import List, Dict
from typing import List, Dict, Set, Optional
from .md5_utils import md5_file, md5_dir
@@ -182,17 +182,27 @@ def _get_url_base() -> str:
return ""
def _get_resources(resources_group: Dict) -> Dict[str, Dict]:
def _get_resources(
valid_types: Set[str], resources_group: Optional[Dict] = None
) -> Dict[str, Dict]:
"""
A recursive function to get all the resources.
A recursive function to get all the workload/resource of the specified type
in the resources.json file.
:returns: A dictionary of resource names to the resource JSON objects.
:param valid_types: The type to return (i.e., "resource" or "workload).
:param resource_group: Used for recursion: The current resource group being
iterated through.
:returns: A dictionary of artifact names to the resource JSON objects.
"""
if resources_group is None:
resources_group = _get_resources_json()["resources"]
to_return = {}
for resource in resources_group:
if resource["type"] == "resource":
# If the type is "resource" then we add it directly to the map
if resource["type"] in valid_types:
# If the type is valid then we add it directly to the map
# after a check that the name is unique.
if resource["name"] in to_return.keys():
raise Exception(
@@ -204,7 +214,9 @@ def _get_resources(resources_group: Dict) -> Dict[str, Dict]:
elif resource["type"] == "group":
# If it's a group we get recursive. We then check to see if there
# are any duplication of keys.
new_map = _get_resources(resource["contents"])
new_map = _get_resources(
valid_types=valid_types, resources_group=resource["contents"]
)
intersection = set(new_map.keys()).intersection(to_return.keys())
if len(intersection) > 0:
# Note: if this error is received it's likely an error with
@@ -216,10 +228,6 @@ def _get_resources(resources_group: Dict) -> Dict[str, Dict]:
)
)
to_return.update(new_map)
else:
raise Exception(
"Error: Unknown type '{}'.".format(resource["type"])
)
return to_return
@@ -315,7 +323,26 @@ def list_resources() -> List[str]:
:returns: A list of resources by name.
"""
return _get_resources(_get_resources_json()["resources"]).keys()
return _get_resources(valid_types={"resource"}).keys()
def get_workload_json_obj(workload_name: str) -> Dict:
"""
Get a JSON object of a specified workload.
:param workload_name: The name of the workload.
:raises Exception: An exception is raised if the specified workload does
not exit.
"""
workload_map = _get_resources(valid_types={"workload"})
if workload_name not in workload_map:
raise Exception(
f"Error: Workload with name {workload_name} does not exist"
)
return workload_map[workload_name]
def get_resources_json_obj(resource_name: str) -> Dict:
@@ -329,7 +356,7 @@ def get_resources_json_obj(resource_name: str) -> Dict:
:raises Exception: An exception is raised if the specified resources does
not exist.
"""
resource_map = _get_resources(_get_resources_json()["resources"])
resource_map = _get_resources(valid_types={"resource"})
if resource_name not in resource_map:
raise Exception(

View File

@@ -0,0 +1,221 @@
# Copyright (c) 2022 The Regents of the University of California
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from .downloader import get_workload_json_obj
from .resource import Resource
from typing import Dict, Any, Optional
class AbstractWorkload:
"""
Workloads contain information needed to build a workload.
A workload specifies a function and its parameters to run on a board to
set a workload. Workload's are passed to board via the `AbstractBoard`'s
`set_workload` function.
The `AbstractBoard` has a `set_workload` function which accepts an
AbstractWorkload. The `set_workload` function uses the `get_function_str`
to determine which function should be called on the board and the
`get_parameters` function specifies the parameters to be passed.
Example
-------
```py
workload = CustomWorkload(
function = "set_se_binary_workload",
parameters = {
"binary" : Resource("x86-print-this"),
"arguments" : ["hello", 6]
},
)
board.set_workload(workload)
```
The above is the equivalent of:
```py
board.set_se_binary_workload(
binary = Resource("x86-print-this"),
arguments = ["hello", 6],
)
```
Notes
-----
This class should not be used directly. Please use `Workload` or
`CustomWorkload`.
"""
def __init__(self, function: str, parameters: Dict[str, Any]) -> None:
self._func = function
self._params = parameters
def get_function_str(self) -> str:
"""
Returns the name of the workload function to be run.
This function is called via the AbstractBoard's `set_workload`
function. The parameters from the `get_parameters` function are passed
to this function.
"""
return self._func
def get_parameters(self) -> Dict[str, Any]:
"""
Returns a dictionary mapping the workload parameters to their values.
These parameters are passed to the function specified by
`get_function_str` via the AbstractBoard's `set_workload` function.
"""
return self._params
def set_parameter(self, parameter: str, value: Any) -> None:
"""
Used to set or override a workload parameter
:param parameter: The parameter of the function to set.
:param value: The value to set to the parameter.
"""
self._params[parameter] = value
class CustomWorkload(AbstractWorkload):
"""
A workload specified locally (i.e., not via gem5-resources as with the
`Workload` class). Here the user specifies the function and the parameters
to be passed.
Usage
-----
```py
workload = CustomWorkload(
function = "set_se_binary_workload",
parameters = {
"binary" : Resource("x86-print-this"),
"arguments" : ["hello", 6]
},
)
board.set_workload(workload)
```
"""
def __init__(self, function: str, parameters: Dict[str, Any]) -> None:
super().__init__(function=function, parameters=parameters)
class Workload(AbstractWorkload):
"""
The `Workload` class loads a workload's information from gem5-resources
based on a name/id passed via the constructor.
Usage
-----
```py
# Determine what workload we want to run.
workload = Workload("example-workload-id")
# Optionally we can override a parameter in the workload. In this example
# we are going to run this workload with a difference kernel.
workload.set_parameter("kernel", Resource("arm64-linux-kernel-4.14.134"))
# We then set this workload to the board.
board.set_workload(workload)
```
"""
def __init__(
self, workload_name: str, resource_directory: Optional[str] = None
) -> None:
"""
This constructor will load the workload details from the workload with
the given name/id.
This function assumes the dictionary returned by the downloader's
`get_workload_json_obj` is a dictionary. An example of the schema is
shown below:
```json
{
"type" : "workload",
"name" : "x86-ubuntu-18.04-echo-hello",
"documentation" : "Description of workload here",
"function" : "set_kernel_disk_workload",
"resources" : {
"kernel" : "x86-linux-kernel-5.4.49",
"disk_image" : "x86-ubuntu-18.04-img"
},
"additional_params" : {
"readfile_contents" : "m5_exit; echo 'hello'; m5_exit"
}
}
```
This resource will result in the equivalent of the following action
being taken:
```python
board.set_kernel_disk_workload(
kernel = Resource("x86-linux-kernel-5.4.49"),
disk_image = Resource("x86-ubuntu-18.04-img"),
readfile_contents = "m5_exit; echo 'hello'; m5_exit",
)
```
:param workload_name: The name of the workload in the resources.json
file to be loaded.
:param resource_directory: An optional parameter that specifies where
any resources should be download and accessed from. If None, a default
location will be used. None by default.
"""
workload_json = get_workload_json_obj(workload_name=workload_name)
func = workload_json["function"]
assert isinstance(func, str)
params = {}
if "resources" in workload_json:
for key in workload_json["resources"].keys():
assert isinstance(key, str)
value = workload_json["resources"][key]
assert isinstance(value, str)
params[key] = Resource(
value, resource_directory=resource_directory
)
if "additional_params" in workload_json:
for key in workload_json["additional_params"]:
assert isinstance(key, str)
params[key] = workload_json["additional_params"][key]
super().__init__(function=func, parameters=params)

View File

@@ -0,0 +1,264 @@
# Copyright (c) 2022 The Regents of the University of California
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import unittest
import tempfile
import os
from gem5.resources.workload import Workload, CustomWorkload
from gem5.resources.resource import Resource
from gem5.resources.downloader import _resources_json_version_required
from typing import Dict
class CustomWorkloadTestSuite(unittest.TestCase):
"""
Tests the `gem5.resources.workload.CustomWorkload` class.
"""
@classmethod
def setUpClass(cls) -> None:
file_contents = (
"{"
+ f'"version" : "{_resources_json_version_required()}",'
+ """
"url_base" : "http://dist.gem5.org/dist/v22-0",
"previous-versions" : {},
"resources": [
{
"type" : "resource",
"name" : "x86-hello64-static",
"documentation" : "A 'Hello World!' binary.",
"architecture" : "X86",
"is_zipped" : false,
"md5sum" : "dbf120338b37153e3334603970cebd8c",
"url" : "{url_base}/test-progs/hello/bin/x86/linux/hello64-static",
"source" : "src/simple"
}
]
}
"""
)
file = tempfile.NamedTemporaryFile(mode="w", delete=False)
file.write(file_contents)
file.close()
cls.test_json = file.name
os.environ["GEM5_RESOURCE_JSON"] = cls.test_json
cls.custom_workload = CustomWorkload(
function="set_se_binary_workload",
parameters={
"binary": Resource("x86-hello64-static"),
"arguments": ["hello", 6],
},
)
@classmethod
def tearDownClass(cls):
# Remove the test json file and unset the environment variable so this
# test does not interfere with others.
os.remove(cls.test_json)
os.environ["GEM5_RESOURCE_JSON"]
def test_get_function_str(self) -> None:
# Tests `CustomResource.get_function_str`
self.assertEqual(
"set_se_binary_workload", self.custom_workload.get_function_str()
)
def test_get_parameters(self) -> None:
# Tests `CustomResource.get_parameter`
parameters = self.custom_workload.get_parameters()
self.assertTrue(isinstance(parameters, Dict))
self.assertEquals(2, len(parameters))
self.assertTrue("binary" in parameters)
self.assertTrue(isinstance(parameters["binary"], Resource))
self.assertTrue("arguments" in parameters)
self.assertTrue(isinstance(parameters["arguments"], list))
self.assertEquals(2, len(parameters["arguments"]))
self.assertEquals("hello", parameters["arguments"][0])
self.assertEquals(6, parameters["arguments"][1])
def test_add_parameters(self) -> None:
# Tests `CustomResource.set_parameter` for the case where we add a new
# parameter value.
self.custom_workload.set_parameter("test_param", 10)
self.assertTrue("test_param" in self.custom_workload.get_parameters())
self.assertEquals(
10, self.custom_workload.get_parameters()["test_param"]
)
# Cleanup
del self.custom_workload.get_parameters()["test_param"]
def test_override_parameter(self) -> None:
# Tests `CustomResource.set_parameter` for the case where we override
# a parameter's value.
old_value = self.custom_workload.get_parameters()["binary"]
self.custom_workload.set_parameter("binary", "test")
self.assertTrue("binary" in self.custom_workload.get_parameters())
self.assertEquals(
"test", self.custom_workload.get_parameters()["binary"]
)
# We set the overridden parameter back to it's old value.
self.custom_workload.set_parameter("binary", old_value)
class WorkloadTestSuite(unittest.TestCase):
"""
Tests the `gem5.resources.workload.Workload` class.
"""
@classmethod
def setUpClass(cls):
# In this constructor we create a json file to load then create a test
# workload.
file_contents = (
"{"
+ f'"version" : "{_resources_json_version_required()}",'
+ """
"url_base" : "http://dist.gem5.org/dist/v22-0",
"previous-versions" : {},
"resources": [
{
"type" : "resource",
"name" : "x86-linux-kernel-5.2.3",
"documentation" : "The linux kernel (v5.2.3), compiled to X86.",
"architecture" : "X86",
"is_zipped" : false,
"md5sum" : "4838c99b77d33c8307b939c16624e4ac",
"url" : "{url_base}/kernels/x86/static/vmlinux-5.2.3",
"source" : "src/linux-kernel"
},
{
"type" : "resource",
"name" : "x86-ubuntu-18.04-img",
"documentation" : "A disk image containing Ubuntu 18.04 for x86..",
"architecture" : "X86",
"is_zipped" : true,
"md5sum" : "90e363abf0ddf22eefa2c7c5c9391c49",
"url" : "{url_base}/images/x86/ubuntu-18-04/x86-ubuntu.img.gz",
"source" : "src/x86-ubuntu",
"additional_metadata" : {
"root_partition": "1"
}
},
{
"type" : "workload",
"name" : "simple-boot",
"documentation" : "Description of workload here",
"function" : "set_kernel_disk_workload",
"resources" : {
"kernel" : "x86-linux-kernel-5.2.3",
"disk_image" : "x86-ubuntu-18.04-img"
},
"additional_params" : {
"readfile_contents" : "echo 'Boot successful'; m5 exit"
}
}
]
}
"""
)
file = tempfile.NamedTemporaryFile(mode="w", delete=False)
file.write(file_contents)
file.close()
cls.test_json = file.name
os.environ["GEM5_RESOURCE_JSON"] = cls.test_json
cls.workload = Workload("simple-boot")
@classmethod
def tearDownClass(cls):
# Remove the test json file and unset the environment variable so this
# test does not interfere with others.
os.remove(cls.test_json)
os.environ["GEM5_RESOURCE_JSON"]
def test_get_function_str(self) -> None:
# Tests `Resource.get_function_str`
self.assertEquals(
"set_kernel_disk_workload", self.workload.get_function_str()
)
def test_get_parameters(self) -> None:
# Tests `Resource.get_parameters`
parameters = self.workload.get_parameters()
self.assertTrue(isinstance(parameters, Dict))
self.assertEqual(3, len(parameters))
self.assertTrue("kernel" in parameters)
self.assertTrue(isinstance(parameters["kernel"], Resource))
self.assertTrue("disk_image" in parameters)
self.assertTrue(isinstance(parameters["disk_image"], Resource))
self.assertTrue("readfile_contents" in parameters)
self.assertTrue(
"echo 'Boot successful'; m5 exit", parameters["readfile_contents"]
)
def test_add_parameters(self) -> None:
# Tests `Resource.set_parameter` for the case where we add a new
# parameter value.
self.workload.set_parameter("test_param", 10)
self.assertTrue("test_param" in self.workload.get_parameters())
self.assertEquals(10, self.workload.get_parameters()["test_param"])
# Cleanup
del self.workload.get_parameters()["test_param"]
def test_override_parameter(self) -> None:
# Tests `Resource.set_parameter` for the case where we override
# a parameter's value.
old_value = self.workload.get_parameters()["readfile_contents"]
self.workload.set_parameter("readfile_contents", "test")
self.assertTrue("readfile_contents" in self.workload.get_parameters())
self.assertEquals(
"test", self.workload.get_parameters()["readfile_contents"]
)
# We set the overridden parameter back to it's old value.
self.workload.set_parameter("readfile_contents", old_value)