How to integrate an existing architectural performance simulator with SST.

MUCH OF THIS INFORMATION IS OBSOLETE - NEW DOCUMENTATION IS UNDER DEVELOPMENT

Introduction

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:

  1. interacting with other performance simulators that have been integrated with SST (e.g. DRAMSim2, gem5, Iris),
  2. utilizing SST’s services for power/temperature/reliability modeling, and
  3. assessing to SST’s parallel simulation environment. The SST aims, over time, to become a standard simulation environment for designing HPC systems by helping Industry, Academia, and the National Labs in designing and evaluating future architectures.

Key Interfaces for Component Writers

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()).


Event Handlers

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

Creating Event Handlers

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.

Using Event Handlers

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::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 :

Other commonly used SST::Link functions are:


Wrapping Your Simulator for SST

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.


What you start with

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 Wrapper Class

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.

registerExit() and registerClock()

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.

Initialization

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.

Simulation Loop

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;
}
...

Finishing Up

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;
}

create, ElementInfoComponent, ElementLibraryInfo

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.


Python Input file

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> 

Compiling

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.


Running

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.