LLM Inference Simulators — Presentation 08

Accelerating Inference Simulators

Making this kind of simulator fast without changing its answers: event abstraction, incremental state, lazy bookkeeping, exact macro-stepping, probe discipline, compiled kernels, parallel and multi-fidelity sweeps, surrogates, sampling, and parallel discrete-event simulation — each measured on the companion simulator.

Event abstraction Macro-stepping Profiling Multi-fidelity PDES Rust / PyO3
Profile → Fewer events → Cheaper events → Parallel runs → Smarter search
00

Topics We'll Cover

Concepts used here, and where they are explained. Each links to its series glossary entry: a short explanation, then links to the slides that explain it in depth, in this series, the Local LLM Hosting and Key Publications decks, or the FHE Accelerator Simulators series.

01

Why Simulator Speed Matters, and a Taxonomy

A simulator's speed decides how many questions the architecture team can ask. At one minute per run, a 1,000-point design-space sweep is an overnight job; at one second it is a coffee break, and people start asking better questions. Speed is a feature, but only if the answers do not change.

FamilyTechniquesChanges answers?
Fewer eventsCoarser event granularity; macro-stepping; analytic sub-modelsExact if nothing observable happens inside the merged events
Cheaper eventsIncremental state, hoisted constants, lazy bookkeeping, fewer allocationsNo (pure refactoring)
Cheaper probesLower sampling rates, streaming statistics, windowed tracingOnly the detail of the outputs
Faster executionPyPy, Cython, C++ or Rust kernelsNo, if verified differentially
Parallelism across runsProcess pools, job arrays, CI farmsNo
Parallelism within a runConservative or optimistic PDESNo, but it is hard to get right
Smarter experimentsAnalytic bounds, bisection, Bayesian optimisation, surrogates, common random numbersFewer runs for the same conclusion
SamplingSimPoint-style representative regions, statistical sampling, checkpointsYes, with quantified error
02

Step Zero: Profile

Guessing where a simulator spends its time is usually wrong. cProfile on the baseline (Llama-3-70B, 2,000 requests, 511 simulated seconds, before the power model was added) showed:

FunctionOwn timeWhat it is
decode_step_done0.32 sPer-sequence token bookkeeping after every decode step
list.append (1.09 M calls)0.17 sRecording every inter-token latency
decode_once0.13 sRebuilding the context list each step
Cost model (decode, _time, properties)≈0.17 sRecomputing model constants and allocating a result object every step
SimPy kernel (step, _resume)≈0.11 sThe event loop itself
sampler0.06 sA passive probe
The lesson

The event kernel is a small fraction. Time goes into model code that runs per sequence per step. That points to two fixes: do less per step (incremental state), and stop touching every sequence on every step (lazy bookkeeping).

Tools: cProfile + snakeviz for call trees; py-spy for sampling without instrumentation overhead; perf and flame graphs for compiled cores.

03

Fewer Events: Choose the Abstraction

The biggest acceleration happens before any optimisation: choosing what an event is. Nothing observable to the scheduler happens inside a decode step, so one event per batch step is exact for serving metrics.

Event granularity, same 511 s of servingEventsRelative
One per batch step (the simulator)37,0991×
One per generated token505,89514×
One per layer per step2,967,92080×
One per clock cycle at 1.5 GHz≈7.7 × 1011≈2 × 107×

