Akita Basics 4: Ticking
Ticking is a solution that can simplify event-driven simulator design. It provides a cycle-based simulation development experience while can still maintain the performance of event-driven simulation.
Cycle-based simulations usually require all the components to define a tick
function. A centralized controller calls the tick
function of all the components in each cycle. Very simple. However, in computer architecture simulations, components are often idle. Calling the tick
function in every cycle is not necessary.
Event-driven simulation is generally more efficient. When a component is idle, it does not schedule events, avoiding unnecessary calls to the tick
function. However, as we have seen in the previous tutorial, implementing an event-driven simulation introduces many lines of code. The component logic easily becomes hard to understand in an event-driven simulation.
Also, there is another intrinsic problem that pure event-driven simulation model cannot solve. When a component needs to send out a message but it fails to send because the connection is busy. What the component can do? The most reasonable solution is to retry sending the message later. Since the component does not know when the connection will free up, it can only schedule the same event in the next cycle. In general, the connection is not likely to be available in the next cycle. The component may need to retry many times before successfully sending out the message. All the retries are unnecessary if the component knows when the connection is available.
In Akita, we use ticking to solve improve developer experience, as well as avoid unnecessary ticks in event-driven simulation.
We first define a special type of component called TickingComponent
. A ticking component needs to embed the TickingCompnent
struct. A TickingComponent
only need to implement a `Tick` function. The tick
function takes the current time as argument and returns a boolean value. The return value indicates if the component is making progress or not in the current tick.
Here is an example of implementing the PingAgent
as a TickingComponent
.
type TickingPingAgent struct {
*akita.TickingComponent
}func NewTickingPingAgent(
name string,
engine akita.Engine,
freq akita.Freq,
) *TickingPingAgent {
a := &TickingPingAgent{}
a.TickingComponent = akita.NewTickingComponent(
name, engine, freq, a)
return a
}func (a *TickingPingAgent) Tick(now akita.VTimeInSec) bool {
panic(“not implemented”)
}
A key element in a ticking component is the frequency. At the beginning of the simulation, components do not tick. A component starts ticking when the component receives the first message. The component will keep ticking in the frequency. The component will stop ticking when the tick function returns false.
Stop ticking when the component is not making progress does not impact simulation result. Two reasons can cause a component to stop making progress. One is that the component is idle. It does not have any task to run. In this case, it does not need to tick. The other reason is that all the out-going port is congested. In this case, the component will not make any further progress until the connection frees up. Since it will not make progress, it does not need to continue ticking.
When the component receives a new message, the component is no longer idle. When the ports notifies the component that the connection is free, the connection is no longer congested. In both cases, we need to let the component to continue ticking. Remember the NotifyRecv
function and the NotifyPortFree
function? They are designed for these purposes. The TickingComponent
implements these two functions by scheduling the next TickEvent
. In this way, although we are not eliminating all the unnecessary ticks, we can avoid the majority of them. We also guarantee that we do not miss any tick that can make a progress. So the simulation result is equivalent to a traditional cycle-based scheduling.
Next, let’s take a closer look at how a Tick function is implemented. This is still the example of the TickingPingAgent
.
func (a *TickingPingAgent) Tick(now akita.VTimeInSec) bool {
madeProgress := false madeProgress = a.sendRsp(now) || madeProgress
madeProgress = a.sendPing(now) || madeProgress
madeProgress = a.countDown() || madeProgress
madeProgress = a.processInput(now) || madeProgress
return madeProgress
}
In the tick function, we divide the component into a few smaller stages. The whole tick function returns true (progress is made), if any of the stages made progress.
In this implementation, agent A creates a ping message in the sendPing
stage and send it over the network at the 1st cycle. Assuming the connection is a perfect connection that has 0 latency, agent B, the receiver, can pick the message up in the processInput
stage at the 2nd cycle. As we set the latency to 2 cycles, the message stays in the countDown
stage of agent B for the 3rd and 4th cycle. At the 5th cycle, agent B send the respond to agent A in the sendRsp
stage. Finally, agent A can process the response at cycle 6. Therefore, the end-to-end latency is 5 cycles.
As a summary, Ticking is one of the most powerful feature of Akita. It allows a programmer to only implement a single function for each component, while still maintain the high performance of even-driven simulation. In the tick function, following the standard structure, a programmer can easily implemented a pipelined component.
In the next blog, we are going to look at how to extract some information form a simulation with hooking and tracing.
Next: Hooking and Tracing