firesim/deploy/runtools/user_topology.py

707 lines
28 KiB
Python

""" Define your additional topologies here. The FireSimTopology class inherits
from UserToplogies and thus can instantiate your topology. """
from __future__ import annotations
from runtools.firesim_topology_elements import (
FireSimPipeNode,
FireSimSwitchNode,
FireSimServerNode,
FireSimSuperNodeServerNode,
FireSimDummyServerNode,
FireSimNode,
)
from runtools.simulation_data_classes import (
PartitionConfig,
PartitionMode,
PartitionNode,
FireAxeEdge,
FireAxeNodeBridgePair,
)
from typing import (
Optional,
Union,
Callable,
Sequence,
TYPE_CHECKING,
cast,
List,
Any,
Dict,
)
if TYPE_CHECKING:
from runtools.firesim_topology_with_passes import FireSimTopologyWithPasses
class UserTopologies:
"""A class that just separates out user-defined/configurable topologies
from the rest of the boilerplate in FireSimTopology()"""
no_net_num_nodes: int
custom_mapper: Optional[Union[Callable, str]]
roots: Sequence[FireSimNode]
def __init__(self, no_net_num_nodes: int) -> None:
self.no_net_num_nodes = no_net_num_nodes
self.custom_mapper = None
self.roots = []
def clos_m_n_r(self, m: int, n: int, r: int) -> None:
"""DO NOT USE THIS DIRECTLY, USE ONE OF THE INSTANTIATIONS BELOW."""
""" Clos topol where:
m = number of root switches
n = number of links to nodes on leaf switches
r = number of leaf switches
and each leaf switch has a link to each root switch.
With the default mapping specified below, you will need:
m switch nodes (on F1: m4.16xlarges).
n fpga nodes (on F1: f1.16xlarges).
TODO: improve this later to pack leaf switches with <= 4 downlinks onto
one 16x.large.
"""
rootswitches = [FireSimSwitchNode() for x in range(m)]
self.roots = rootswitches
leafswitches = [FireSimSwitchNode() for x in range(r)]
servers = [[FireSimServerNode() for x in range(n)] for y in range(r)]
for rswitch in rootswitches:
rswitch.add_downlinks(leafswitches)
for leafswitch, servergroup in zip(leafswitches, servers):
leafswitch.add_downlinks(servergroup)
def custom_mapper(fsim_topol_with_passes: FireSimTopologyWithPasses) -> None:
for i, rswitch in enumerate(rootswitches):
switch_inst_handle = (
fsim_topol_with_passes.run_farm.get_switch_only_host_handle()
)
switch_inst = fsim_topol_with_passes.run_farm.allocate_sim_host(
switch_inst_handle
)
switch_inst.add_switch(rswitch)
for j, lswitch in enumerate(leafswitches):
numsims = len(servers[j])
inst_handle = (
fsim_topol_with_passes.run_farm.get_smallest_sim_host_handle(
num_sims=numsims
)
)
sim_inst = fsim_topol_with_passes.run_farm.allocate_sim_host(
inst_handle
)
sim_inst.add_switch(lswitch)
for sim in servers[j]:
sim_inst.add_simulation(sim)
self.custom_mapper = custom_mapper
def clos_2_8_2(self) -> None:
"""clos topol with:
2 roots
8 nodes/leaf
2 leaves."""
self.clos_m_n_r(2, 8, 2)
def clos_8_8_16(self) -> None:
"""clos topol with:
8 roots
8 nodes/leaf
16 leaves. = 128 nodes."""
self.clos_m_n_r(8, 8, 16)
def fat_tree_4ary(self) -> None:
# 4-ary fat tree as described in
# http://ccr.sigcomm.org/online/files/p63-alfares.pdf
coreswitches = [FireSimSwitchNode() for x in range(4)]
self.roots = coreswitches
aggrswitches = [FireSimSwitchNode() for x in range(8)]
edgeswitches = [FireSimSwitchNode() for x in range(8)]
servers = [FireSimServerNode() for x in range(16)]
for switchno in range(len(coreswitches)):
core = coreswitches[switchno]
base = 0 if switchno < 2 else 1
dls = list(map(lambda x: aggrswitches[x], range(base, 8, 2)))
core.add_downlinks(dls)
for switchbaseno in range(0, len(aggrswitches), 2):
switchno = switchbaseno + 0
aggr = aggrswitches[switchno]
aggr.add_downlinks([edgeswitches[switchno], edgeswitches[switchno + 1]])
switchno = switchbaseno + 1
aggr = aggrswitches[switchno]
aggr.add_downlinks([edgeswitches[switchno - 1], edgeswitches[switchno]])
for edgeno in range(len(edgeswitches)):
edgeswitches[edgeno].add_downlinks(
[servers[edgeno * 2], servers[edgeno * 2 + 1]]
)
def custom_mapper(fsim_topol_with_passes: FireSimTopologyWithPasses) -> None:
"""In a custom mapper, you have access to the firesim topology with passes,
where you can access the run_farm nodes:
Requires 2 fpga nodes w/ 8+ fpgas and 1 switch node
To map, call add_switch or add_simulation on run farm instance
objs in the aforementioned arrays.
Because of the scope of this fn, you also have access to whatever
stuff you created in the topology itself, which we expect will be
useful for performing the mapping."""
# map the fat tree onto one switch host instance (for core switches)
# and two 8-sim-slot (e.g. 8-fpga) instances
# (e.g., two pods of aggr/edge/4sims per f1.16xlarge)
switch_inst_handle = (
fsim_topol_with_passes.run_farm.get_switch_only_host_handle()
)
switch_inst = fsim_topol_with_passes.run_farm.allocate_sim_host(
switch_inst_handle
)
for core in coreswitches:
switch_inst.add_switch(core)
eight_sim_host_handle = (
fsim_topol_with_passes.run_farm.get_smallest_sim_host_handle(num_sims=8)
)
sim_hosts = [
fsim_topol_with_passes.run_farm.allocate_sim_host(eight_sim_host_handle)
for _ in range(2)
]
for aggrsw in aggrswitches[:4]:
sim_hosts[0].add_switch(aggrsw)
for aggrsw in aggrswitches[4:]:
sim_hosts[1].add_switch(aggrsw)
for edgesw in edgeswitches[:4]:
sim_hosts[0].add_switch(edgesw)
for edgesw in edgeswitches[4:]:
sim_hosts[1].add_switch(edgesw)
for sim in servers[:8]:
sim_hosts[0].add_simulation(sim)
for sim in servers[8:]:
sim_hosts[1].add_simulation(sim)
self.custom_mapper = custom_mapper
def example_multilink(self) -> None:
self.roots = [FireSimSwitchNode()]
midswitch = FireSimSwitchNode()
lowerlayer = [midswitch for x in range(16)]
self.roots[0].add_downlinks(lowerlayer)
servers = [FireSimServerNode()]
midswitch.add_downlinks(servers)
def example_multilink_32(self) -> None:
self.roots = [FireSimSwitchNode()]
midswitch = FireSimSwitchNode()
lowerlayer = [midswitch for x in range(32)]
self.roots[0].add_downlinks(lowerlayer)
servers = [FireSimServerNode()]
midswitch.add_downlinks(servers)
def example_multilink_64(self) -> None:
self.roots = [FireSimSwitchNode()]
midswitch = FireSimSwitchNode()
lowerlayer = [midswitch for x in range(64)]
self.roots[0].add_downlinks(lowerlayer)
servers = [FireSimServerNode()]
midswitch.add_downlinks(servers)
def example_cross_links(self) -> None:
self.roots = [FireSimSwitchNode() for x in range(2)]
midswitches = [FireSimSwitchNode() for x in range(2)]
self.roots[0].add_downlinks(midswitches)
self.roots[1].add_downlinks(midswitches)
servers = [FireSimServerNode() for x in range(2)]
midswitches[0].add_downlinks([servers[0]])
midswitches[1].add_downlinks([servers[1]])
def small_hierarchy_8sims(self) -> None:
self.custom_mapper = "mapping_use_one_8_slot_node"
self.roots = [FireSimSwitchNode()]
midlevel = [FireSimSwitchNode() for x in range(4)]
servers = [[FireSimServerNode() for x in range(2)] for x in range(4)]
self.roots[0].add_downlinks(midlevel)
for swno in range(len(midlevel)):
midlevel[swno].add_downlinks(servers[swno])
def small_hierarchy_2sims(self) -> None:
self.custom_mapper = "mapping_use_one_8_slot_node"
self.roots = [FireSimSwitchNode()]
midlevel = [FireSimSwitchNode() for x in range(1)]
servers = [[FireSimServerNode() for x in range(2)] for x in range(1)]
self.roots[0].add_downlinks(midlevel)
for swno in range(len(midlevel)):
midlevel[swno].add_downlinks(servers[swno])
def example_1config(self) -> None:
self.roots = [FireSimSwitchNode()]
servers = [FireSimServerNode() for y in range(1)]
self.roots[0].add_downlinks(servers)
def example_2config(self) -> None:
self.roots = [FireSimSwitchNode()]
servers = [FireSimServerNode() for y in range(2)]
self.roots[0].add_downlinks(servers)
def example_4config(self) -> None:
self.roots = [FireSimSwitchNode()]
servers = [FireSimServerNode() for y in range(4)]
self.roots[0].add_downlinks(servers)
def example_8config(self) -> None:
self.roots = [FireSimSwitchNode()]
servers = [FireSimServerNode() for y in range(8)]
self.roots[0].add_downlinks(servers)
def example_16config(self) -> None:
self.roots = [FireSimSwitchNode()]
level2switches = [FireSimSwitchNode() for x in range(2)]
servers = [[FireSimServerNode() for y in range(8)] for x in range(2)]
for root in self.roots:
root.add_downlinks(level2switches)
for l2switchNo in range(len(level2switches)):
level2switches[l2switchNo].add_downlinks(servers[l2switchNo])
def example_32config(self) -> None:
self.roots = [FireSimSwitchNode()]
level2switches = [FireSimSwitchNode() for x in range(4)]
servers = [[FireSimServerNode() for y in range(8)] for x in range(4)]
for root in self.roots:
root.add_downlinks(level2switches)
for l2switchNo in range(len(level2switches)):
level2switches[l2switchNo].add_downlinks(servers[l2switchNo])
def example_64config(self) -> None:
self.roots = [FireSimSwitchNode()]
level2switches = [FireSimSwitchNode() for x in range(8)]
servers = [[FireSimServerNode() for y in range(8)] for x in range(8)]
for root in self.roots:
root.add_downlinks(level2switches)
for l2switchNo in range(len(level2switches)):
level2switches[l2switchNo].add_downlinks(servers[l2switchNo])
def example_128config(self) -> None:
self.roots = [FireSimSwitchNode()]
level1switches = [FireSimSwitchNode() for x in range(2)]
level2switches = [[FireSimSwitchNode() for x in range(8)] for x in range(2)]
servers = [
[[FireSimServerNode() for y in range(8)] for x in range(8)]
for x in range(2)
]
self.roots[0].add_downlinks(level1switches)
for switchno in range(len(level1switches)):
level1switches[switchno].add_downlinks(level2switches[switchno])
for switchgroupno in range(len(level2switches)):
for switchno in range(len(level2switches[switchgroupno])):
level2switches[switchgroupno][switchno].add_downlinks(
servers[switchgroupno][switchno]
)
def example_256config(self) -> None:
self.roots = [FireSimSwitchNode()]
level1switches = [FireSimSwitchNode() for x in range(4)]
level2switches = [[FireSimSwitchNode() for x in range(8)] for x in range(4)]
servers = [
[[FireSimServerNode() for y in range(8)] for x in range(8)]
for x in range(4)
]
self.roots[0].add_downlinks(level1switches)
for switchno in range(len(level1switches)):
level1switches[switchno].add_downlinks(level2switches[switchno])
for switchgroupno in range(len(level2switches)):
for switchno in range(len(level2switches[switchgroupno])):
level2switches[switchgroupno][switchno].add_downlinks(
servers[switchgroupno][switchno]
)
@staticmethod
def supernode_flatten(arr: List[Any]) -> List[Any]:
res: List[Any] = []
for x in arr:
res = res + x
return res
def supernode_example_6config(self) -> None:
self.roots = [FireSimSwitchNode()]
self.roots[0].add_downlinks([FireSimSuperNodeServerNode()])
self.roots[0].add_downlinks([FireSimDummyServerNode() for x in range(5)])
def supernode_example_4config(self) -> None:
self.roots = [FireSimSwitchNode()]
self.roots[0].add_downlinks([FireSimSuperNodeServerNode()])
self.roots[0].add_downlinks([FireSimDummyServerNode() for x in range(3)])
def supernode_example_8config(self) -> None:
self.roots = [FireSimSwitchNode()]
servers = UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(2)
]
)
self.roots[0].add_downlinks(servers)
def supernode_example_16config(self) -> None:
self.roots = [FireSimSwitchNode()]
servers = UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(4)
]
)
self.roots[0].add_downlinks(servers)
def supernode_example_32config(self) -> None:
self.roots = [FireSimSwitchNode()]
servers = UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(8)
]
)
self.roots[0].add_downlinks(servers)
def supernode_example_64config(self) -> None:
self.roots = [FireSimSwitchNode()]
level2switches = [FireSimSwitchNode() for x in range(2)]
servers = [
UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(8)
]
)
for x in range(2)
]
for root in self.roots:
root.add_downlinks(level2switches)
for l2switchNo in range(len(level2switches)):
level2switches[l2switchNo].add_downlinks(servers[l2switchNo])
def supernode_example_128config(self) -> None:
self.roots = [FireSimSwitchNode()]
level2switches = [FireSimSwitchNode() for x in range(4)]
servers = [
UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(8)
]
)
for x in range(4)
]
for root in self.roots:
root.add_downlinks(level2switches)
for l2switchNo in range(len(level2switches)):
level2switches[l2switchNo].add_downlinks(servers[l2switchNo])
def supernode_example_256config(self) -> None:
self.roots = [FireSimSwitchNode()]
level2switches = [FireSimSwitchNode() for x in range(8)]
servers = [
UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(8)
]
)
for x in range(8)
]
for root in self.roots:
root.add_downlinks(level2switches)
for l2switchNo in range(len(level2switches)):
level2switches[l2switchNo].add_downlinks(servers[l2switchNo])
def supernode_example_512config(self) -> None:
self.roots = [FireSimSwitchNode()]
level1switches = [FireSimSwitchNode() for x in range(2)]
level2switches = [[FireSimSwitchNode() for x in range(8)] for x in range(2)]
servers = [
[
UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(8)
]
)
for x in range(8)
]
for x in range(2)
]
self.roots[0].add_downlinks(level1switches)
for switchno in range(len(level1switches)):
level1switches[switchno].add_downlinks(level2switches[switchno])
for switchgroupno in range(len(level2switches)):
for switchno in range(len(level2switches[switchgroupno])):
level2switches[switchgroupno][switchno].add_downlinks(
servers[switchgroupno][switchno]
)
def supernode_example_1024config(self) -> None:
self.roots = [FireSimSwitchNode()]
level1switches = [FireSimSwitchNode() for x in range(4)]
level2switches = [[FireSimSwitchNode() for x in range(8)] for x in range(4)]
servers = [
[
UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(8)
]
)
for x in range(8)
]
for x in range(4)
]
self.roots[0].add_downlinks(level1switches)
for switchno in range(len(level1switches)):
level1switches[switchno].add_downlinks(level2switches[switchno])
for switchgroupno in range(len(level2switches)):
for switchno in range(len(level2switches[switchgroupno])):
level2switches[switchgroupno][switchno].add_downlinks(
servers[switchgroupno][switchno]
)
def supernode_example_deep64config(self) -> None:
self.roots = [FireSimSwitchNode()]
level1switches = [FireSimSwitchNode() for x in range(2)]
level2switches = [[FireSimSwitchNode() for x in range(1)] for x in range(2)]
servers = [
[
UserTopologies.supernode_flatten(
[
[
FireSimSuperNodeServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
FireSimDummyServerNode(),
]
for y in range(8)
]
)
for x in range(1)
]
for x in range(2)
]
self.roots[0].add_downlinks(level1switches)
for switchno in range(len(level1switches)):
level1switches[switchno].add_downlinks(level2switches[switchno])
for switchgroupno in range(len(level2switches)):
for switchno in range(len(level2switches[switchgroupno])):
level2switches[switchgroupno][switchno].add_downlinks(
servers[switchgroupno][switchno]
)
def dual_example_8config(self) -> None:
"""two separate 8-node clusters for experiments, e.g. memcached mutilate."""
self.roots = [FireSimSwitchNode()] * 2
servers = [FireSimServerNode() for y in range(8)]
servers2 = [FireSimServerNode() for y in range(8)]
self.roots[0].add_downlinks(servers)
self.roots[1].add_downlinks(servers2)
def triple_example_8config(self) -> None:
"""three separate 8-node clusters for experiments, e.g. memcached mutilate."""
self.roots = [FireSimSwitchNode()] * 3
servers = [FireSimServerNode() for y in range(8)]
servers2 = [FireSimServerNode() for y in range(8)]
servers3 = [FireSimServerNode() for y in range(8)]
self.roots[0].add_downlinks(servers)
self.roots[1].add_downlinks(servers2)
self.roots[2].add_downlinks(servers3)
def no_net_config(self) -> None:
self.roots = [FireSimServerNode() for x in range(self.no_net_num_nodes)]
# Spins up all of the precompiled, unnetworked targets
def all_no_net_targets_config(self) -> None:
hwdb_entries = [
"firesim_boom_singlecore_no_nic_l2_llc4mb_ddr3",
"firesim_rocket_quadcore_no_nic_l2_llc4mb_ddr3",
]
assert len(hwdb_entries) == self.no_net_num_nodes
self.roots = [
FireSimServerNode(hwdb_entries[x]) for x in range(self.no_net_num_nodes)
]
########################################################################
# DOC include start: user_topology.py fireaxe_topology_config
def fireaxe_topology_config(
self,
hwdb_entries: Dict[int, str],
edges: List[FireAxeEdge],
slotid_to_pidx: List[int],
mode: PartitionMode,
) -> None:
pidx_to_slotid = dict()
for sid, pid in enumerate(slotid_to_pidx):
pidx_to_slotid[pid] = sid
# Create partition nodes
pidx_to_partition_node: Dict[int, PartitionNode] = dict()
for pidx, hwdb in hwdb_entries.items():
node = PartitionNode(hwdb, pidx)
pidx_to_partition_node[pidx] = node
# Add edges to nodes
for edge in edges:
u_node = pidx_to_partition_node[edge.u.pidx]
v_node = pidx_to_partition_node[edge.v.pidx]
u_node.add_edge(edge.u.bidx, edge.v.bidx, v_node)
v_node.add_edge(edge.v.bidx, edge.u.bidx, u_node)
# Create PartitionConfigs and FireSimServerNode
servers: Dict[int, FireSimServerNode] = dict()
for pidx, node in pidx_to_partition_node.items():
partition_cfg = PartitionConfig(node, pidx_to_slotid, mode)
servers[pidx_to_slotid[node.pidx]] = FireSimServerNode(
partition_cfg.get_hwdb(), partition_config=partition_cfg
)
# Sort the servers by their sim slot id
servers = dict(sorted(servers.items()))
self.roots = list(servers.values())
# DOC include end: user_topology.py fireaxe_topology_config
# DOC include start: user_topology.py fireaxe_rocket_fastmode_config
def fireaxe_rocket_fastmode_config(self) -> None:
# DOC include start: fireaxe_fastmode_config hwdb_entries
# hwdb_entries maps the partition index to the hwdb name
hwdb_entries = {0: "f1_rocket_split_soc_fast", 1: "f1_rocket_split_tile_fast"}
# DOC include end: fireaxe_fastmode_config hwdb_entries
# DOC include start: fireaxe_fastmode_config slot_to_pidx
# slotid_to_pidx maps the partition index to the FPGA slotid
# For instance, `slotid_to_pidx = [2, 1, 0]` will map partition
# index 2 to simulation slot 0, partition index 1 to simulation slot 1,
# and partition index 0 to simulation slot 2.
slotid_to_pidx = [0, 1]
# DOC include end: fireaxe_fastmode_config slot_to_pidx
# DOC include start: fireaxe_fastmode_config edges
# The `FireAxeEdge` class is used to depict the connections between the partitions.
edges = [FireAxeEdge(FireAxeNodeBridgePair(0, 0), FireAxeNodeBridgePair(1, 0))]
# DOC include end: fireaxe_fastmode_config edges
# DOC include start: fireaxe_fastmode_config mode
# The PartitionMode enum contains the different partition modes that are available
mode = PartitionMode.FAST_MODE
# DOC include end: fireaxe_fastmode_config mode
# DOC include start: fireaxe_fastmode_config summing it all up
self.fireaxe_topology_config(hwdb_entries, edges, slotid_to_pidx, mode)
# DOC include end: fireaxe_fastmode_config summing it all up
# DOC include end: user_topology.py fireaxe_rocket_fastmode_config
# DOC include start: user_topology.py fireaxe_rocket_exactmode_config
def fireaxe_rocket_exactmode_config(self) -> None:
hwdb_entries = {
0: "f1_firesim_rocket_tile_exact",
1: "f1_firesim_rocket_soc_exact",
}
slotid_to_pidx = [0, 1]
edges = [
# DOC include start: fireaxe_rocket_exactmode_config edge 0
FireAxeEdge(FireAxeNodeBridgePair(0, 0), FireAxeNodeBridgePair(1, 0)),
# DOC include end: fireaxe_rocket_exactmode_config edge 0
# DOC include start: fireaxe_rocket_exactmode_config edge 1
FireAxeEdge(FireAxeNodeBridgePair(0, 1), FireAxeNodeBridgePair(1, 1)),
# DOC include end: fireaxe_rocket_exactmode_config edge 1
]
# DOC include start: fireaxe_rocket_exactmode_config mode
mode = PartitionMode.EXACT_MODE
# DOC include end: fireaxe_rocket_exactmode_config mode
self.fireaxe_topology_config(hwdb_entries, edges, slotid_to_pidx, mode)
# DOC include end: user_topology.py fireaxe_rocket_exactmode_config
# DOC include start: user_topology.py fireaxe_ring_noc_config
def fireaxe_ring_noc_config(self) -> None:
hwdb_entries = {
0: "xilinx_u250_quad_rocket_ring_0",
1: "xilinx_u250_quad_rocket_ring_1",
2: "xilinx_u250_quad_rocket_ring_base",
}
slotid_to_pidx = [0, 1, 2]
# DOC include start: fireaxe_ring_noc_config edges
edges = [
FireAxeEdge(FireAxeNodeBridgePair(0, 0), FireAxeNodeBridgePair(2, 1)),
FireAxeEdge(FireAxeNodeBridgePair(2, 0), FireAxeNodeBridgePair(1, 1)),
FireAxeEdge(FireAxeNodeBridgePair(1, 0), FireAxeNodeBridgePair(0, 1)),
]
# DOC include end: fireaxe_ring_noc_config edges
mode = PartitionMode.NOC_MODE
self.fireaxe_topology_config(hwdb_entries, edges, slotid_to_pidx, mode)
# DOC include end: user_topology.py fireaxe_ring_noc_config
# ######Used only for tutorial purposes####################
# def example_sha3hetero_2config(self):
# self.roots= [FireSimSwitchNode()]
# servers = [FireSimServerNode(server_hardware_config=
# "firesim_boom_singlecore_nic_l2_llc4mb_ddr3"),
# FireSimServerNode(server_hardware_config=
# "firesim_rocket_singlecore_sha3_nic_l2_llc4mb_ddr3")]
# self.roots[0].add_downlinks(servers)