At batch-step granularity the plain SimPy model already runs at about 840× real time on a desktop i7-3770 (660–780× when first measured, on the same model before its 2026-10-03 correction; speed depends on how much the cost model computes per step and on the machine's load). The rule from deck 01 applies again: go finer only where the question needs it, for example per-layer events when studying layer-wise KV streaming.

04

Cheaper Events: Incremental State and Hoisting

Before: O(batch) per step, objects everywhere
ctx = [r.prompt_len + r.tokens_out
       for r in self.running]
cost = self.cost.decode(ctx)       # sums the list,
                                    # recomputes params,
                                    # allocates StepCost
for r in self.running:              # touches every sequence
    r.itls.append(now - r.last_token)
    r.tokens_out += 1
    r.last_token = now
After: O(1) per step
# once, outside the loop
fa, fb = 2*m.matmul_params, 4*m.n_layers*m.d_model
w  = m.weight_bytes_streamed   # layers + LM head
er = m.embedding_row_bytes     # 1 row per token
kv = m.kv_bytes_per_token

# per step: a running sum and arithmetic
flops  = fa*b + fb*(ctx + b)   # + itself
nbytes = w + b*er + (ctx + b)*kv
dt = max(flops/fr, nbytes/br) + ovh
ctx += b                            # every sequence grew by 1
step_end.append(t)                  # one record per instance
05

Exact Macro-Stepping

Between two state changes (an admission or a finish) a decode instance's future is deterministic: the batch is fixed and the context grows by one per step. So compute the whole run of steps in a tight loop and give SimPy one timeout for all of them. If new work arrives mid-way, interrupt: let the step in flight finish and admit the newcomer at that boundary, exactly as the baseline would.

baseline macro-step one timeout: steps 0..8 committed together planned, discarded (speculation) new request arrives: interrupt step in flight finishes at 414, admit, re-plan
sim.py, FastDecodeInstance.run (core)
k = min(steps_until_next_finish, horizon)          # bounded look-ahead
ends = plan(k)                                     # tight arithmetic loop
self._macro = True
try:
    yield env.timeout(ends[-1] - env.now); done = k
    horizon = min(4096, horizon * 2)                # uninterrupted: look further
except simpy.Interrupt:                          # submit() interrupted us
    j = bisect.bisect_right(ends, env.now)         # the step in flight
    yield env.timeout(ends[j] - env.now); done = j + 1
    horizon = max(8, 2 * done)                      # interrupted: look less far
self.commit(ends[:done], ...)
06

Measured: What Each Technique Bought

From examples/benchmark_acceleration.py on an i7-3770 desktop (4 cores, 8 threads; Python 3.12, SimPy 4.1), with the power model (deck 07) enabled, as recorded in examples/results.md. Every speed-up is checked for identical results. Rerun on 2026-10-03 after the cost model was corrected: steps no longer read the whole embedding table, and decode attention includes each token's attention to itself (Toolkit deck 10). The fast-path code on slide 04 shows the corrected terms.

TechniqueScenarioBeforeAfterGainAnswers
Event abstraction (step, not token)Defaults, 2,000 requests506k events37.1k events14× fewer eventsSame model
Fast path (incremental + lazy + macro)Defaults (1P1D, 4 req/s)0.74 s0.35 s2.1×Bit-identical
Fast path1k-token outputs, 1 req/s2.20 s0.87 s2.5×Bit-identical
Fast path2P2D, 6 req/s0.72 s0.36 s2.0×Bit-identical
Probe rate (5 ms → off)Defaults1.02 s0.55 s1.9×Same metrics, less timeline
Parallel sweep, 8 processes8 rates1.46 s0.90 s1.6×Identical
Analytic bracket + bisectionMax load at 90% SLO40 runs, 7.96 s7 runs, 1.23 s6.5×5.97 vs 5.75 req/s (grid step 0.25)
Reading the numbers honestly

Macro-stepping pays most when there are many steps per state change (long outputs, few arrivals per instance); with frequent arrivals it is cut short often. Adding the power model lowered the fast path's gain from 2.4–3.1× to 2.0–2.6× when it was added (2.0–2.5× in the current recording): the per-step DVFS and energy arithmetic is work macro-stepping cannot remove. Model fidelity costs simulator speed, and Amdahl's law (slide 14) says exactly how much. The parallel sweep is far below 8× because each run takes only about 0.2 s, so process start-up and data transfer dominate; with minute-long runs it approaches the core count. The largest gain came from doing fewer simulations, not faster ones.

07

The Cost of Watching: Probes and Traces

Sampling the system every 5 ms of simulated time nearly doubled run time (1.02 s against 0.55 s with sampling off). Probes are code, and code costs time.

08

Faster Execution: Compilers, Runtimes, Rewrites

OptionEffortTypical fitCaveat
PyPyNone (if dependencies allow)Pure-Python, generator-heavy SimPy modelsC-extension dependencies; measure, as gains vary
Cython / mypycLow to mediumHot model functions with typed loopsBuild step; the hot path must be type-annotated to gain much
NumbaLowNumerical kernels (cost models, vectorised analytics)Poor fit for generators and object-heavy event code
NumPy / JAX vectorisationMediumAnalytical layers evaluated over thousands of configurations at onceOnly for the closed-form parts
C++ core + pybind11HighThe event loop and models, with Python configurationTwo languages to maintain; the usual industry end state
Rust core + PyO3/maturinHighAs C++, with memory safety and easy packagingTeam skills; ecosystem smaller than C++'s

Whatever the route, keep the Python model as the reference and add a differential test in the style of test_javascript_port_matches_python: the compiled core must reproduce the reference exactly on a fixed set of workloads. That test is what makes a rewrite safe.

09

Parallel Runs

Sweeps, replications and searches are made of independent runs. Parallelising across runs is the simplest and most reliable speed-up there is.

search.py: a parallel sweep is one line of concurrency
def sweep(cfg, wl, rates, workers=1):
    jobs = [(cfg, wl, r) for r in rates]
    if workers <= 1:
        return [_attainment(j) for j in jobs]
    with ProcessPoolExecutor(max_workers=workers) as ex:      # processes, not threads: the GIL
        return list(ex.map(_attainment, jobs))
10

Smarter Experiments: Multi-Fidelity, Search, Surrogates

Multi-fidelity bracketing

A closed-form capacity bound costs microseconds. For the defaults it says prefill caps throughput at 7.49 req/s (decode 28.8, link 74.5). The simulator then only searches below that ceiling, because the bound is optimistic by construction.

Bisection, not grids

SLO attainment falls with load, so bisection on the 90% threshold converged in 7 simulations to 5.97 req/s. A 0.25-step grid needed 40 to say 5.75. Add a tolerance for the noise in tail metrics, or bisect on the mean of a few replications.

Bayesian optimisation

For many parameters (prefill:decode ratio, TP degree, batch caps, link), fit a Gaussian-process surrogate to completed runs and choose the next run to maximise expected improvement in goodput per GPU. It typically needs far fewer runs than grids, and libraries such as Optuna and Ax make it routine.

Learned surrogates and early stopping

Vidur learns operator runtimes so the simulator need not profile every shape. Going further, a regressor trained on simulator outputs can screen thousands of configurations before the simulator confirms the best few. Early stopping aborts a run once its SLO verdict is statistically certain.

And reuse randomness

Common random numbers (deck 06) cut the replications needed to compare two designs, often a bigger saving than any code optimisation.

11

Sampling, Checkpoints and Mode Switching

Detailed simulators (cycle-level, or serving simulators with very fine models) cannot afford to simulate everything. Computer architecture solved this two decades ago.

For LLM serving the natural units to sample are traffic regimes (quiet, typical, burst), each simulated in detail and weighted by how often it occurs in production traces.

12

Parallel Discrete-Event Simulation

When one run is too big for one core, split the model into logical processes (LPs) that exchange timestamped messages.

Conservative (Chandy–Misra–Bryant)

An LP processes an event only when it is certain no earlier message can arrive. That needs lookahead: a minimum delay on every link between LPs. Physical link latency is natural lookahead; null messages prevent deadlock. SST and parallel gem5 work this way.

Optimistic (Time Warp)

LPs run ahead speculatively and roll back when a straggler message arrives in their past, sending anti-messages to cancel what they sent. More parallelism, but state saving and rollback cost memory and time: the macro-stepping trade-off writ large.

Why this simulator does not need it

Each event is tiny and the pools are tightly coupled through the routers, so synchronisation would cost more than it saves. PDES pays off for large models with many loosely coupled components and real lookahead: a datacentre of accelerators, a large NoC, a full SoC with many cores. Parallelism across runs comes first.

13

Accelerating the RTL End

The same ideas apply when the simulator under pressure is RTL, which is where pre-tape-out verification spends its compute.

TechniqueWhat it does
VerilatorCompiles synthesisable SystemVerilog to optimised, optionally multi-threaded C++: a cycle-based (not event-driven) model, often much faster than interpretive event-driven simulators on large designs
Smaller RTL islandsCo-simulate only the block under test in RTL, with the rest of the system in TLM or the Python model (deck 01)
EmulationPalladium, Veloce, ZeBu: around 1 MHz, enough to run real software and long workloads
FPGA prototypingTens of MHz; the fastest pre-silicon platform, at the cost of debug visibility and partitioning effort
Hybrid fast-forwardBoot and warm up in a virtual platform, checkpoint, then transfer state to emulation or RTL for the window of interest
Regression farm disciplinePrioritise tests by coverage gained per CPU hour; run the expensive tiers nightly
14

Interactive: Amdahl's Law for Simulators

Every technique speeds up only the fraction of time it touches. Set the profile shares (they are normalised) and the speed-up of each fix, and see the end-to-end gain. The defaults approximate the profile on slide 02 and the measured gains.

45
6
15
3
20
5
20
8
30
Single-run speed-up
—
Upper limit (others → ∞)
—
Sweep speed-up vs serial baseline
—

The sweep figure models a serial overhead per run (dispatch, pickling, result collection) that parallelism cannot hide: speed-up = single ÷ (1/workers + overhead). With the defaults it comes out well below the worker count, as measured on slide 06.

15

What to Take Away

Next

Deck 09 connects the simulator to the outside world: PyTorch, ONNX Runtime and HEIR.