Core concepts¶
Everything in a sequential llmsim model is built from four ideas: the Sim that owns the clock and the event queue, the events processes wait on, the processes themselves, and the conditions that compose events.
The Sim¶
Sim is the simulation: a clock (sim.now), a
priority queue of scheduled events, and a random number generator. A Sim and
everything attached to it belong to exactly one thread — the sequential
hot path takes no locks.
sim = llmsim.Sim(seed=0) # seeded RNG for reproducibility
sim = llmsim.Sim(rng=my_random.Random()) # or bring your own Random
sim.run(until=...) drives the loop; until may be None (run until the
schedule empties), a time, or an Event (run until
it fires and return its value).
Events¶
An event is a value that becomes available at some simulated time. A process
yields an event to suspend until the event fires; the yield expression then
evaluates to the event's value.
sim.delay(t, value=None)returns aTimeoutthat firestunits from now.sim.event()returns a freshEventyou trigger yourself — the building block for custom synchronization.
Events are generic in their result type: Event[T] fires with a value of type
T, so value = yield some_event is fully typed.
Processes¶
A Process is a running generator (or
coroutine). Start one with
sim.spawn(fn, *args, **kwargs); the Sim is
injected as fn's first argument. A process is itself an event — you can
yield another process to wait for it to finish:
def wash(sim):
yield sim.delay(3.0)
return "clean"
def owner(sim):
result = yield sim.spawn(wash) # wait for wash to complete
assert result == "clean"
sim.spawn(owner)
Interrupts¶
One process can interrupt another. The interrupted process sees an
Interrupt raised at its current yield:
def driver(sim, worker):
yield sim.delay(2.0)
worker.interrupt("stop")
def worker(sim):
try:
yield sim.delay(100.0) # a long wait...
except llmsim.Interrupt as interrupt:
print("interrupted:", interrupt.cause) # ...cut short at t=2
Conditions¶
Conditions wait on sets of events:
sim.all_of(events)— anAllOfthat fires when every event has fired (a barrier / join).sim.any_of(events)— anAnyOfthat fires when the first event fires (a race / timeout guard).
def fetch_with_timeout(sim, work):
done = sim.any_of([work, sim.delay(10.0)])
result = yield done
if work in result:
return result[work]
raise TimeoutError("work did not finish in 10 units")
Both are Conditions and compose, so a
condition can itself be an operand of another.
Errors¶
The engine's exceptions all derive from
SimulationError.
EmptySchedule signals that a run(until=event)
can never complete because the queue drained first.
Interrupt is the process-interruption signal
above.