Navigation: Home | Downloads | Getting SST | Documentation | Support
`— title: How to integrate an existing architectural performance simulator with SST. published: true category: SSTDocumentation —
#
MUCH OF THIS INFORMATION IS OBSOLETE - NEW DOCUMENTATION IS UNDER DEVELOPMENT
The SST provides a framework for simulating large-scale HPC systems. It allows parallel simulation of large machines at multiple levels of detail. The SST tends to include existing architectural performance simulators and couples models for processors, memory, and network subsystems. An architectural performance simulator can gain several benefits from the integration with SST including:
SST::Component
The most important class for is SST::Component, the base class from which all simulation components inherit. At the very least, a component writer must create a class which inherits from SST::Component and which contains a constructor.
//simpleComponent.h
#include <sst/core/sst_types.h>
#include <sst/core/component.h>
namespace SST {
namespace SimpleComponent {
class simpleComponent : public SST::Component {
public:
simpleComponent(SST::ComponentId_t id, SST::Params& params);
int Setup() {return 0;}
int Finish() {return 0;}
private:
SST::Link* N;
SST::Link* S;
SST::Link* E;
SST::Link* W;
};
}
}
Note that SST::Component
only requires the id
argument. params
is used to get parameters from the Python File to your component.
This class should be created in a dynamic library (.so file) and should include a C-style function of the form create
_ComponentName
, which returns a pointer to a newly instantiated component. In addition, to prevent name-clashes, it is highly-recommended that all symbols (other than the create
_ComponentName
function) be wrapped in a C++ namespace.
//simpleComponent.cc
#include "simpleComponent.h"
using namespace SST::SimpleComponent;
...
static Component* create_simpleComponent(SST::ComponentId_t id,
SST::Params& params)
{
return new simpleComponent( id, params );
}
Two other pieces of code required at the end of your .cc file are a ElementInfoComponent and an ElementLibraryInfo object.
//simpleComponent.cc
...
static const ElementInfoComponent components[] = {
{
"simpleComponent",
"Simple Demo Component",
NULL,
create_simpleComponent
},
{ NULL, NULL, NULL, NULL }
};
ElementLibraryInfo simpleComponent_eli = {
"simpleComponent",
"Demo Component",
components,
};
SST::Component also contains useful functions for component setup (SST::Component::Setup()), cleanup (SST::Component::Finish()), power reporting (SST::Component::regPowerStats()), data monitoring by the introspectors (SST::Component::registerMonitorInt() and SST::Component::getIntData()), controlling when the simulation stops (SST::Component::registerExit() and SST::Component::unregisterExit()), and for handling time (such as SST::Component::getCurrentSimTime()).
SST components use event handling functors to handle interactions with other components (i.e. through an SST::Event sent over a SST::Link) and recurring events (i.e. component clock ticks). The Event Handler
Class, SST::Event::Handler, is templated to create a event handler by creating a functor which invokes a given member function whenever triggered. For example:
//simpleComponent.cc
#include "simpleComponent.h"
using namespace SST;
using namespace SST::SimpleComponent;
...
simpleComponent::simpleComponent(ComponentId_t id, Params& params):Component(id){
...
linkEventHandler = new Event::Handler<simpleComponent>(this, &simpleComponent::handleEvent) );
...
}
...
void simpleComponent::handleEvent(Event *ev) {
...
}
creates an event handler which calls simpleComponent::handleEvent() with an argument of type Event*
- the SST::Event to be processed. Similarly, SST::Clock::Handler, is templated to create a handler that is triggered by a clock
//simpleComponent.cc
#include "simpleComponent.h"
using namespace SST;
using namespace SST::SimpleComponent;
...
simpleComponent::simpleComponent(ComponentId_t id, Params& params):Component(id){
...
clockHandler = new Clock::Handler<simpleComponent>(this, &simpleComponent::clockTic );
...
}
...
bool simpleComponent::clockTic(Cycle_t) {
...
return false;
}
creates an event handler which invokes the function simpleComponent::clockTic() with the current cycle time.
Once created, an SST::EventHandler must be registered with the simulation core. This can be done with the SST::Component::configureLink() function for events coming from another component, or by SST::Component::registerClock(), for event handlers triggered by a clock. For example, the handlers created above could be registered in this way:
//simpleComponent.cc
#include "simpleComponent.h"
using namespace SST;
using namespace SST::SimpleComponent;
...
simpleComponent::simpleComponent(ComponentId_t id, Params& params):Component(id){
...
N = configureLink( "Nlink",linkEventHandler );
registerClock( "1Ghz", clockHandler );
...
}
Note that SST::Component::registerClock() can have its period or frequency expressed in SI units in a string. The allowed units are specified in SST::TimeLord::getTimeConverter() function.
Also note that the SST::Component::configureLink() function does not require an event handler if the receiving component uses the “event pull” mechanism with SST::Link::Recv().
SST::Link
SST::Component_s use _SST::Link to communicate by passing events. An SST::Link is specified in the XML file use to configure the simulation, and must also be added to each component which uses it by the SST::Component::configureLink() function. For example,
...
<component name=c0 type=simpleComponent.simpleComponent>
<params>
<workPerCycle>1000</workPerCycle>
<commFreq>100000</commFreq>
<commSize>100</commSize>
</params>
<link name=Nlink port=A lat=10ns />
<link name=Slink port=A lat=10ns />
<link name=Elink port=A lat=10ns />
<link name=Wlink port=A lat=10ns />
</component>
...
specifies two components, a processor and a memory. These components are connected by an SST::Link. Each link element contains a name, a port, and a lat :
The name is a global identifier for the SST::Link object. Since multiple components connect to the link (“memory” and “processor” in this case), each component has a link block and the name is used to make it clear that they are both referencing the same link object.
The port specifies which port of the link we are connecting to
The lat specifies the latency of the link in SI units. This is the minimum latency for an event to be passed from one end of the link to another. I.e. After the event is sent from one component with SST::Link::Send(), it will cause the event handler on the other component to be invoked after at least this period of time. More time can be added to this delay with different varients of the SST::Link::Send() function.
Other commonly used SST::Link functions are:
SST::Link::Send(CompEvent *event)
: Send an SST::Event
across the link.
SST::Link::Send(SimTime_t delay, CompEvent *event)
: Send an SST::Event
with an additional delay, where the delay is in units of the link frequency.
SST::Link::Send(SimTime_t delay, TimeConverter *tc, CompEvent *event)
: Send an SST::Event
with additional delay, where the delay is specified in units of the SST::TimeConverter
object supplied.
SST::Link::Recv()
: Pull a pending SST::Event
from the SST::Link
. If there is no event handler registered with the Link, this function should be used to gather incoming events from the link. (I.e. a component “pulls” events from the link, rather than having them “pushed” into an event handler).
For this example, a few assumptions are made, these aren’t necessarily requirements for SST, but they will be stated in an attempt to mitigate confusion.
If your component doesn’t fit these requirements, this section should still give a pretty good explanation of how to integrate with SST.
Below is an example of a basic cpu simulator. We are not concerned with the internal operation of the simulator and we assume that it was tested and verified as a stand-alone unit.
class CPU{
public:
CPU(){...}
~CPU(){...}
void init(bool B){...}
void preReport(){...}
void postReport(){...}
bool simulate_tic(){...}
long param1;
bool param2;
char* param3;
}
int main(int argc, char **argv)
{
//**local control variables
int arg1;
bool arg2;
char* arg3;
int cycle_count;
bool done;
long max_cycles;
//**Instance of simulator
CPU *my_cpu;
//**Assign defaults;
arg1=0;
arg2=false;
arg3="blank";
//**Read in input arguments
for (i = 1; i < argc; i++) {
//integer argument
if (!strcmp("--arg1",argv[i])) {
if (i == argc-1) doHelp();
arg1 = atoi(argv[++i]);
}
//bool argument
else if (!strcmp("--arg2",argv[i])) {
arg2 = true;
}
//string argument
else if (!strcmp("--arg3",argv[i])) {
if (i == argc-1) doHelp();
arg3 = argv[++i];
}
}//end arguments
//**Initialize
my_cpu= new CPU();
my_cpu->init(arg2);
my_cpu->param1=arg1;
my_cpu->param3=arg3;
cycle_count=0;
done=false;
max_cycles=100000;
//**simulate
my_cpu->preReport(); //Before simulation
for(; !done; ++cycle_count)
done=(my_cpu->simulate_tic() ) | (cycl_count>=max_cycles);
my_cpu->postReport(); //After simulation
//**Clean up
delete my_cpu;
my_cpu=0;
}//main
The function of the wrapper class is to take the place of the main. In more complex systems, it will also handle and routes events that come in through links. But for this example, we are just trying to get the simulator runnable by SST. The first step is setting up the basic wrapper class. All components simualted in SST are derived from SST::Component which requires an component id as an argument. In addition, when SST calls the implemented Component it also passes a params argument which contains the parameters defined in the Python File. Below is our basic wrapper class.
//sst_CPU.h
#include "CPU.h"
#include <sst/core/component.h>
namespace SST {
namespace SST_CPU {
class sst_CPU : public SST::Component{
public:
sst_CPU(SST::ComponentId_t id, SST::Params& params);
sst_CPU();
~sst_CPU();
//**Override SST::Component Virtual Methods
int Setup( );
int Finish( );
bool Status( );
};
}
}
This our basic wrapper class, it has the ability to pass in the component id and the parameters and overrides the virtual methods of SST::Component
Next we need to implement the local control variables and an instance of the simulator, the simplest way to do this is to incorporate them as members of the wrapper class
//sst_CPU.h
#include "CPU.h"
#include <sst/core/component.h>
namespace SST {
namespace SST_CPU {
class sst_CPU : public SST::Component{
public:
sst_CPU(SST::ComponentId_t id, SST::Params& params);
sst_CPU();
~sst_CPU();
//**Override SST::Component Virtual Methods
int Setup( );
int Finish( );
bool Status( );
...
...
private:
//**Instance of simulator
CPU *my_cpu;
//**local control variables
int arg1;
bool arg2;
char* arg3;
int cycle_count;
bool done;
long max_cycles;
...
...
};
}
}
This allows us to access them in a similar fashion to that done in our standalone main.
We now need to implement the rest of the main(). This first step is done in the constructor, this is where we read in our parameters. With sst, these are not passed through the command line but, instead, are passed through the params argument and originate in the Python File. In our constructor, we look for parameters and assign defaults in their absence. All arguments come in the form of char* strings and have to be parsed.
//sst_CPU.cc
#include sst_CPU.h
using namespace SST; //This is handy because a lot of what is used is from SST::
using namespace SST::SST_CPU;
sst_CPU(SST::ComponentId_t id, SST::Params& params):Component(id){
//Integer: Convert string to integer
if ( params.find("arg1") == params.end() )
arg1=0;
else
arg1 = strtol( params[ "arg1" ].c_str(), NULL, 0 );
//Boolean: Convert String to integer and test for 0
if ( params.find("arg2") == params.end() )
arg2=false;
else
arg2 = (strtol( params[ "arg2" ].c_str(), NULL, 0 ))!=0;
//String: No conversion
if ( params.find("arg3") == params.end() )
arg3="blank";
else
arg3 = params[ "arg3" ].c_str();
}
We have combined the default value assignment with parameter detection. It is necesary to check whether the parameter exists before fetching so it a good time to assign a default value when it is not specified. The params object can be indexed with a string and that string should match a parameter from the Python File.
Because our simulator is stand alone and can self-determine when it is complete, we need to tell SST not to exit until we are done. This is done with SST::Component::registerExit(). SST will continue to run until a predetermined time or until all components call SST::Component::unregisterExit(). Since we want SST to be in control of the clock, we need to link an SST clock into our component with SST::Componenter::registerClock().
//sst_CPU.cc
#include sst_CPU.h
using namespace SST; //This is handy because a lot of what is used is from SST::
sst_CPU(SST::ComponentId_t id, SST::Params& params):Component(id){
//Integer: Convert string to integer
if ( params.find("arg1") == params.end() )
arg1=0;
else
arg1 = strtol( params[ "arg1" ].c_str(), NULL, 0 );
//Boolean: Convert String to integer and test for 0
if ( params.find("arg2") == params.end() )
arg2=false;
else
arg2 = (strtol( params[ "arg2" ].c_str(), NULL, 0 ))!=0;
//String: No conversion
if ( params.find("arg3") == params.end() )
arg3="blank";
else
arg3 = params[ "arg3" ].c_str();
registerExit();
registerClock( "1GHz", new Clock::Handler<sst_CPU,bool>(this, &sst_CPU::tic ) );
}
bool sst_CPU::tic(Cycle_t){ //The current cycle is passed via the Clock::Handler
return false;
}
We have now fetched our parameters, told SST not to exit until we are done, and registered a clock and a clock handling method. Instead of hardcoding the “1GHz” frequency, we could easily pass it in as a parameter from the Python File.
The Setup() is called before the simulation begins and is a good place to initialize variables, do pre simulation reporting and configuration.
//sst_cpu.cc
...
int sst_CPU::Setup(){
my_cpu= new CPU();
my_cpu->init(arg2);
my_cpu->param1=arg1;
my_cpu->param3=arg3;
cycle_count=0;
done=false;
max_cycles=100000;
preReport();
return 0;
}
We create an instance of our simulator, initialize it, and set up our looping variables. This is also where we would call pre simulation reports, open files, and do anything else we need before the loop starts. Again, we could pass max_cycles as a parameter instead of hard-coding it.
We no longer have a loop because SST takes care of that. We just need to translate the logic for ending into our tic function which is called for every clock cycle.
//sst_cpu.cc
...
bool sst_CPU::tic(Cycle_t){ //The current cycle is passed via the Clock::Handler
done=(my_cpu->simulate_tic() ) | (cycle_count>=max_cycles);
++cycle_count;
if(done)
unregisterExit(); //Tell SST it can finish the simulation
return done;
}
...
Finish() is called after SST completes the simulation. This is where we dump results and clean up after ourselves.
//sst_CPU.cc
...
int sst_CPU::Finish(){
my_cpu->postReport(); //After simulation
//**Clean up
delete my_cpu;
my_cpu=0;
return 0;
}
Next we need to make sure we include the maintenance elements. Without these, the code will compile, but you will get errors when you go to run.
//sst_CPU.cc
...
create_sst_CPU(SST::ComponentId_t id, SST::Params& params)
{
return new sst_CPU( id, params );
}
static const ElementInfoComponent components[] = {
{
"sst_CPU",
"Description of sst_CPU",
NULL,
create_sst_CPU
},
{ NULL, NULL, NULL, NULL }
};
ElementLibraryInfo sst_CPU_eli = {
"sst_CPU",
"Description of sst_CPU",
components,
};
With these last remaining objects, the wrapper is complete.
A full guide to the Python Input file is available on the Python File page. Here is what the file would look like for our component.
<?xml version="1.0"?>
<sdl version="2.0" />
<config>
run-mode=both
partitioner=self
</config>
<sst>
<component name="TheCpu" type="sst_CPU.sst_CPU">
<params>
<arg1><!--define arg1 -->
123
</arg1>
<arg2><!-- blanks parameters don't get through to params -->
</arg2>
<arg3><!-- comments are ignored completely -->
text1
<!-- only the first piece of text gets used -->
text2 <!-- text2 will get ignored -->
</arg3>
</params>
</component>
</sst>
The easiest way to make sure everything compiles smoothly is to use the build system already set up for SST. The first step is placing your project in it’s own directory under the sst/elements directory in the source tree you used to install SST. Note: This directory name must match the component library name (i.e. sst_CPU
in the example below). Next, create a Makefile.am file
# -*- Makefile -*-
#
#
AM_CPPFLAGS = \
$(BOOST_CPPFLAGS) \
$(MPI_CPPFLAGS)
compdir = $(pkglibdir)
comp_LTLIBRARIES = libsst_CPU.la
libsst_CPU_la_SOURCES = \
sst_CPU.h \
sst_CPU.cpp \
ANY_OTHER_FILES
libsst_CPU_la_LDFLAGS = -module -avoid-version
Then move the root of the source tree and run the autogen.sh script
$sstsource> ./autogen.sh
This will create a Makefile.in in your project directory. Next, run the configure script
$sstsource> ./configure
This will create a Makefile in your project directory. You can now run make from here or from your project directory and your libraries will be created.
You can now switch to your project directory and run sst.x
$sst_CPU> sst.x --lib-path .libs text.xml
The makefile generates a static library, but a shared library is a byproduct and libtool puts those byproducts in ‘.libs’. If you run make install, the shared library will be put into /usr/local/lib/sst directory and the –lib-path option will not be needed.