This document is relevant for: Inf1, Inf2, Trn1, Trn2, Trn3
Deep dive: Plan inference parallelism with the Parallelism Planner skill#
Why read this guide? This guide is for ML engineers who need to decide how to shard a
transformer LLM for serving on AWS Trainium. It covers how many NeuronCores to use, which tensor,
context, and data parallelism degrees to pick, and what batch size to run at. It explains the
Parallelism Planner skill. The skill is an agent-driven workflow that turns a model’s
HuggingFace config.json and a set of hardware constants into a ranked parallelism
recommendation, using an analytic hardware-entitlement model. You get the recommendation
before you compile anything or reserve a Trainium instance.
How to use this guide: Read this guide before your first on-hardware sweep for a new model, or when a deployed config misses its latency or throughput target and you want to know whether a different sharding could meet it. Skip to the workflow if you already understand run-specs and serving objectives. For per-operator analysis of one chosen config, see the Hardware Entitlement skill.
This topic explores the Parallelism Planner skill in depth. It covers the run-spec input, the three-layer analytic engine, oracle validation, the serving objectives and SLA gates, the parallelism axes the planner sweeps, and how to read its output. You need experience with transformer inference, tensor parallelism, and basic roofline analysis to fully understand this content.
Prerequisites#
Before you start, you must be familiar with the following:
Transformer inference phases: The difference between prefill (processing the prompt) and decode (generating one token per step), and what time-to-first-token (TTFT) and inter-token latency (ITL) measure.
Parallelism concepts: How tensor, context, and data parallelism split work across NeuronCores. See Parallelism Techniques for LLM Inference.
Roofline analysis: The difference between compute-bound and memory-bandwidth-bound work.
Overview#
Picking a parallelism config by trial and error on hardware is slow. Each candidate needs a compile, a warm-up, and a benchmark. The Parallelism Planner skill narrows the search before you touch hardware. It computes a theoretical lower bound, called the entitlement, on decode ITL, prefill TTFT, throughput, and per-device HBM for every candidate config. It then selects the config that best meets your serving objective.
The planner is an entitlement model, not a measurement. Real latencies are higher because of dispatch overhead, synchronization, idle gaps, and kernel inefficiency. Use the planner’s recommendation to decide where to point the first on-hardware sweep, then validate the prediction with the profiling skills.
The skill handles:
Dense and Mixture of Experts (MoE) models: Including always-on shared experts and dense-then-MoE layer splits.
Attention variants: Standard multi-head attention (MHA), grouped-query attention (GQA), Multi-head Latent Attention (MLA), and mixed sliding-window and global attention.
Precision options: BF16 or FP8 weights, FP8 KV cache, and FP8 compute.
Trainium generations: trn1, trn2, and trn3. Each generation is described by a hardware-constants file.
Linear-attention, recurrent, and state-space layers are not supported. See Supported architectures for the full list.
The workflow has four stages:
Run-spec authoring: Describe the model (pristine HF
config.json), the hardware constants, and your SLAs in a run-spec JSON file.Oracle validation: Confirm the analytic engine’s FLOP and weight counts for this model match a shape-exact meta-device oracle.
Sweep and select: Evaluate every config in the parallelism × context-bucket grid at its maximum feasible batch size, gate on the SLAs, and rank the survivors.
Cell inspection: Break a single operating point down per component and layer type to see what binds, and hand off to profiling for validation.
Important
The planner models single-node, colocated serving only. One replica serves both
prefill and decode, and every config must fit inside one node
(TP × DP ≤ cores_per_node). Disaggregated prefill and decode, split pools, and any
multi-node or multi-instance topology are out of scope. The engine raises an error if you
request them. If no single colocated config meets your SLAs, the planner reports the
best achievable config and the gap instead of proposing a different topology.
Supported architectures#
The planner models decoder-only, causal transformer LLMs with softmax attention. The
architecture is read from the HF config.json by the arch_from_hf parser. The
following table lists what the parser and oracle model today.
Area |
Supported |
Config fields read |
|---|---|---|
Attention |
Multi-head (MHA), grouped-query (GQA), and multi-query (MQA) softmax attention. |
|
Latent attention |
Multi-head Latent Attention (MLA) with a compressed KV cache, as in DeepSeek-V2/V3. |
|
Sliding-window attention |
Uniform sliding window, and mixed sliding-window and full-context layers with separate KV-head count and head size for the global layers, as in Gemma. |
|
K/V aliasing |
Global layers where V aliases K. |
|
Dense FFN |
Gated MLP (gate, up, down projections). |
|
Mixture of Experts |
Routed experts with top-k routing, always-on shared experts, dense-then-MoE layer splits, and a routed pool in parallel with a dense MLP. |
|
Embeddings |
Tied or untied embedding and lm_head tables. |
|
Config layout |
Flat configs, and multimodal configs with a nested |
|
Models the planner has been exercised on include Granite-4.1-8B (dense GQA, tied
embeddings), Gemma-4-31B (mixed sliding and global attention, attention_k_eq_v),
Gemma-4-26B-A4B (MoE in parallel with a dense MLP), and Devstral-2-123B (untied embeddings,
FP8 weights).
Not supported#
Architecture |
Status |
|---|---|
Linear attention and recurrent layers, including Gated DeltaNet (GDN) and other hybrid linear/full-attention models |
Not modeled. The engine has no representation for recurrent state or the non-tensor- engine work these layers do. |
State-space layers (for example, Mamba) |
Not modeled. |
Encoder-only and encoder-decoder models |
Not modeled. The engine assumes causal self-attention with a KV cache, and has no cross-attention. |
Vision and audio towers in multimodal models |
Ignored. Only the text decoder in |
Per-layer FFN widths other than the dense-then-MoE split |
Not modeled. |
MoE with |
Raises |
MoE configs that use other field names (for example, |
Not recognized. The model is parsed as dense. |
Operators that exist only in a kernel language |
Must be expressed in the oracle by hand before validation. See
|
Warning
The parser does not reject unsupported architectures. It mis-models them without an
error. In layer_types, it recognizes only sliding_attention and
full_attention. Depending on the other layer types present, it then either treats
unrecognized layers, such as linear_attention, as full softmax attention, or drops
them from the layer count. An unrecognized MoE config is planned as a dense model.
Before you trust any plan for a new model, confirm that every value in layer_types
is sliding_attention or full_attention, that the parsed layer count equals
num_hidden_layers, and that the corrected weight total matches the model card. Do
not plan only the layers that happen to parse. To support a new architecture, extend
the engine and oracle first.
Hardware and software requirements#
The planner does not need Neuron hardware. It runs on any machine, including a laptop or a CPU-only instance.
Python: 3.12 (the skill was developed and tested with
python3.12).Planner CLI: Python standard library only.
Oracle validation:
torch(CPU build is enough; the oracle runs on themetadevice) andpytestfor the tests.Figures (optional):
matplotlibfor the--visualizeoutput.Model config: The model’s unmodified HuggingFace
config.json. Model weights are not needed.
Note
Hardware is needed only when you validate the planner’s prediction. Use the
neuron-framework-profiling-vllm-neuron and
neuron-framework-profile-analysis-vllm-neuron skills on a Trainium instance for that
step.
Inputs: the run-spec#
Everything the planner computes is driven by a run-spec, a JSON file that points at the model config and the hardware constants and states your serving targets. The skill ships no default model. If you omit the run-spec, the planner stops with an error rather than silently analyzing some other model.
A run-spec resolves hf_config and hardware relative to its own location, so keep the
three files in one directory.
{
"model": "granite-4.1-8B (dense)",
"hardware_name": "trn2",
"hf_config": "granite4-8b-hf.json",
"hardware": "trn2_constants.json",
"serving": "colocated",
"decode_itl_ceiling_ms": 50,
"prefill_itps_floor": 30000
}
Model, hardware, and SLA fields#
Field |
Default |
Description |
|---|---|---|
|
(required) |
Display name printed in every report. |
|
(required) |
Short hardware label, for example |
|
(required) |
Path to the model’s pristine HuggingFace |
|
(required) |
Path to a hardware-constants JSON file. The skill ships trn2 constants at
|
|
|
Serving mode. Only |
|
50 |
Maximum acceptable decode inter-token latency, in milliseconds. |
|
30000 |
Prefill SLA expressed as an input-tokens-per-second floor. The TTFT ceiling scales
with context: |
|
(unset) |
Alternative prefill SLA: a flat TTFT ceiling in milliseconds at every context.
Mutually exclusive with |
Physical serving fields#
These fields change the system being modeled, not what the planner optimizes for. All are optional.
Field |
Default |
Description |
|---|---|---|
|
model dtype |
Served weight element size ( |
|
|
Element size for a separate, untied lm_head. A tied model rejects a conflicting value. |
|
weight dtype |
KV-cache element size, for example |
|
|
|
|
|
Apply DCP attention sharding on the prefill pass. |
|
|
How activation HBM traffic is charged. See The component cost model. |
|
0.90 |
Fraction of per-core HBM available for weights, KV cache, and activations. Lower it to model a tighter memory budget. |
Note
When you model an FP8 deployment, set weight_dtype_bytes: 1 and
compute_precision: "fp8" together. Halving weight bytes without modeling the FLOP
speedup understates FP8 prefill by about 2×. Both factors are applied inside the engine.
Do not scale the planner’s output by hand.
Hardware constants#
The hardware-constants file supplies the numbers every planner result divides by. The shipped trn2 file describes one LNC2 logical NeuronCore:
Constant |
trn2 value |
Meaning |
|---|---|---|
|
158 TFLOP/s |
BF16 tensor-engine peak per logical core. |
|
716 GB/s |
HBM bandwidth per logical core. |
|
24 GiB |
HBM capacity per logical core (96 GiB device ÷ 4 logical cores). |
|
64 |
Logical cores per trn2.48xlarge at LNC2. This is the single-node cap. |
|
1.28 TB/s |
Link bandwidth used by the analytic collective model. |
A stale or wrong constant produces a silently wrong plan. If a result looks implausible,
calibrate the constants by microbenchmarking. See the Hardware Entitlement skill’s
references/microbench-calibration.md.
Key concepts#
Entitlement and the roofline#
For each piece of work, the planner computes two lower bounds and takes the larger:
compute time = FLOPs / tensor-engine peak
load time = (weight bytes + KV bytes + activation bytes) / HBM bandwidth
floor = max(compute time, load time)
The ratio tensor-engine peak / HBM bandwidth is the ridge point, about 221 FLOP/byte on
trn2. Work whose arithmetic intensity (FLOPs per byte loaded) is below the ridge point is
memory-bound. Work above it is compute-bound. Small-batch decode sits far below the ridge
point. Whole-prompt prefill sits far above it.
The component cost model#
The planner does not apply a single roofline to the whole model, and it does not sum per-operator latencies. It applies the roofline to each component: an attention body, an FFN body, or the terminal lm_head. It then adds the component floors in sequence and adds the collective time:
component_floor(c) = max(TE_work(c) / TE_peak, HBM_bytes(c) / HBM_bandwidth)
step_time = sum(component_floor(c)) + collective_time
Components are serialized because in a tensor-parallel transformer graph each attention and
FFN body ends at an all-reduce or reduce-scatter whose result the next body consumes. Inside
a component, the model assumes ideal fusion: internal operator interfaces stay on chip, and
only one gathered input read and one pre-collective output write are charged as activation
HBM traffic. This is the default component_boundary policy. Setting
activation_io_policy to per_operator instead charges every operator’s input and output,
which models an unfused or eager execution.
Collective time is modeled separately from the TE/HBM floor using a topology-aware ring model (TP all-reduce, CP K/V ring). It is added after the component floors, not overlapped with them.
KV cache replication#
This is the most error-prone part of planning tensor parallelism for a GQA or MQA model:
The TP ceiling is the query-head count, not the KV-head count. TP shards query heads, one per rank at most.
The KV cache does not shard past the KV-head count. It replicates. Per-device KV bytes divide by
min(TP, n_kv_heads)for each attention group. Past that point, extra ranks hold redundant copies of the cache.
For example, a model with 32 query heads and 8 KV heads can run at TP=32, but at TP=16 every KV head is stored twice, and at TP=32 four times. That replication makes high-TP decode go KV-read-bound early and wastes HBM that could hold more sequences or a prefix cache.
Context parallelism is the planner’s remedy. A CP group is carved out of the TP group, and
during decode it splits the sequence dimension of the replicated KV cache across those
otherwise-redundant ranks. CP therefore recovers exactly the replication factor and no more:
the effective CP for a group is min(CP, TP / n_kv_heads). See Supported parallelism.
Oracle validation#
The analytic engine is cross-checked against a meta-device oracle: a hand-written,
shape-exact torch module built from the same parsed architecture and run on the meta
device under a FLOP counter. Because it runs on meta, it needs no accelerator and no
weights.
Quantity |
Authority |
|---|---|
Projection, FFN, router, and lm_head FLOPs |
Oracle (exact). |
Weight bytes |
Oracle. The analytic |
KV-cache bytes |
Analytic. The oracle reports zero because SDPA decomposes on |
Prefill SDPA FLOPs |
Analytic causal active-pair count. The oracle’s raw dense count is reported separately and is expected to be higher. |
Warning
The oracle validates that the engine and the oracle agree, not that the architecture
was parsed correctly. Both are built from the same parse, so a mis-parsed model can still
read +0.0%. Sanity-check the corrected weight total against the model card’s parameter
count, and confirm the layer count matches num_hidden_layers.
Serving objectives#
Selection is gate-then-rank. SLA ceilings decide which configs are valid, and a throughput metric ranks the survivors.
Objective |
Behavior |
|---|---|
Maximize OTPS under SLAs (default) |
Keep configs that meet both the prefill TTFT ceiling and the decode ITL ceiling, then pick the highest decode output-tokens-per-second (OTPS) per node. |
Minimize prefill TTFT |
Pick the lowest TTFT, ignoring decode throughput. Favors configs with more cores, because prefill parallelizes almost perfectly. |
Maximize decode OTPS |
Pick the highest per-node OTPS without the ITL ceiling. Favors large-batch configs. |
Per-node throughput is otps × cores_per_node / cores. This assumes the node is filled
with independent data-parallel replicas of the chosen config, so a small replica that holds
the weights scores well even though each replica uses few cores.
Throughput-capacity versus interactivity#
The sweep evaluates every config at its maximum feasible batch, the largest batch whose peak HBM fits the usable budget. Those cells answer the throughput-capacity question: how much throughput can this footprint hold? They favor low TP with DP scale-out.
Low-ITL operating points live below maximum batch and favor higher TP. The
--visualize per-node Pareto figure sweeps batch size below the maximum for each config
and draws the ITL versus per-node-throughput frontier. Use that figure, not the max-batch
table alone, when interactivity matters.
Supported parallelism#
The planner sweeps TP, CP, DP, and EP on a single node. PP is not supported. A
config’s core count is world = TP × DP. CP is carved out of TP and adds no cores.
Axis |
Status |
Behavior and restrictions |
|---|---|---|
TP |
Supported, swept |
|
CP |
Supported, swept |
|
DP |
Supported |
|
EP |
Supported, MoE only |
|
PP |
Not supported |
|
Other topology restrictions:
Single node only.
TP × DPcan never exceedcores_per_node, which is 64 logical cores on trn2.48xlarge at LNC2. Multi-node and multi-instance configs are never produced.Colocated only. One replica serves both prefill and decode. Disaggregated prefill and decode is not modeled.
Weights must fit. A config whose per-device weights alone exceed usable HBM is dropped from the grid.
Speculative decoding is not a parallelism axis. It is modeled separately; see Tools reference.
The candidate degrees (TP_CHOICES, CP_CHOICES, DP_CHOICES), the number of context
buckets (N_BUCKETS), and batch granularity are constants in scripts/planner/plan.py.
Edit them only to retune the search. To exclude an axis for one run, use the
--disable-tp, --disable-cp, --disable-dp, or --disable-ep flags, which pin that
axis to 1 (EP: off).
Workflow#
The examples below run from your project directory with the skill’s scripts/ directory on
PYTHONPATH. Do not cd into the skill. The scripts refuse to write artifacts inside the
skill package.
# ~/.claude for Claude Code, ~/.kiro for Kiro
export NEURON_AGENTIC_DEVELOPMENT_ROOT=~/.claude
SKILL=$NEURON_AGENTIC_DEVELOPMENT_ROOT/skills/neuron-parallelism-planner/scripts
Stage 1: Write the run-spec#
Create a directory containing the model’s pristine config.json, a copy of the hardware
constants, and a run-spec that points at both. The skill’s
samples/granite4-8b-walkthrough.md contains a complete, known-good example you can copy.
Before trusting results for a new model, check the parse:
Confirm the config’s field names are ones
arch_from_hfrecognizes. For example, MoE configs that usemoe_num_expertsinstead ofn_routed_expertshave previously been modeled as dense.Confirm every
layer_typesvalue issliding_attentionorfull_attention, and that the parsed layer count equalsnum_hidden_layers. Other values, such aslinear_attention, are silently mis-modeled or dropped.If the architecture is not in Supported architectures (for example, a new attention variant or recurrent linear attention), extend the engine and oracle first. See
references/oracle-construction.md.
Stage 2: Validate the engine against the oracle#
python3.12 $SKILL/validate_oracle.py ./my-model/my-model-run.json
The script prints, per segment, the oracle and analytic FLOPs and weight bytes for a decode cell and a prefill cell.
Pass criteria:
Weight bytes match to well under 1% on every segment.
Decode FLOPs match within a few percent.
Prefill attention FLOPs may differ. The oracle counts the dense score rectangle, while the engine charges causal active pairs. Projection, FFN, and lm_head prefill FLOPs should still match.
A large gap localizes a missing term to the segment where it appears. Fix it before sweeping.
Stage 3: Sweep and select#
PYTHONPATH=$SKILL python3.12 -m planner ./my-model/my-model-run.json
The planner:
Builds the config grid, rejecting configs that do not shard evenly, whose per-rank weights exceed usable HBM, or whose
TP × DPexceedscores_per_node.Derives the context buckets from the model’s
max_position_embeddings. It keeps six buckets, pinning the full context as the last one.Solves each cell’s maximum feasible batch in closed form. Peak HBM is affine in batch size, and the result is rounded down to an even number.
Costs every cell for decode and prefill, gates on the SLAs, and selects a config per bucket.
For each bucket, the output prints the selected config and the full decode landscape:
--- 8k ---
SELECTED TP8 cores=8 TTFT=0.22s ITL=35ms sla_met=True tput/node=28089
TP4 cores=4 bs=56 ITL= 33.5ms tput/node= 26768 bind=attention:kv_load
TP8 cores=8 bs=124 ITL= 35.3ms tput/node= 28089 bind=attention:kv_load
TP16 cores=16 bs=130 ITL= 37.4ms tput/node= 13895 bind=attention:kv_load
...
bind names the component and term that dominates the step: kv_load, weight_load,
activation_load, matmul, or comm. When no config meets both SLAs, the planner falls
back to the lowest-TTFT config among those that meet the ITL ceiling, or the lowest-TTFT config
overall if none do, and marks it sla_met=False. That is a documented miss, not an error.
Useful options:
Option |
Effect |
|---|---|
|
Override the run-spec’s ITL ceiling. |
|
Override the prefill SLA (mutually exclusive). |
|
Override the usable HBM fraction. Precedence: CLI flag, then run-spec, then hardware JSON, then 0.90. |
|
Pin an axis ( |
|
Write |
|
Write the roofline, Pareto, per-node Pareto, and prefill-footprint figures (PNG). |
|
Write artifacts under |
Stage 4: Inspect a single cell#
To compare against one on-hardware run, evaluate the exact operating point you plan to deploy:
PYTHONPATH=$SKILL python3.12 -m planner ./my-model/my-model-run.json \
--cell --tp 8 --cp 1 --bs 16 --ctx 8192
--cell skips the sweep and prints a per-(component, layer type) breakdown: layer count,
weight, KV, and activation megabytes, FLOPs, load time, compute time, the component floor, and
its binding term. Add --prefill to evaluate the prefill regime instead of decode.
The cell is the hypothesis a profile confirms or refutes. Hand it to
neuron-framework-profiling-vllm-neuron to capture a trace at the same operating point,
then to neuron-framework-profile-analysis-vllm-neuron to compare measured per-segment
time against the planner’s expected time. For a per-operator breakdown of the same cell, use
the Hardware Entitlement skill.
File organization#
The planner writes every artifact under <project root>/agent_artifacts/tmp/. The project
root is the current directory unless you set --project-root or $NEURON_PROJECT_ROOT.
The Hardware Entitlement skill honors the same variable, so one analysis keeps one evidence
trail.
<project root>/agent_artifacts/tmp/
├── sweep_results.json # Every decode and prefill cell, plus the batch sweep
├── target_config.json # Selected config per bucket (--emit)
├── pareto_frontier.json # Per-bucket frontier (--emit)
├── entitlement.json # Weight totals and minimum footprint (--emit)
├── roofline_<model>.png # Decode cells on the TE/HBM roofline (--visualize)
├── pareto_<model>.png # OTPS vs ITL, area = cores (--visualize)
├── pareto_per_node_<model>.png # Interactivity Pareto, DP-normalized (--visualize)
└── prefill_footprint_<model>.png # Prefill TTFT vs footprint with SLA (--visualize)
Tools reference#
Planner CLI#
Module: scripts/planner/__main__.py (python3.12 -m planner)
Runs the full sweep and selection for a run-spec, or evaluates a single cell with --cell.
Engine package#
Package: scripts/planner/
Module |
Role |
|---|---|
|
Aggregates the model into per-(class, layer type) weight, KV, and FLOP segments. |
|
Owns every TP, CP, DP, and EP divisor rule, including KV replication. |
|
Builds the ordered component graph and the activation boundary tensors. |
|
Applies the per-component roofline and serializes components into a step time. |
|
Builds the grid, solves maximum feasible batch, selects buckets, and applies gate-then-rank selection. |
|
Loads the run-spec. |
|
Write the JSON artifacts and PNG figures. |
Model and hardware layer#
Module: scripts/entitlement_formulas.py
Parses the HF config (arch_from_hf) and hardware constants, and provides the accounting
primitives and the topology-aware collective model the engine builds on.
Oracle and validation#
Module |
Role |
|---|---|
|
The shape-exact |
|
FLOP, weight, and KV counter (subclasses |
|
Diffs the engine’s analytic totals against the oracle per segment. |
Speculative decoding model#
Speculative decoding is not a sharding axis. It changes the cost per accepted token. The model
in entitlement_counter.py takes the draft length γ and the acceptance rate α:
expected accepted tokens N = (1 - α^(γ+1)) / (1 - α)
weight-read speedup = N / (1 + γ · draft_weight / target_weight)
The planner infers nothing about the draft model. You must supply its weight bytes, KV bytes, and FLOPs explicitly, either directly, from a measured report, or from the draft’s HF config.
Common issues#
Symptom |
Solution |
|---|---|
|
Pass a path to a run-spec JSON. There is no default model. |
|
The argument must be a file path. Model names are not resolved. |
Error when |
Only colocated, single-node serving is supported. Remove the field or set it to
|
Scripts refuse to write artifacts |
You are running from inside the skill package. Run from your project directory, or pass
|
|
A weight term is missing or double counted. Check tied embeddings, MoE expert counts,
and the |
Model planned as dense but it is MoE |
The config uses MoE field names |
Parsed layer count is lower than |
|
Every config shows |
No colocated config meets both SLAs at maximum batch. Check the per-node Pareto figure for lower-batch points that meet the ITL ceiling, or relax an SLA. |
CP rows look identical to their TP row |
Expected when |
Project guidelines#
The Parallelism Planner skill enforces these rules:
Single-node, colocated serving only. Never propose disaggregated prefill and decode, split pools, or multi-node topologies, even when no colocated config meets the SLA.
Always pass an explicit run-spec path. No environment variable or default selects a model.
Keep the HF config pristine. Put precision, KV dtype, and memory budget in the run-spec.
Validate against the oracle before trusting a new model.
Report the full TP sweep, not just the winner. For collective-bound models, the latency optimum is often an interior TP degree, not the maximum.
Treat every result as a prediction until it is profiled on hardware.
Knowledge base#
The skill’s references/ and samples/ directories contain:
methodology.md: The full sweep methodology, the findings that drive config selection, the two serving objectives, prefill, and the prefix-cache tradeoff.parallelism-cost-model.md: How each parallelism axis scales compute, weight, KV, and communication cost, and the speculative-decoding cost model.tp-feasibility-and-kv-replication.md: Why the TP ceiling is the query-head count and how KV replication works. Read before trusting any high-TP number.oracle-construction.md: How the oracle is built, and how to extend it for a new architecture.design.md: The engine’s design: aggregation, sharding, the cost model, and selection.trn2_constants.json: The shipped trn2 hardware constants.samples/granite4-8b-walkthrough.md: An end-to-end walkthrough for Granite-4.1-8B on trn2, including oracle validation, the sweep, a cell breakdown, and a comparison against measured hardware.