As outlined before, an ideal interface model would ensure that each component has no knowledge of the names of entities in other components to which it connects. Also, these components should not know about the methodology or programming paradigm used to construct other components. For maximum independence in building components, which leads to reusability of components, the interface should be a contract between a component and the component framework, rather than between two components.
Our interface model requires components to specify the data they use and produce. It takes the connection specification (glue) between components out of the component code. The connection specification code may even be written as a script that is translated and compiled into the application. This approach provides the application composer and the runtime system with a control-point to maximize reuse of components. Asynchronous remote method invocation semantics with message-driven execution (see section 2.2) is assumed for dispatching produced data to the component that uses them, thus supplying the runtime system with a control-point for effective resource utilization.
The interface of a component consists of two parts: a set of input ports, and a set of output ports. A component publishes the data it produces on its output ports. These data become visible (when scheduled by the runtime system) to the connected component's input port. The connection between an input port and an output port are specified outside of the object's code, using the Charisma interface language. Each object can be thought of as having its own scheduler which schedules method invocations based on the availability of data on any of its input ports, and possibly emitting data at its output ports. For message-driven object-based languages, such as Charm++, input ports of components have methods bound to them (see section 3.5), so that when data become available on the input port, a component method is enabled.3.2 For languages such as MPI, an input port may be treated as a ``pseudo-processor'' that the component receives messages from (see section 5.4). Unlike other interface models, Charisma does not dictate the language binding for the port, leaving it to the language's runtime system. This makes the transition to Charisma easier for the component developer. It is up to the language developers to ensure that their implementation of ports remains interoperable with other languages.
The following examples illustrate key concepts behind the Charisma interface model. We have chosen the Charm++ implementation to illustrate the interface model. Also, we have used the Charisma interface description language (IDL) for Charm++. As described in section 3.5, one can provide the interface description to the runtime system using a C++ API instead of Charisma IDL. The Charisma interface language translator generates C++ code that invokes this API.
A simple producer-consumer application using Charisma interface model is shown in figure 3.13. Note that both the producer and the consumer know nothing about each other's methods. Yet, with very simple scripting glue, they can be combined into a single program. Thus we achieve the separation of application execution from application definition. Individual component codes can be developed independently, because they do not specify application execution. They merely specify their definitions. For example, the producer component does not ask the runtime system to deliver the data to the consumer component. It merely tells the runtime system that the data is available for delivery. In this model, the producer does not have to name the peer component, and thus can be developed independently, and is therefore more reusable. The connection script outside of the component, along with the message-driven execution semantics specifies the actual execution.
{CodeOne}
class producer {
in Start(void);
in ProduceNext(void);
out PutData(int);
};
{CodeTwo}
producer::Start(void) {
data = 0;
PutData.emit(0);
}
producer::ProduceNext(void) {
data++;
PutData.emit(data);
}
{CodeThree}
class consumer {
in GetData(int);
out NeedNext(void);
};
{CodeFour}
consumer::GetData(int d) {
// do something with d
NeedNext.emit();
}
{CodeFive}
producer p;
consumer c;
connect p.PutData to c.GetData;
connect c.NeedNext to p.ProduceNext;
connect system.Start to p.Start;
In figure 3.13, the data types of the connected ports of producer and consumer match exactly. In general, wherever transformations between data types is possible, the system will implicitly apply such transformation. For example, if the type of producer::data is double, and the type of consumer::data is int, the system will automatically apply the required transformations, and will still allow them to be connected. However, if producer::data is Rational and consumer::data is Complex, then system will not allow the requested connection. Thus, the application composer can insert a transformer object (see figure 3.14) between the producer and the consumer, without having to rewrite any portions of producer or consumer.
A performance improvement hint ``inline'' (figure 3.14a) can be interpreted by the translator for the scripting language to execute the method associated with the input port immediately instead of putting it off for scheduling it later. This hint also guides the runtime system to place the transformer object on the same processor as the object that connects to its input.
{CodeOne}
class transformer {
inline in input(Rational);
out output(Complex);
};
{CodeTwo}
transformer::input(Rational d) {
Complex c;
c.re = d.num/d.den; c.im = 0;
output.emit(c);
}
{CodeThree}
producer p;
consumer c;
transformer t;
connect p.PutData to t.input;
connect t.output to c.GetData;
connect c.NeedNext to p.ProduceNext;
connect system.Start to p.Start;
The real power of this interface model comes from being able to define collections of such objects. For example, one could connect individual sub-image smoother components as a 2-D array (figure 3.15) to compose a parallel image smoother component (see figure 3.16).
{CodeOne}
class SubImage {
in[4] InBorder(Pixels *);
out[4] OutBorder(Pixels *);
}
{CodeTwo}
enum {EAST=0, WEST=1, NORTH=2, SOUTH=3};
class ImageSmoother <int N, int M> {
in[4] InBorder(Pixels *);
out[4] OutBorder(Pixels *);
in[2*N+2*M] InSurface(Pixels *);
out[2*N+2*M] OutSurface(Pixels *);
SubImage si[N][M];
// Make the east and west elements connections to surface
for(int i=0; i<N; i++) {
connect si[i][0].InBorder[WEST] to this.OutSurface[i];
connect si[i][0].OutBorder[WEST] to this.InSurface[i];
connect si[i][M-1].InBorder[EAST] to this.OutSurface[i+N];
connect si[i][M-1].OutBorder[EAST] to this.InSurface[i+N];
}
// Similarly connect north and south border elements to surface
for(int j=0; j<M; i++) {
// ...
}
// Now, make internal elements connect to each other
for(int i=1; i<=(N-1); i++) {
for(int j=1; j<=(M-1); j++) {
connect si[i][j].InBorder[0] to si[i-1][j].OutBorder[2];
// ...
}
}
}
The composite ImageSmoother component specifies connections for all the InBorder and OutBorder ports of its SubImage constituents. Note that by specifying connections for all its components' ports, and providing unconnected input and output ports with the same names and types as SubImage, ImageSmoother becomes portwise-isomorphic to SubImage and can be substituted for SubImage in any application. Code for ImageSmoother methods InBorder and InSurface is not shown here for lack of space. InBorder splits the input pixels into subarrays and emits them on OutSurface ports. InSurface buffers the pixels until all the border pixels are handed over to it from a particular direction. It then combines all the pixels into a single array, and emits them onto corresponding OutBorder port.
Also, note that the ImageSmoother component can configure itself with parameters N and M. N and M determine the number of rows and columns in a 2-D array of SubImages. They can be treated as attributes of the class ImageSmoother, which can be set through a script.
In this example, the number of ports in SubImage was fixed. User of this component was expected to feed and receive one array in each of the four directions. The above example demonstrates that one can substitute a parallel component in place of the sequential component by writing appropriate glue code to keep the original interface with fixed number of ports. One can design the interfaces so that the number of ports of a component can be configured by the application composer, as the next example shows. The configurable parameter supplied by the application script to the component acts as a guideline to the component for deciding its internal parallel structure.
Consider the problem of interfacing Fluids and Solids modules in a coupled
simulation [63]. Each of the Fluids and Solids component is
implemented as a parallel object. (Constituents of these modules, namely
FluidsChunk and SolidsChunk, are not shown here for the sake
of brevity.) A fluid-solid interface component FSInter specific to the
application-domain is used to connect an arbitrary number of Fluids chunks to
any number of Solids chunks by carrying out the appropriate interpolations.
The core interface description of this situation is shown in
figure 3.17. Figure 3.17(a)
shows the interface of the parallel Fluids component, and
figure 3.17(b) shows the interface of the parallel
Solids component. Each of these components have a configurable
parameter that determines the number of ports presented by each component. The
FSInter component (Figure 3.17(c)) presents
two sets of ports, one for connecting to Fluids and the other to
Solids. Figure 3.17(d) shows the skeleton
application code that configures Fluids and Solids to have 32
and 64 input and output ports, respectively. The two sets of ports in
FSInter are then configured accordingly to appropriately connect the
Fluids component with 32 ports and Solids component with 64
ports. The Fluids and Solids components interacts solely with
FSInter, which carries out the interpolation of data it receives from
either either component, and passes it on to the other component at each
timestep. Note that the configurable parameters only dictate the number of
ports a component contains, and does not enforce the parallel structure on the
component. For example, when configured to have 32 input and output ports, the
Fluids component may internally partition its grid into
chunks, with its border chunks connected to FSInter with 32 ports as
shown in figure 3.18. Alternatively, it may partition its
grid in chunks equal in number to available processors, and can still split the
grid boundary data into 32 parts that are published on 32 ports
(Figure 3.19).
{CodeOne}
class Fluids<int N> {
in[N] Input(FluidInput);
out[N] Output(FluidOutput);
};
{CodeTwo}
class Solids<int M> {
in[M] Input(SolidInput);
out[M] Output(SolidOutput);
};
{CodeThree}
class FSInter<int F, int S> {
in[F] FInput(FluidOutput);
out[F] FOutput(FluidInput);
in[S] SInput(SolidOutput);
out[S] SOutput(SolidInput);
};
{CodeFour}
Fluids f<32>;
Solids s<64>;
FSInter fs<32,64>;
for(int i=0;i<32;i++){
connect f.Output[i] to fs.FInput[i];
connect fs.FOutput[i] to f.Input[i];
}
for(int i=0;i<64;i++){
connect s.Output[i] to fs.SInput[i];
connect fs.SOutput[i] to s.Input[i];
}