> ## Content Index
> Fetch the complete content index at: https://globaltrendanalysis.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Intro to Architectural Modeling
- URL: https://globaltrendanalysis.com/intro-to-architectural-modeling/
- Published: 2026-09-11T17:49:56.000Z
- Updated: 2026-09-11T17:49:56.000Z
- Author: Aaron Lucas
- Tags: Architectural Modeling

Designing almost anything follows the same shape: an idea, then a model of that idea, then production. The clothing industry works this way too — design, model, test, ship. Electronic design follows the same path with a very different toolbox: we model what we want to build using all kinds of different tools, and at the level of high-level system architecture, that modeling can get abstract fast.

In many cases, what you're actually studying is how each part interacts with the rest. For an SSD, that might be how the NAND interacts with the controller. Not everyone does this the same way, and in this piece we'll look at two structurally different methods: a static (capacity) model tells you the ceiling — the maximum this system can ever do. An event-based model tells you what happens on the way to that ceiling, and what happens when something you didn't account for shows up. Neither replaces the other, and knowing which one you're reaching for — and why the other one can't answer the question you actually have — is the actual skill.

That NAND/controller relationship is one instance of a more general pattern: any system built from stages with finite capacity — a network switch, a web service behind a load balancer, a manufacturing line — can be modeled the same two ways. In an earlier notebook entry on [SSD product design](https://globaltrendanalysis.com/studies/), we modeled the drive as a chain of capacity-constrained stages and took the minimum — a static, closed-form model that answers "which stage is the limit, and how fast can this system go?" That model is fast to build, cheap to explore, and answers a real question. It also has a blind spot baked into its structure: it has no clock. It cannot tell you what happens to latency as load approaches that ceiling, what a queue does when two things compete for the same resource, or how the system behaves in the first few milliseconds before it reaches whatever steady state the formula assumed. Answering those requires simulating the system as a sequence of events unfolding over time — which is where discrete-event simulation, and a specific Python tool for building it, come in.

> A static model tells you the ceiling. An event-based model tells you what the room looks like as you approach it.

01

#### Static / capacity modeling

The system as a chain of capacities. Closed-form, instant to evaluate, and blind to time.

02

#### Event-based modeling

The system as a sequence of arrivals and completions. Slower to build, and the only way to see queueing, contention, and transients.

Sections

---

## 01 — Static modelingThe system as a chain of capacities

Every stage has a maximum rate. The system's rate is the smallest one. No clock required.

A static or capacity model treats a system as a set of stages, each with an independently computable maximum throughput, and takes the minimum across them — the same logic as a minimum-cut in a network-flow problem. Data (or work, or parts, or requests) has to pass through every stage, so the achievable end-to-end rate is bounded by the narrowest one:

System\_Throughput(workload) = min( Stage\_1, Stage\_2, ..., Stage\_N )

For an SSD, the stages are the PCIe link, the controller's compute budget, DRAM bandwidth for the mapping table, the ECC engine, the NAND channel bus, and the NAND array itself — each with a closed-form formula in terms of lane counts, clock rates, and per-operation timings. The full breakdown, with a formula and a "what makes it the limit" column for each stage, is in the [SSD product design notebook entry](https://globaltrendanalysis.com/studies/); the shape generalizes past storage. A network switch is a chain of ingress bandwidth, switching-fabric bandwidth, and egress bandwidth. A web service is a chain of load-balancer throughput, application-server request-handling rate, and database query capacity. Any pipeline with distinguishable stages fits the same template.

### What it's good for

Speed and leverage. A static model is algebra — you can evaluate it in a spreadsheet, sweep a parameter in seconds, and get an immediate answer to "if we double the channel count, does the bottleneck move?" It forces you to be explicit about units (bandwidth vs. operations-per-second, converted through a block size or a request size) and about which stage binds under which workload — which is often the more valuable output than the number itself. It is the right first move on any new architecture, because it is nearly free and it tells you where to look next.

### What it structurally cannot tell you

A static model has no concept of a queue, because it has no concept of time. It answers "what is the sustained rate," not "how long does any individual request wait." Three specific things fall out of that gap:

- **Behavior near saturation.** The model says a system below its ceiling is fine and a system above it is impossible — a step function. Real systems queue as they approach the ceiling, and latency there is not a step; it is a curve that bends upward well before the ceiling and diverges near it. The static model has nothing to say about the shape of that curve.
- **Contention between things that aren't the modeled workload.** A background process quietly using the same resource (garbage collection folding into NAND, a batch job sharing a database connection pool) doesn't change the static ceiling — the stage's rated capacity is the same number either way — but it very much changes what a foreground request experiences while sharing that resource.
- **Transients.** Warm-up behavior, bursts, and correlated arrivals (a client that issues ten requests back to back, then goes quiet) aren't represented at all. A static model implicitly assumes steady state and an averaged arrival process; anything that varies over time is invisible to it by construction.

None of this is a flaw in the static model — it's doing exactly what it's for. But when the actual question is "how bad does the tail get at 90% load" or "what does GC traffic cost my foreground reads," a formula with no clock cannot answer it. That's a different kind of model.

---

## 02 — Event-based modelingThe system as a sequence of events

Instead of solving for a steady state, simulate the arrivals and completions that produce one — and see what happens before it arrives.

Discrete-event simulation (DES) models a system as a sequence of discrete events — a request arriving, a resource becoming free, an operation finishing — and advances a simulated clock from event to event rather than in fixed time steps. Between events, nothing happens and nothing is computed; the simulation jumps straight to the next thing that changes state. That's the key structural difference from a static model: a DES has entities with state, a clock, and a scheduler, and the aggregate numbers you actually care about (mean latency, p99, achieved throughput) emerge from running the simulation and measuring what happened, rather than being solved for in closed form.

This is worth reaching for specifically when the question is about *dynamics* rather than *capacity*:

- **Queueing behavior as utilization climbs.** Even a simple queue — one arrival stream, one pool of identical servers — has no simple closed form once service times aren't exponential, and real systems have several interacting queues. Simulating it is often easier than solving it.
- **Resource contention between distinct workloads.** Two kinds of work sharing one pool of servers (host reads and background GC, foreground queries and a batch export) is trivial to represent as two arrival processes competing for one resource — and hard to represent in a formula.
- **Transients and warm-up.** A simulation naturally has a "before steady state" period, which is often the actual regime of interest — a burst, a cold cache, a cascading retry storm.
- **Policy comparisons.** Priority scheduling, admission control, different queue disciplines — these are a few lines of simulation logic and a genuinely hard analytical problem.

The cost is real: a DES takes longer to build than a formula, needs a source of randomness and therefore needs enough simulated time (and often enough random seeds) to trust the result, and produces a distribution of outcomes rather than a single number — which is arguably the more honest output, but it is more work to interpret. The right process, in practice, is to build the static model first because it's nearly free and tells you where the bottleneck is, then build a focused event-based model of *that specific stage* once you need to know how it behaves under real, time-varying load. You rarely need to simulate the whole system in detail — just the part the static model flagged.

A structural note

Static and event-based models aren't two ways of answering the same question — they're answers to different questions that happen to be about the same system. "What's the maximum?" and "what does the queue do at 90% of the maximum?" are both legitimate, and neither model can answer the other's question. Treating a static model's silence on latency as good news, or building a full-system simulation to answer a question algebra already answers, are both the same mistake in opposite directions.

---

## 03 — SimPyA discrete-event framework built on generators

A clock, a scheduler, and processes that pause themselves. That's most of what a DES needs, and it's what SimPy provides.

[SimPy](https://simpy.readthedocs.io/en/latest/?ref=globaltrendanalysis.com) is a process-based discrete-event simulation framework in plain Python. "Process-based" is the important word: instead of hand-managing an event queue, you write each active entity in the system — a request, a customer, a machine — as an ordinary Python generator function, and SimPy drives it forward by resuming it each time the thing it's waiting for happens. The mental model has four pieces:

| Concept              | What it is                                               | Typical use                                                                                                          |
| -------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Environment          | The simulation clock and event scheduler                 | One per simulation run; env.run(until=...) drives it forward                                                         |
| A process            | A generator function representing one active entity      | Started with env.process(fn(...)); runs concurrently with every other process                                        |
| yield env.timeout(d) | "Pause this process for duration d"                      | Modeling a service time, a network delay, an inter-arrival gap                                                       |
| Resource             | A limited-capacity thing with a built-in FIFO wait queue | A pool of servers, dies, channels, or connections; request()/release() (or a with block) is the queueing step itself |

A process yields an event and control returns to SimPy's scheduler, which resumes whichever process's event fires next — there is no manual bookkeeping of "what happens after this." Requesting a `Resource` that's already at capacity is exactly how queueing gets represented: the requesting process simply doesn't resume until an earlier holder releases it, and the wait is whatever real time elapsed in between. SimPy also ships `PriorityResource` and `PreemptiveResource` for scheduling disciplines, and `Store`/`Container` for modeling queues of discrete objects or bulk quantities — useful if you need priority, but the plain `Resource` above is enough to answer the two questions this section builds toward.

One explicit limitation, from SimPy's own documentation: it's built for discrete, asynchronous events, not fixed-timestep continuous simulation — it will feel like overkill (or the wrong tool) for something that's naturally a numerical integration over a fixed `dt`. "A request arrives, a die frees up, an operation finishes" is exactly the shape it's built for.

### The experiment

Take the same die pool from the SSD tail-latency work — a fixed number of independently-operable NAND dies, each able to service one read at a time — and instead of the closed-form binomial contention model, simulate it directly: a pool of `N_DIES` dies as a single `simpy.Resource`, host reads arriving as a Poisson process, each read requesting a die, holding it for a fixed service time, and releasing it. Two questions, two sweeps of the same model:

def command(env, dies, service_time_us, wait_times, warmup_end_us):
    arrive_at = env.now
    with dies.request() as req:
        yield req                          # queues here if every die is busy
        wait = env.now - arrive_at
        if arrive_at >= warmup_end_us:
            wait_times.append(wait)
        yield env.timeout(service_time_us)  # hold the die for the operation

def arrival_process(env, dies, rate_per_us, service_time_us, wait_times,
                     warmup_end_us, rng):
    while True:
        yield env.timeout(rng.expovariate(rate_per_us))
        env.process(command(env, dies, service_time_us, wait_times, warmup_end_us))

That's the entire model. `run_utilization_sweep()` in the companion script (`architectural_modeling_simpy_example.py`) runs this at offered loads from 5% to 99% of the pool's static capacity (`N_DIES / T_READ` — exactly the same formula section 01 would use) and records queueing delay. `run_interference_sweep()` reuses the identical `command`/`arrival_process` pair, but adds a second Poisson stream — background erase operations, far longer service time — requesting the *same* die pool. `simpy.Resource` is FIFO by request order regardless of which process made the request, so an ordinary read simply queues behind an ordinary erase exactly as it would behind another read; no special-casing was needed to represent that.

### What the static model says, and what the simulation adds

For a 16-die pool with a 50 µs read time, the static model gives exactly one number: a ceiling of 320,000 IOPS. It's correct, and it's also the entire output — the model has nothing else to say about a workload running at 70% of that ceiling versus 97% of it.

![Log-scale chart of read latency versus utilization for a 16-die pool, showing mean and p99 latency flat near the static per-operation time at low utilization and rising sharply as utilization approaches the static capacity ceiling.](__PLOT1__)

**Figure 1 — Latency vs. utilization, 16-die pool, simulated.** Flat at the fast-path 50 µs floor out to roughly 50% utilization, then bending upward, with p99 diverging well before mean latency does. The static model predicts the vertical dashed line at utilization = 1.0 and nothing about the curve leading up to it.

| Utilization | Mean latency | p50 latency | p99 latency |
| ----------- | ------------ | ----------- | ----------- |
| 10%         | 50.0 µs      | 50.0 µs     | 50.0 µs     |
| 70%         | 50.8 µs      | 50.0 µs     | 65.2 µs     |
| 90%         | 59.5 µs      | 52.5 µs     | 111.7 µs    |
| 95%         | 73.5 µs      | 64.2 µs     | 177.1 µs    |
| 99%         | 165.2 µs     | 125.3 µs    | 540.6 µs    |

At 70% utilization the picture still looks like the static model's floor — mean latency barely off the fast path. By 99% the mean has more than tripled and p99 is over 10× the fast-path number, entirely from queueing, with no change to any per-operation timing. That curve — where it starts to bend, how steep it gets — is not a detail; it's the actual answer to "how much headroom do we have," and a static model cannot produce it at any resolution.

### Interference: the ceiling didn't move, but the experience did

The second sweep holds host reads fixed at 70% of static capacity — the same 70% row above — and adds background erase traffic sharing the same die pool, increasing only how much of the pool that background traffic holds on average.

![Log-scale chart of host read latency versus background erase load at fixed 70 percent host utilization, showing mean and p99 read latency both rising sharply as background load increases even though host read IOPS is unchanged.](__PLOT2__)

**Figure 2 — Read latency vs. background load, host reads fixed at 70% of static capacity.** Host IOPS is identical in every point on this chart. Only the background load changes, and read latency rises by more than an order of magnitude at the tail.

| Background load | Mean read latency | p99 read latency |
| --------------- | ----------------- | ---------------- |
| 0%              | 50.8 µs           | 65.5 µs          |
| 5%              | 51.8 µs           | 75.2 µs          |
| 10%             | 56.4 µs           | 171.0 µs         |
| 15%             | 77.9 µs           | 486.7 µs         |
| 20%             | 1,091.4 µs (p99)  | —                |

The static capacity number for this pool is 320,000 IOPS at every single row of that table — it has no term for background load, because background load isn't part of what it models. But read tail latency at 20% background load is nearly 17× what it was at 0%, with host read throughput held perfectly constant throughout. This is the write-suspend and GC-folding effect from the tail-latency notebook entry, arrived at from a completely different direction: not a probability ladder, but a shared, simulated resource two independent processes are actually fighting over.

---

## ClosingTwo models, one loop

Build the static model first. It's algebra, it's nearly free to explore, and it answers the question "which stage sets the ceiling, and how does that ceiling move if I change a parameter." Then, once you know which stage that is, build a focused event-based model of it — not the whole system — when the question changes to "what does this actually feel like as load climbs toward that ceiling" or "what does it cost me when something else needs the same resource." SimPy is a reasonable default for that second model: the process/event/resource vocabulary maps directly onto commands, dies, channels, and queues, and a few dozen lines is usually enough to get a real answer.

> The static model finds the wall. The event-based model tells you what it's like to stand next to it.

### Notes on the numbers

All latency and utilization figures in section 03 are computed by the companion script (`architectural_modeling_simpy_example.py`), a runnable SimPy model rather than a hand-drawn illustration — the die count, service times, arrival rates, and background load are all editable parameters, and the tables and figures regenerate together from the same runs. The 16-die pool, 50 µs read time, and 3,000 µs background-erase time are illustrative order-of-magnitude values chosen to sit on the same scale as the tail-latency notebook entry's die-contention numbers, not a specific drive's datasheet. Each sweep discards the first 10% of simulated time as warm-up and uses a fixed random seed per experiment for reproducibility — rerunning with a different seed will move the exact figures slightly without changing the shape of either curve.