Skip to content

Execution Model

The previous pages introduced modules, nets, time, behavior, and tokens. This page ties them together by explaining how the Sitar simulation kernel actually runs a model step by step.


What "Running a Module" Means

Each module has a run(cycle, phase) function generated from its behavior block. Calling run() on a module nudges it: execution proceeds through the behavior until the next wait statement is reached, at which point the module suspends and control returns to the kernel. The module's execution state (its position in the behavior, all local variables) is preserved between calls.

For hierarchical execution, Sitar also provides a runHierarchical() function for each module. This first nudges the module's own behavior, then calls runHierarchical() on each child submodule in turn, recursively down the hierarchy.

Kernel behavior and customization

The kernel maintains a flat list of all modules in the system, as well as a reference to TOP which in turn holds references to its child modules and so on.

  • By default:, the kernel calls run() on every module in the flat list once per phase. The execution order among modules within a phase does not matter.

  • An alternative: is to call runHierarchical() on just TOP once per phase, which traverses the entire hierarchy in declaration order.

The main execution loop is generated as readable C++ code and can be customized by the user to suit specific needs, such as a custom static thread-to-module mapping for parallel execution. This is described in detail in Enabling Parallel Execution.


The Sequential Execution Loop

The basic simulation loop is straightforward:

cycle = 0
while (cycle < simulation_end_time)
{
    phase = 0
    for each module m:
        m.run(cycle, phase)

    phase = 1
    for each module m:
        m.run(cycle, phase)

    cycle = cycle + 1
}

Every module is run exactly once per phase. The execution order among modules within a phase does not affect the result. This is guaranteed by the two-phase rule: in phase 0 all modules only read from nets; in phase 1 all modules only write to nets. There are no read-write or write-write conflicts between modules within a phase.

Why ordering doesn't matter

Because nets can only be read in phase 0 and written in phase 1, no module can observe another module's output from the same phase. All reads see the state of nets as of the end of the previous phase. This is precisely what makes the execution order irrelevant and parallelization straightforward.


Parallel Execution with OpenMP

Since execution order among modules is irrelevant within a phase, the loop over modules can be parallelized trivially. Sitar uses OpenMP for this:

cycle = 0
while (cycle < simulation_end_time)
{
    phase = 0
    #pragma omp for
    for (m = 0; m < num_modules; m++)
        module[m].run(cycle, phase)
    //implicit omp barrier

    phase = 1
    #pragma omp for
    for (m = 0; m < num_modules; m++)
        module[m].run(cycle, phase)
    //implicit omp barrier

    cycle = cycle + 1
}

The modules are distributed across threads by OpenMP's scheduler (either dynamically, or according to a static mapping specified by the modeler). All threads synchronize at the end of each phase via a barrier. No locks or critical sections are needed for net access, since the two-phase rule guarantees at most one writer and one reader per net per phase.

sequenceDiagram
    participant T1 as Thread 1
    participant T2 as Thread 2
    participant T3 as Thread 3
    Note over T1,T3: Phase 0
    T1->>T1: run modules A, B
    T2->>T2: run modules C, D
    T3->>T3: run modules E, F
    Note over T1,T3: barrier
    Note over T1,T3: Phase 1
    T1->>T1: run modules A, B
    T2->>T2: run modules C, D
    T3->>T3: run modules E, F
    Note over T1,T3: barrier
    Note over T1,T3: cycle complete

Enabling parallel execution

Parallel execution requires no changes to the model. Compile with the --openmp flag:

sitar compile --openmp
See Enabling Parallel Execution for details on thread mapping and performance tuning.


Modeling Scope and the Moore Restriction

A consequence of the two-phase rule is that Sitar can only model systems where every path from one module's input to the next (a different) module's input passes through at least one net (and therefore incurs at least one cycle of latency). This is equivalent to requiring that all inter-module communication is through Moore-type components: components whose outputs depend only on their state at the start of a cycle, not on their inputs within the same cycle.

This restriction rules out purely combinational (zero-latency) paths between separate modules. Such paths can still be modeled by placing the interacting components within a single module as branches of a parallel block, where execution order is fixed and deterministic. Each branch can optionally be wrapped as a procedure for modularity. Branches of a parallel block are executed sequentially, and multiple times (if required) until convergence.See Parallel for details on parallel blocks.

Summary

Sitar's execution model in one sentence: run every module once in phase 0, synchronize, run every module once in phase 1, synchronize, advance the cycle.

The two-phase read/write discipline makes this loop correct, deterministic, and straightforward to parallelize. The cost is two barrier synchronizations per clock cycle. This overhead is justified when the model is large enough, or has heavy compute in each phase, so that each thread has significant compute work per phase, and the time spent at barriers is small relative to the useful work done between them.

What's Next

With the core concepts and execution model in place, the next step is learning the full Sitar modeling language in detail. The Language and Examples section covers every construct with examples. If you prefer to dive straight into complete working models first, jump to Basic Examples.