# PMU counters

## Purpose

Choose what the per-layer measurement counts. hpx captures cycles,
instructions, cache and bus activity, and on a Cortex-M55 the MVE vector
unit and, on Atomiq110, the Ethos-U NPU. The tables on this page are
generated from the counter registry of the hpx you are reading about, so
the names and groups are the ones the profiler accepts.

## Prerequisites

A working profile ([First profile](https://ambiqai.github.io/helia-profiler/getting-started/first-profile/))
and a board whose family you know from [Boards and targets](https://ambiqai.github.io/helia-profiler/guide/boards/).

## Which groups your board has

| SoC | Profiling domains | Counter groups | Per-operator record budget |
|---|---|---|---|
| `apollo3p` | cpu | `cpu` | 2048 |
| `apollo4p` | cpu | `cpu` | 2048 |
| `apollo4l` | cpu | `cpu` | 2048 |
| `apollo510` | cpu, memory, mve | `cpu`, `memory`, `mve` | 4096 |
| `apollo510b` | cpu, memory, mve | `cpu`, `memory`, `mve` | 4096 |
| `apollo5b` | cpu, memory, mve | `cpu`, `memory`, `mve` | 4096 |
| `apollo330P` | cpu, memory, mve | `cpu`, `memory`, `mve` | 512 |
| `apollo510L` | cpu, memory, mve | `cpu`, `memory`, `mve` | 512 |
| `atomiq110` | cpu, memory, mve, ethos_npu | `cpu`, `ethos_npu`, `memory`, `mve` | 4096 |

The rows come from `list_socs()` and `SocDef.profiling_domains` through
`supported_groups_for_domains()` in `src/helia_profiler/platform/`. On a
DWT-only SoC the profiler warns and captures cycle counts only; a
`pmu_counters` selection has no effect there, and the `mve` group is
rejected at preflight.

## Steps

### 1. Select counters

`profiling.pmu_counters` maps a group to a selection:

| Selection | Meaning |
|---|---|
| `default` | the curated subset for that group, sized to fit one pass |
| `all` | every counter in the group, captured over several passes |
| `[name, ...]` | an explicit list of counter names |

```yaml
profiling:
  pmu_counters:
    cpu: default
    memory: [ARM_PMU_L1D_CACHE, ARM_PMU_L1D_CACHE_MISS_RD, ARM_PMU_DTCM_ACCESS]
    mve: all
```

```bash
hpx profile model.tflite --pmu-counters cpu:default --pmu-counters mve:all
hpx profile model.tflite --pmu-counters mve:ARM_PMU_MVE_INST_RETIRED,ARM_PMU_MVE_STALL
```

`per_layer` defaults to `true`, `iterations` to `100`, `warmup` to `5` and
`aggregation` to `median`. Names are case-sensitive and must match the
catalogue exactly; an unknown name fails with the full sorted list.

### 2. The catalogue

Each group below lists every counter hpx knows, with the curated `default`
selection first.

#### `cpu`: 21 counters

`default` selects `ARM_PMU_CPU_CYCLES`, `ARM_PMU_INST_RETIRED`, `ARM_PMU_STALL_FRONTEND`, `ARM_PMU_STALL_BACKEND`.

| Counter | Event | Description |
|---|---|---|
| `ARM_PMU_BR_IMMED_RETIRED` | 13 | Immediate branch architecturally executed |
| `ARM_PMU_BR_MIS_PRED_RETIRED` | 34 | Mispredicted branch instruction architecturally executed |
| `ARM_PMU_BR_RETIRED` | 33 | Branch instruction architecturally executed |
| `ARM_PMU_BR_RETURN_RETIRED` | 14 | Function return instruction architecturally executed and the condition code check pass |
| `ARM_PMU_CPU_CYCLES` (default) | 17 | Cycle |
| `ARM_PMU_EXC_RETURN` | 10 | Exception return instruction architecturally executed and the condition code check pass |
| `ARM_PMU_EXC_TAKEN` | 9 | Exception entry |
| `ARM_PMU_INST_RETIRED` (default) | 8 | Instruction architecturally executed |
| `ARM_PMU_L1I_CACHE_REFILL` | 1 | L1 I-Cache refill |
| `ARM_PMU_LD_RETIRED` | 6 | Memory-reading instruction architecturally executed and condition code check pass |
| `ARM_PMU_LE_CANCEL` | 264 | Loop end instruction not taken |
| `ARM_PMU_LE_RETIRED` | 256 | Loop end instruction executed |
| `ARM_PMU_PC_WRITE_RETIRED` | 12 | Software change to the Program Counter (PC). Instruction is architecturally executed and condition code check pass |
| `ARM_PMU_SE_CALL_NS` | 277 | Call to non-secure function, resulting in Security state change |
| `ARM_PMU_SE_CALL_S` | 276 | Call to secure function, resulting in Security state change |
| `ARM_PMU_STALL` | 60 | Stall cycle for instruction or operation not sent for execution |
| `ARM_PMU_STALL_BACKEND` (default) | 36 | No operation issued because of the backend |
| `ARM_PMU_STALL_FRONTEND` (default) | 35 | No operation issued because of the frontend |
| `ARM_PMU_ST_RETIRED` | 7 | Memory-writing instruction architecturally executed and condition code check pass |
| `ARM_PMU_SW_INCR` | 0 | Software update to the PMU_SWINC register, architecturally executed and condition code check pass |
| `ARM_PMU_UNALIGNED_LDST_RETIRED` | 15 | Unaligned memory memory-reading or memory-writing instruction architecturally executed and condition code check pass |

#### `ethos_npu`: 9 counters

`default` selects `ETHOSU_PMU_CYCLE`, `ETHOSU_PMU_NPU_ACTIVE`, `ETHOSU_PMU_MAC_ACTIVE`, `ETHOSU_PMU_SRAM_RD_DATA_BEAT_RECEIVED`.

| Counter | Event | Description |
|---|---|---|
| `ETHOSU_PMU_CYCLE` (default) | 0 | Total NPU cycles while the command stream runs |
| `ETHOSU_PMU_EXT_RD_DATA_BEAT_RECEIVED` | 0 | External (flash/MRAM) read data beats |
| `ETHOSU_PMU_EXT_WR_DATA_BEAT_WRITTEN` | 0 | External (flash/MRAM) write data beats |
| `ETHOSU_PMU_MAC_ACTIVE` (default) | 0 | Cycles the MAC engine is active |
| `ETHOSU_PMU_NPU_ACTIVE` (default) | 0 | Cycles the NPU is active (not idle/stalled) |
| `ETHOSU_PMU_NPU_IDLE` | 0 | Cycles the NPU is idle |
| `ETHOSU_PMU_SRAM_RD_DATA_BEAT_RECEIVED` (default) | 0 | SRAM interface read data beats |
| `ETHOSU_PMU_SRAM_WR_DATA_BEAT_WRITTEN` | 0 | SRAM interface write data beats |
| `ETHOSU_PMU_WD_ACTIVE` | 0 | Cycles the weight decoder is active |

#### `memory`: 15 counters

`default` selects `ARM_PMU_MEM_ACCESS`, `ARM_PMU_L1D_CACHE_REFILL`, `ARM_PMU_BUS_ACCESS`, `ARM_PMU_BUS_CYCLES`.

| Counter | Event | Description |
|---|---|---|
| `ARM_PMU_BUS_ACCESS` (default) | 25 | Bus access |
| `ARM_PMU_BUS_CYCLES` (default) | 29 | Bus cycles |
| `ARM_PMU_DTCM_ACCESS` | 16392 | Data TCM access |
| `ARM_PMU_ITCM_ACCESS` | 16391 | Instruction TCM access |
| `ARM_PMU_L1D_CACHE` | 4 | L1 D-Cache access |
| `ARM_PMU_L1D_CACHE_ALLOCATE` | 31 | Level 1 data cache allocation without refill |
| `ARM_PMU_L1D_CACHE_MISS_RD` | 57 | Level 1 data cache read miss |
| `ARM_PMU_L1D_CACHE_RD` | 64 | Level 1 data cache read |
| `ARM_PMU_L1D_CACHE_REFILL` (default) | 3 | L1 D-Cache refill |
| `ARM_PMU_L1D_CACHE_WB` | 21 | Level 1 data cache write-back |
| `ARM_PMU_L1I_CACHE` | 20 | Level 1 instruction cache access |
| `ARM_PMU_LL_CACHE_MISS_RD` | 55 | Last level data cache read miss |
| `ARM_PMU_LL_CACHE_RD` | 54 | Last level data cache read |
| `ARM_PMU_MEMORY_ERROR` | 26 | Local memory error |
| `ARM_PMU_MEM_ACCESS` (default) | 19 | Data memory access |

#### `mve`: 34 counters

`default` selects `ARM_PMU_MVE_INST_RETIRED`, `ARM_PMU_MVE_INT_MAC_RETIRED`, `ARM_PMU_MVE_LDST_RETIRED`, `ARM_PMU_MVE_STALL`.

| Counter | Event | Description |
|---|---|---|
| `ARM_PMU_MVE_FP_HP_RETIRED` | 520 | MVE half-precision floating-point instruction architecturally executed |
| `ARM_PMU_MVE_FP_MAC_RETIRED` | 532 | MVE floating-point multiply or multiply-accumulate instruction architecturally executed |
| `ARM_PMU_MVE_FP_RETIRED` | 516 | MVE floating-point instruction architecturally executed |
| `ARM_PMU_MVE_FP_SP_RETIRED` | 524 | MVE single-precision floating-point instruction architecturally executed |
| `ARM_PMU_MVE_INST_RETIRED` (default) | 512 | MVE instruction architecturally executed |
| `ARM_PMU_MVE_INT_MAC_RETIRED` (default) | 552 | MVE multiply or multiply-accumulate instruction architecturally executed |
| `ARM_PMU_MVE_INT_RETIRED` | 548 | MVE integer instruction architecturally executed |
| `ARM_PMU_MVE_LDST_CONTIG_RETIRED` | 580 | MVE contiguous load or store instruction architecturally executed |
| `ARM_PMU_MVE_LDST_MULTI_RETIRED` | 604 | MVE memory instruction targeting multiple registers architecturally executed |
| `ARM_PMU_MVE_LDST_NONCONTIG_RETIRED` | 592 | MVE non-contiguous load or store instruction architecturally executed |
| `ARM_PMU_MVE_LDST_RETIRED` (default) | 568 | MVE load or store instruction architecturally executed |
| `ARM_PMU_MVE_LDST_UNALIGNED_NONCONTIG_RETIRED` | 664 | MVE unaligned noncontiguous load or store instruction architecturally executed |
| `ARM_PMU_MVE_LDST_UNALIGNED_RETIRED` | 652 | MVE unaligned memory load or store instruction architecturally executed |
| `ARM_PMU_MVE_LD_CONTIG_RETIRED` | 584 | MVE contiguous load instruction architecturally executed |
| `ARM_PMU_MVE_LD_MULTI_RETIRED` | 608 | MVE memory load instruction targeting multiple registers architecturally executed |
| `ARM_PMU_MVE_LD_NONCONTIG_RETIRED` | 596 | MVE non-contiguous load instruction architecturally executed |
| `ARM_PMU_MVE_LD_RETIRED` | 572 | MVE load instruction architecturally executed |
| `ARM_PMU_MVE_LD_UNALIGNED_RETIRED` | 656 | MVE unaligned load instruction architecturally executed |
| `ARM_PMU_MVE_PRED` | 696 | Cycles where one or more predicated beats architecturally executed |
| `ARM_PMU_MVE_STALL` (default) | 716 | Stall cycles caused by an MVE instruction |
| `ARM_PMU_MVE_STALL_BREAK` | 723 | Stall cycles caused by an MVE chain break |
| `ARM_PMU_MVE_STALL_DEPENDENCY` | 724 | Stall cycles caused by MVE register dependency |
| `ARM_PMU_MVE_STALL_RESOURCE` | 717 | Stall cycles caused by an MVE instruction because of resource conflicts |
| `ARM_PMU_MVE_STALL_RESOURCE_FP` | 719 | Stall cycles caused by an MVE instruction because of floating-point resource conflicts |
| `ARM_PMU_MVE_STALL_RESOURCE_INT` | 720 | Stall cycles caused by an MVE instruction because of integer resource conflicts |
| `ARM_PMU_MVE_STALL_RESOURCE_MEM` | 718 | Stall cycles caused by an MVE instruction because of memory resource conflicts |
| `ARM_PMU_MVE_ST_CONTIG_RETIRED` | 588 | MVE contiguous store instruction architecturally executed |
| `ARM_PMU_MVE_ST_MULTI_RETIRED` | 612 | MVE memory store instruction targeting multiple registers architecturally executed |
| `ARM_PMU_MVE_ST_NONCONTIG_RETIRED` | 600 | MVE non-contiguous store instruction architecturally executed |
| `ARM_PMU_MVE_ST_RETIRED` | 576 | MVE store instruction architecturally executed |
| `ARM_PMU_MVE_ST_UNALIGNED_RETIRED` | 660 | MVE unaligned store instruction architecturally executed |
| `ARM_PMU_MVE_VREDUCE_FP_RETIRED` | 676 | MVE floating-point vector reduction instruction architecturally executed |
| `ARM_PMU_MVE_VREDUCE_INT_RETIRED` | 680 | MVE integer vector reduction instruction architecturally executed |
| `ARM_PMU_MVE_VREDUCE_RETIRED` | 672 | MVE vector reduction instruction architecturally executed |

Generated from the counter registry at source tree `fe269596788d6ab1604cf433559e96b5f48c1d13`: 79 counters in 4 groups across 9 SoCs.

The Ethos-U counters are sampled from the NPU's own PMU through the core
driver, per command stream, and only mean something for Vela-compiled
models on the `ethos_u` engine backend; layers the NPU never dispatched
are reported through the `NPU_DISPATCHED` column rather than as zeros.

### 3. Multi-pass capture

The Cortex-M55 PMU has 8 hardware counter slots; hpx chains pairs into
32-bit logical counters to avoid overflow, so each firmware pass captures
4 counters per group. A larger selection is planned into passes, each pass
builds, flashes and captures `warmup + iterations` inferences, and the
results merge into one per-layer table. `profile_results.csv` is always
the merged result; `output.detailed: true` adds `detailed/profile_<group>.csv`
and `profile_<group>_<pass>.csv`. Selecting `all` on every group spans
the whole catalogue and runs many independent passes; use `default` or an
explicit list while iterating.

### 4. Aggregation and iterations

`profiling.iterations` inferences per pass, after `profiling.warmup`
discarded runs, combined per layer by `profiling.aggregation`:

| Method | Behaviour |
|---|---|
| `median` (default) | robust to corrupted or outlier iterations |
| `mean` | arithmetic average |
| `trimmed` | drops the high and low extremes, then averages |

All three first reject structurally invalid samples (a uint32 wrap, a
frozen-zero row) and log how many; a counter with no surviving samples is
omitted rather than replaced. `profiling.per_layer: false` captures
whole-model counters only. Every run also performs a clean end-to-end
pass with no per-layer instrumentation and reports its cycle count next
to the per-layer sum; a small delta between the two is the
instrumentation overhead and is shown in the terminal.

### 5. Read the results

- The terminal's top-layers table: layers by cycles with their share of
  the total, and on a Cortex-M55 the MVE instruction share and MACs per
  MVE instruction.
- `profile_results.csv`: one row per layer, one column per captured
  counter, merged across passes; `overflow` is `True` for a layer whose
  counter saturated, whose values are then a lower bound.
- `detailed/profile_<group>.csv` and per-pass files with `output.detailed`.
- `model_explorer/me_overlay_<COUNTER>.json`, one per counter, for
  [Model Explorer](https://ambiqai.github.io/helia-profiler/guide/model-explorer/).

| Derived metric | Formula | Where |
|---|---|---|
| L1D hit rate | `(1 - L1D_CACHE_MISS_RD / L1D_CACHE_RD) x 100%`, falling back to `L1D_CACHE` and `L1D_CACHE_REFILL` as a complete pair | `summary.json`, `detailed/memory.json` |
| MVE instruction share | `MVE_INST_RETIRED / INST_RETIRED x 100%` | terminal |
| MVE MAC density | `MVE_INT_MAC_RETIRED / MVE_INST_RETIRED` | terminal |
| MVE load/store density | `MVE_LDST_RETIRED / MVE_INST_RETIRED` | terminal |
| MVE stall share | `MVE_STALL / CPU_CYCLES x 100%` | terminal |

A derived rate is omitted, not guessed, when no consistent counter pair
covers every layer.

### 6. Apollo3 and Apollo4: the debugger stays attached

On the Cortex-M4F families the `DWT->CYCCNT` cycle counter lives in the
core debug power domain, powered only while a debugger asserts the Debug
Access Port's `CDBGPWRUPREQ`, which firmware cannot set. If the host
released the probe after reset the domain would power down mid-run and
every per-layer cycle count would read 0. The `rtt` and `swo` transports
hold a debugger attached anyway; for `uart` and `usb_cdc` hpx detects
these families and keeps a `pylink` session attached for the whole
capture. Apollo5 boards keep releasing the probe after reset: their
Armv8-M PMU does not need it and the secure bootloader prefers it. The
dedicated power binary never has a debugger, which is why it times its
window with STIMER on those families (`SocCapabilities.power_window_timer`).

Each SoC also reserves a fixed number of per-operator PMU records
(`SocDef.pmu_max_ops`). A model that runs more operators than that fails
the capture with `HPX_ERROR=operator_count_exceeds_capacity` on every
engine; the limit is per SoC rather than a configuration key.

## Expected output

`profile_results.csv` with one column per requested counter,
`overflow_detected: false` in `summary.json`, and a clean-pass cycle count
close to the per-layer sum.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `overflow_detected: true` | a counter wrapped its 32-bit accumulator on a very long or hot layer, or that layer's PMU read failed | fewer iterations, or fewer or narrower counters for that layer |
| selections silently ignored | a DWT-only SoC | expected; `hpx profile --verbose` shows the warning |
| per-layer cycles read 0 on Apollo4 | the probe was released early over `uart` or `usb_cdc` | check for a custom reset or lifecycle override |
| `all` takes a long time | many independent build, flash and capture passes | narrow the selection |
| unknown counter name | case or spelling | copy it from the catalogue above |
| group not supported for this target | `mve` needs a Cortex-M55; `ethos_npu` needs Atomiq110 | pick a group the family table lists |

## What next

[Power measurement](https://ambiqai.github.io/helia-profiler/guide/power/) to add energy to the same window, or
[Output files](https://ambiqai.github.io/helia-profiler/guide/output/) for every column the CSV carries.
