Adding an example for Spatter (#1272)

This change adds a new utility function for processing Spatter traces
into SpatterKernels under parse_kernels.
Additionally, it adds documentation for all the utility functions in
spatter_kernel.py.
Lastly, it adds an example script for running one spatter trace using
SpatterGenerator to the examples.
This commit is contained in:
Mahyar Samani
2024-06-21 02:23:41 -07:00
committed by Bobby R. Bruce
parent 30bfdc8e52
commit 21bd1c28ab
4 changed files with 319 additions and 44 deletions

View File

@@ -30,4 +30,6 @@ from .spatter_kernel import (
SpatterKernel,
parse_kernel,
partition_trace,
prepare_kernels,
unroll_trace,
)

View File

@@ -24,7 +24,9 @@
# (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 json
from math import ceil
from pathlib import Path
from typing import (
List,
Tuple,
@@ -35,36 +37,6 @@ from m5.params import Addr
from m5.util import inform
def parse_kernel(kernel: dict, default_delta=8) -> Tuple[int, int, str, List]:
delta = kernel.get("delta", default_delta)
if delta < 0:
inform(
f"Negative delta found: {delta}. Setting it to {default_delta}."
)
delta = default_delta
count = kernel.get("count", 1)
type = kernel.get("kernel", None)
if type is None:
raise ValueError(f"Keyword 'kernel' not found.")
type = SpatterKernelType(type.lower())
trace = kernel.get("pattern", [])
if len(trace) == 0:
raise ValueError(f"Empty 'pattern' found.")
return (delta, count, type, trace)
def partition_trace(original_trace, num_partitions, interleave_size):
partitions = [[] for _ in range(num_partitions)]
num_leaves = ceil(len(original_trace) / interleave_size)
for i in range(num_leaves):
lower_bound = i * interleave_size
upper_bound = min(lower_bound + interleave_size, len(original_trace))
partitions[i % num_partitions] += original_trace[
lower_bound:upper_bound
]
return partitions
class SpatterKernel:
"""This class encapsulates one kernel in a spatter trace.
A spatter trace is represented with a json file.
@@ -85,15 +57,30 @@ class SpatterKernel:
are loads or stores.
The field `pattern` stores the index array.
This file provides two utility function to parse spatter traces:
This file provides four utility function to parse spatter traces:
parse_kernel: takes a dictionary and returns a tuple of
delta, count, type, and trace.
partition_trace: takes the original trace, number of partitions,
partition_trace: takes an original trace, number of partitions,
and interleave_size.
It returns a list of `num_partitions` partitions where each partition
is an list including interleaved elements from `original_trace`.
It returns a list of `num_partitions` partitions where each
partition includes interleaved elements from `original_trace`.
The elements in the `original_trace` are interleaved with a
granularity of `interleave_size`.
unroll_trace: takes an original trace, delta, count, and minimum
number of elements.
It will unroll `original_trace` by adding `delta` to the last
`og_len` (len(`original_trace`)) elements of the trace in steps.
In each step it will decrement `count` by 1.
If the logical length of `original_trace` (`og_len` * `count`) is
smaller than `min_elements`, it allows for filling the trace with
zeros or elements from the pattern.
By filling elements from the pattern, the unrolling process goes
beyond the `count` limit.
However, it will not decrements `count`.
prepare_kernels: takes a trace_path, number of cores,
interleave_size, base_index_addr, and base_value_addr.
It will return a list of lists of kernels where each list of
kernels represents a kernel with a length of `num_cores`.
The code snippet below shows how to use these functions to create kernels.
generator = SpatterGenerator(num_cores)
@@ -128,6 +115,15 @@ class SpatterKernel:
`count` from spatter trace.
kernel_type (SpatterKernelType): The type of the kernel.
`kernel` from spatter trace.
base_index (int): The index from the index array to start from.
It's most meaningful when used in multi-generator simulations.
User defined, i.e. spatter traces don't have this field.
indices_per_stride (int): The number of indices from the index
array to read from before making a jump of size `stride_size`.
User defined, i.e. spatter traces don't have this field.
stride_size (int): The size of the jump to make after reading
`indices_per_stride` indices from the index array.
User defined, i.e. spatter traces don't have this field.
kernel_trace (List[int]): The elements of the `index` array.
`pattern` from spatter trace.
index_size (int): The size of elements in `index`.
@@ -158,7 +154,6 @@ class SpatterKernel:
value_size: int,
base_value_addr: Addr,
kernel_trace: List[int],
fix_empty_trace: bool = False,
):
self._id = kernel_id
self._delta = kernel_delta
@@ -173,15 +168,6 @@ class SpatterKernel:
self._base_value_addr = base_value_addr
self._trace = kernel_trace
if fix_empty_trace and len(kernel_trace) == 0:
inform(
"Empty trace found. Fixing it by adding a dummy element. "
"Also setting delta to 0 and count to 1.",
)
self._trace = [0]
self._delta = 0
self._count = 1
def empty(self):
return len(self._trace) == 0
@@ -207,3 +193,192 @@ class SpatterKernel:
f"count={self._count}, type={self._type}, "
f"trace[:8]={self._trace[:8]}"
)
def parse_kernel(kernel: dict, default_delta=8) -> Tuple[int, int, str, List]:
"""
Function to parse a kernel from a dictionary. Each Spatter trace is
represented as a list of dictionaries in JSON. Each dictionary in the list
represents a kernel. This function will one kernel and return a tuple of
delta, count, type, and trace.
Args:
kernel (dict): A dictionary representing a kernel.
default_delta (int): The default delta value to use when the delta
value is not found in the kernel dictionary.
Returns:
Tuple[int, int, str, List]: A tuple of delta, count, type,
and trace extracted from the kernel.
"""
delta = kernel.get("delta", default_delta)
if delta < 0:
inform(
f"Negative delta found: {delta}. Setting it to {default_delta}. "
"You can change the default delta value by passing "
"`default_detla` argument to this function."
)
delta = default_delta
count = kernel.get("count", 1)
type = kernel.get("kernel", None)
if type is None:
raise ValueError(f"Keyword 'kernel' not found.")
type = SpatterKernelType(type.lower())
trace = kernel.get("pattern", [])
if len(trace) == 0:
raise ValueError(f"Empty 'pattern' found.")
return (delta, count, type, trace)
def unroll_trace(
original_trace: List,
delta: int,
count: int,
min_elements: int,
fill_zero=False,
fill_pattern=False,
):
"""
Function to unroll a trace by creating replicated elements in the trace.
This function will add `delta` to the last `og_len` (len(`original_trace`))
elements of the trace in steps. In each step it will decrement `count`.
If the logical length of `original_trace` (`og_len` * `count`) is smaller
than `min_elements`, it allows for filling the trace with zeros or elements
from the pattern. By filling elements from the pattern, the unrolling
process goes beyond the `count` limit. However, it will not decrement
`count`.
Args:
original_trace (List): The original trace to unroll.
delta (int): The delta value as provided from the kernel in JSON.
count (int): The count value as provided from the kernel in JSON.
min_elements (int): The minimum number of elements the trace should
have after unrolling.
fill_zero (bool): If True, the trace will be filled with zeros when
the unrolling process runs out of elements from the original trace.
fill_pattern (bool): If True, the trace will be filled with the pattern
allowing to go over the `count` limit (from the kernel in JSON) when
unrolling.
"""
if fill_zero and fill_pattern:
raise ValueError(
f"Only one of fill_zero or fill_pattern can be True. "
"However, both can be False at the same time."
)
if (len(original_trace) * count) < min_elements and (
not fill_zero and not fill_pattern
):
raise ValueError(
f"Trace is too small (len(`pattern`) * `count`) < {min_elements}. "
f"It will not have {min_elements} elements after unrolling. "
"You can set fill_zero or fill_pattern to True to fill pattern. "
"fill_zero will fill with zeros when the unrolling process runs "
"out of elements from the original trace. "
"fill_pattern will fill with the pattern allowing to go over the "
"`count` limit (from the kernel in JSON) when unrolling."
)
og_len = len(original_trace)
ret_count = count
ret_trace = original_trace
while (len(ret_trace) < min_elements) and ret_count > 1:
ret_trace += [element + delta for element in ret_trace[-og_len:]]
ret_count -= 1
if (len(ret_trace) < min_elements) and fill_zero:
inform(
"You have chosen to fill the trace with zero "
f"until it reaches at least {min_elements} elements."
)
ret_trace += [0] * (min_elements - len(ret_trace))
if (len(ret_trace) < min_elements) and fill_pattern:
inform(
"You have chosen to fill the trace with the pattern "
"(without dectementing count) until it "
f"reaches at least {min_elements} elements."
)
while len(ret_trace) < min_elements:
ret_trace += [element + delta for element in ret_trace[-og_len:]]
return ret_count, ret_trace
def partition_trace(original_trace, num_partitions, interleave_size):
if len(original_trace) < (num_partitions * interleave_size):
raise ValueError(
"Trace (`original_trace`) is too small for the "
"given number of partitions and interleave size. "
"The trace (`original_trace`) should have at least "
"`num_partitions` * `interleave_size` elements."
"It might be due to either the trace being too small "
"or it being folded too many times. You can solve "
"this issue by using the `unroll_trace` function. "
)
partitions = [[] for _ in range(num_partitions)]
num_leaves = ceil(len(original_trace) / interleave_size)
for i in range(num_leaves):
lower_bound = i * interleave_size
upper_bound = min(lower_bound + interleave_size, len(original_trace))
partitions[i % num_partitions] += original_trace[
lower_bound:upper_bound
]
return partitions
def prepare_kernels(
trace_path: Path,
num_cores: int,
interleave_size: int,
base_index_addr: Addr,
base_value_addr: Addr,
) -> List[List[SpatterKernel]]:
"""
Function to prepare kernels from a spatter trace. It will read the trace
from the given path and prepare kernels for the given number of cores and
interleave size. It will partition the trace into `num_cores` partitions
with `interleave_size` elements in each partition. It will also unroll the
trace to have at least `num_cores` * `interleave_size` elements in the
trace using the `unroll_trace` function. In case the trace is too small,
it will ask `unroll_trace` to fill the trace with elements from the
pattern. It will return a list of list of kernels where each list of
kernels represents a kernel with a length of `num_cores`.
Args:
trace_path (Path): Path to the spatter trace.
num_cores (int): Number of cores to partition the trace.
interleave_size (int): Number of elements to interleave the trace by.
base_index_addr (Addr): The base address of the index array.
base_value_addr (Addr): The base address of the value array.
Returns:
List[List[SpatterKernel]]: A list of list of kernels where each list
of kernels represents a kernel with a length of `num_cores`.
"""
trace_file = trace_path.open("r")
kernels = json.load(trace_file)
ret = []
for i, kernel in enumerate(kernels):
delta, count, type, og_trace = parse_kernel(kernel)
new_count, unrolled_trace = unroll_trace(
og_trace,
delta,
count,
num_cores * interleave_size,
fill_pattern=True,
)
traces = partition_trace(unrolled_trace, num_cores, interleave_size)
temp = []
for j, trace in enumerate(traces):
temp.append(
SpatterKernel(
kernel_id=i,
kernel_delta=delta,
kernel_count=new_count,
kernel_type=type,
base_index=j * interleave_size,
indices_per_stride=interleave_size,
stride_size=interleave_size * num_cores,
index_size=4,
base_index_addr=base_index_addr,
value_size=8,
base_value_addr=base_value_addr,
kernel_trace=trace,
)
)
ret.append(temp)
return ret