Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions perfutils/collect_neoversev3_perf_counters.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,67 @@ L2_CHI_BUSY='r198,r199,r19A,r19B,r19C'
# sve_pred_full_spec (0x8076), sve_pred_partial_spec (0x8077)
SVE_PRED='r8074,r8075,r8076,r8077'

### CMN-Cypress Uncore PMU Events (SLC / System Level Cache) ---------
### Auto-discover arm_cmn_N devices (one per chiplet on NV3).
### HN-S (Home Node with SLC) events provide true SLC miss rate,
### which is not observable from the core PMU on Neoverse V3.
### Memory bandwidth + CMN uncore telemetry --------------------------------
###
### Doc basis (Arm):
### * Phoenix SoC Architecture Spec (110009_0100), "System Telemetry":
### - Table 15-6 (DMS Performance Telemetry): true memory bandwidth is
### measured at the DDR memory controller ("DMC Bandwidth Measurement").
### On this platform that PMU is exposed as arm_cspmu_mc_<N> (one per DDR
### data sub-channel); config=0 counts DDR data beats (32 B each).
### - Table 15-4 (CMN Performance Events): the HN-S "effectiveness" group
### (slc/sf hit, pocq occupancy, mc_requests, mc_retry).
### * CMN-S3 "Cyprus" mesh (arm_cmn identifier 0x43e = PART_CMN_S3; the Phoenix
### SoC spec names it "CMN-Cyprus Coherent Mesh Network"). HN-S PMU event
### 0x0D = PMU_HN_MC_REQS_EVENT ("requests sent to MC"), with filter
### pmu_sn_home_sel [40:39]: 00=All, 01=SN-bound, 10=Home-bound. Perf exposes
### these as hns_mc_reqs_{local,remote}_{all,sn,home}. (Event encoding is
### shared with the CMN-700 TRM, doc 102308, cmn_hns_pmu_event_sel.)
###
### PRIMARY MEMORY BANDWIDTH = DMC PMU (arm_cspmu_mc, config=0 x 32 B), summed
### over the ACTIVE data channels. This is Arm's documented true-DRAM-bandwidth
### path, is bounded by the physical DDR ceiling, and is a SEPARATE PMU so it
### does not consume the CMN DTC counter budget. Validated to within ~2% of
### mm-mem across read/write/mixed loads.
###
### SECONDARY (locality only) = CMN HN-F mc_reqs with the SN filter
### (hns_mc_reqs_{local,remote}_sn). "_sn" = requests dispatched to the memory
### controller (Slave Node), i.e. actual DRAM accesses -- a bounded MESH
### MC-REQUEST ESTIMATE + local/remote split. We deliberately AVOID the "_all"
### filter for bandwidth: hns_mc_reqs_*_all = _home + _sn (arrival- plus
### dispatch-side counts of overlapping requests), which OVER-READS past the
### physical DRAM ceiling under saturated streaming. We also do not derive
### bandwidth from XP DAT flits (they fire at every mesh crosspoint -> multi-hop
### overcount).
###
### NOTE ON MULTIPLEXING: each CMN DTC exposes only ~4 usable PMU counters, and
### system telemetry daemons (e.g. dynolog) may hold some. Keep the CMN group
### lean so perf does not multiplex/scale the counts. The DMC (arm_cspmu_mc)
### group uses its own counters and is collected separately.

### DMC memory-controller PMU (primary bandwidth).
### Active data-channel instances only; override with CSPMU_ACTIVE if the SoC
### exposes a different active set (12 DIMMs x 2 sub-channels = 24 here).
CSPMU_ACTIVE="${CSPMU_ACTIVE:-0 1 2 3 4 5 6 7 8 9 10 11 24 25 26 27 28 29 30 31 32 33 34 35}"
DMC_MEM_EVENTS=""
for i in ${CSPMU_ACTIVE}; do
[ -d "/sys/bus/event_source/devices/arm_cspmu_mc_${i}" ] && \
DMC_MEM_EVENTS+="arm_cspmu_mc_${i}/config=0/,"
done
DMC_MEM_EVENTS="${DMC_MEM_EVENTS%,}"

### CMN HN-S effectiveness + MC-request-locality events (lean group).
CMN_SLC_EVENTS=""
for cmn_dev in $(find /sys/bus/event_source/devices/ -maxdepth 1 -name 'arm_cmn_*' -printf '%f\n' 2>/dev/null | sort); do
CMN_SLC_EVENTS+="${cmn_dev}/hns_slc_sf_cache_access_all/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_cache_miss_all/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_cache_fill_all/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_sf_hit_all/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_reqs_local_sn/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_reqs_remote_sn/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_retries_local_all/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_reqs_local_all/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_pocq_reqs_recvd_all/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_qos_pocq_occupancy_read/,"
CMN_SLC_EVENTS+="${cmn_dev}/hns_qos_pocq_occupancy_write/,"
CMN_SLC_EVENTS+="${cmn_dev}/dtc_cycles/,"
done
CMN_SLC_EVENTS="${CMN_SLC_EVENTS%,}"

Expand All @@ -152,7 +202,7 @@ CPU_GROUP_MUX="${INSTRUCTIONS_RATE},${L1_DCACHE_MISSES},${L1_ICACHE_MISSES},${L2

PERF_PID=
wrapup() {
kill -INT "$PERF_PID"
kill -INT "$PERF_PID" 2>/dev/null
}

trap wrapup SIGINT SIGTERM
Expand All @@ -175,10 +225,17 @@ collect_counters() {
fi
interval_ms="$((interval * 1000))"
# Core PMU events in a single multiplexed group, plus CMN uncore if available.
# The DMC (arm_cspmu_mc) group is a SEPARATE PMU with its own counters, so it
# is added as an independent -e group and does not compete with the CMN DTC
# counter budget.
events="-e ${CPU_GROUP_MUX}"
if [[ -n "$DMC_MEM_EVENTS" ]]; then
events+=" -e ${DMC_MEM_EVENTS}"
fi
if [[ -n "$CMN_SLC_EVENTS" ]]; then
events+=" -e ${CMN_SLC_EVENTS}"
fi

if [[ -n "$outfile" ]]; then
perf_stat "$events" "$interval_ms" > "$outfile"
else
Expand Down
236 changes: 222 additions & 14 deletions perfutils/generate_arm_neoversev3_perf_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
# (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.

"""
ARM Neoverse V3 performance report generator.
Expand Down Expand Up @@ -165,11 +164,11 @@ def _align(grouped_df, ev_a, ev_b):


def _sum_cmn_event(grouped_df, event_suffix):
"""Sum a CMN HN-S event across all chiplets (arm_cmn_0, arm_cmn_1, ...).
"""Sum a CMN HN-S event across all mesh instances (arm_cmn_0, arm_cmn_1, ...).

CMN-Cypress exposes one arm_cmn_N PMU per chiplet. This helper aggregates
a given event across all discovered chiplets so metrics reflect the full
system-level cache.
Arm CMN exposes one arm_cmn_N PMU per mesh instance (one per die on
multi-die parts). This helper aggregates a given event across all
discovered mesh instances so metrics reflect the full system-level cache.
"""
total = None
for name, group in grouped_df:
Expand All @@ -185,6 +184,32 @@ def _sum_cmn_event(grouped_df, event_suffix):
return total.reset_index(drop=True)


def _sum_cspmu_config0(grouped_df):
"""Sum the DMC memory-controller PMU data-beat counter (config=0) across all
active arm_cspmu_mc_<N> channels.

Each arm_cspmu_mc_<N> instance is one DDR data (sub)channel; config=0 counts
DDR data beats (32 B each). Summing the active channels gives the physical
DRAM bandwidth (Arm "DMC Bandwidth Measurement", Phoenix SoC spec Table 15-6).
"""
total = None
for name, group in grouped_df:
if (
isinstance(name, str)
and name.startswith("arm_cspmu_mc_")
and "config=0" in name
):
if total is None:
total = group.counter_value.copy()
else:
vals = group.counter_value
vals.index = total.index
total = total + vals
if total is None:
raise KeyError("arm_cspmu_mc/config=0")
return total.reset_index(drop=True)


# ===========================================================================
# Core throughput metrics
# ===========================================================================
Expand Down Expand Up @@ -788,28 +813,204 @@ def dispatch_stall_mcq(grouped_df):


# ===========================================================================
# SVE predication effectiveness (V3-specific)
# CMN mesh (uncore) + DMC memory-controller metrics
#
# Doc basis (Arm):
# * Phoenix SoC Architecture Spec (110009_0100), "System Telemetry":
# - Table 15-6 (DMS): true memory bandwidth is the DDR memory-controller
# "DMC Bandwidth Measurement". On this platform the DMC PMU is exposed as
# arm_cspmu_mc_<N> (one per DDR data sub-channel); config=0 counts DDR
# data beats of 32 B each.
# - Table 15-4 (CMN): HN-S "effectiveness" group (slc/sf hit, pocq
# occupancy, mc_requests, mc_retry).
# * CMN-S3 "Cyprus" mesh (arm_cmn identifier 0x43e = PART_CMN_S3; the Phoenix
# SoC spec names it "CMN-Cyprus Coherent Mesh Network"). HN-S PMU event
# 0x0D = PMU_HN_MC_REQS_EVENT ("requests sent to MC"); filter pmu_sn_home_sel
# [40:39]: 00=All, 01=SN-bound, 10=Home-bound. (Event encoding is shared with
# the CMN-700 TRM, doc 102308, cmn_hns_pmu_event_sel.)
#
# PRIMARY memory bandwidth is taken from the DMC PMU (arm_cspmu_mc, config=0
# x 32 B) -- Arm's documented true-DRAM-bandwidth path, bounded by the physical
# DDR ceiling and validated to ~2% of mm-mem.
#
# The CMN HN-S mc_reqs counters are used ONLY as a bounded mesh MC-request
# estimate + local/remote locality split, via the SN filter
# (hns_mc_reqs_{local,remote}_sn) x 64 B. "_sn" counts requests dispatched to
# the memory controller (Slave Node) = actual DRAM accesses. We deliberately do
# NOT use hns_mc_reqs_*_all x 64 B for bandwidth: "_all" = "_home" + "_sn"
# (arrival- plus dispatch-side counts of overlapping requests), which over-reads
# past the physical DRAM ceiling under saturated cross-mesh streaming.
# ===========================================================================


@skip_if_missing
def cmn_mem_read_bw_MBps(grouped_df):
"""Memory read bandwidth from CMN MC request counters.
def dmc_mem_bw_MBps(grouped_df):
"""PRIMARY memory bandwidth (read+write) from the DMC memory-controller PMU.

Arm's documented "DMC Bandwidth Measurement" (Phoenix SoC spec Table 15-6).
Sums arm_cspmu_mc/config=0 (DDR data beats, 32 B each) over the active data
channels. Bounded by the physical DDR ceiling; the ground-truth bandwidth.
"""
beats = _sum_cspmu_config0(grouped_df)
dur = get_duration_series(grouped_df.get_group("instructions"))
beats.index = dur.index
bw_series = (beats * 32).div(dur)
return {
"name": "DMC Memory Bandwidth (MBps)",
"series": bw_series,
"prefix": 10**-6,
}


@skip_if_missing
def cmn_mem_bw_MBps(grouped_df):
"""Mesh MC-request bandwidth ESTIMATE (read+write) from CMN HN-S counters.

Each hns_mc_reqs_local_all is a cache-line (64B) request to the memory
controller, analogous to Grace's SCF cmem_rd_data.
Sums local + remote HN-S -> memory-controller requests (SN filter) across
all mesh instances; each is a 64 B cache-line request dispatched to a memory
controller. This is a bounded ESTIMATE (it undercounts pure-write ~24% due to
write-combining at the MC and is not a physical data-beat count) -- the DMC
PMU metric above is the authoritative bandwidth. Reported for cross-check and
because it provides the local/remote locality split the DMC PMU cannot.
"""
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_all")
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_sn")
try:
mc_reqs = mc_reqs + _sum_cmn_event(grouped_df, "hns_mc_reqs_remote_sn")
except KeyError:
pass
dur = get_duration_series(grouped_df.get_group("instructions"))
mc_reqs.index = dur.index
bw_series = (mc_reqs * 64).div(dur)
return {
"name": "CMN Memory Read Bandwidth (MBps)",
"name": "CMN MC-Req Bandwidth (MBps, est)",
"series": bw_series,
"prefix": 10**-6,
}


@skip_if_missing
def cmn_mem_local_bw_MBps(grouped_df):
"""Local mesh MC-request bandwidth (est) — SN-filter requests homed on the
local mesh instance's memory controllers."""
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_sn")
dur = get_duration_series(grouped_df.get_group("instructions"))
mc_reqs.index = dur.index
bw_series = (mc_reqs * 64).div(dur)
return {
"name": "CMN Local MC-Req Bandwidth (MBps, est)",
"series": bw_series,
"prefix": 10**-6,
}


@skip_if_missing
def cmn_mem_remote_bw_MBps(grouped_df):
"""Remote mesh MC-request bandwidth (est) — SN-filter requests routed to
another mesh instance's memory controllers (cross-mesh / cross-node)."""
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_remote_sn")
dur = get_duration_series(grouped_df.get_group("instructions"))
mc_reqs.index = dur.index
bw_series = (mc_reqs * 64).div(dur)
return {
"name": "CMN Remote MC-Req Bandwidth (MBps, est)",
"series": bw_series,
"prefix": 10**-6,
}


@skip_if_missing
def cmn_mem_read_pct(grouped_df):
"""Approximate read share of memory traffic from HN-S PoCQ occupancy.

The HN-S has no read/write split on MC requests, but the QoS PoCQ
occupancy counters (read vs write) track how long read- vs write-class
requests sit in the point-of-coherency queue, giving a usable read/write
mix proxy. Reported as the read fraction of (read + write) occupancy.
"""
read_occ = _sum_cmn_event(grouped_df, "hns_qos_pocq_occupancy_read")
write_occ = _sum_cmn_event(grouped_df, "hns_qos_pocq_occupancy_write")
write_occ.index = read_occ.index
total = read_occ + write_occ
return {
"name": "CMN Memory Read Mix %",
"series": read_occ.div(total),
"prefix": 100,
}


@skip_if_missing
def cmn_mc_retry_pct(grouped_df):
"""Memory-controller retry rate — retried MC requests / total MC requests.

Maps to the HN-S "mc_retry" metric. A high retry rate indicates
the memory controller is backpressuring the mesh (DRAM-bound).
"""
retries = _sum_cmn_event(grouped_df, "hns_mc_retries_local_all")
try:
retries = retries + _sum_cmn_event(grouped_df, "hns_mc_retries_remote_all")
except KeyError:
pass
reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_all")
try:
reqs = reqs + _sum_cmn_event(grouped_df, "hns_mc_reqs_remote_all")
except KeyError:
pass
retries.index = reqs.index
return {
"name": "CMN MC Retry %",
"series": retries.div(reqs),
"prefix": 100,
}


@skip_if_missing
def cmn_sf_hit_rate(grouped_df):
"""Snoop-filter hit rate — hns_sf_hit_all / hns_slc_sf_cache_access_all.

Maps to the HN-S "sf_hit_ratio". Complements the existing SLC
(L3) hit-rate metric with coherence-directory effectiveness.
"""
hit_s = _sum_cmn_event(grouped_df, "hns_sf_hit_all")
access_s = _sum_cmn_event(grouped_df, "hns_slc_sf_cache_access_all")
hit_s.index = access_s.index
return {
"name": "CMN Snoop Filter Hit Rate %",
"series": hit_s.div(access_s),
"prefix": 100,
}


@skip_if_missing
def cmn_mesh_freq_ghz(grouped_df):
"""Mesh clock frequency (GHz) derived from the DTC cycle counter.

dtc_cycles increments at the mesh clock; dividing by the wall-clock
sample duration recovers the effective mesh frequency. Useful for
detecting mesh DVFS throttling under load.
"""
cyc = _sum_cmn_event(grouped_df, "dtc_cycles")
# dtc_cycles is summed across mesh instances; use the per-instance average.
n_cmn = 0
for name, _ in grouped_df:
if isinstance(name, str) and name.endswith("/dtc_cycles/"):
n_cmn += 1
dur = get_duration_series(grouped_df.get_group("instructions"))
cyc.index = dur.index
if n_cmn > 1:
cyc = cyc / n_cmn
freq = cyc.div(dur) # cycles per second
return {
"name": "CMN Mesh Frequency (GHz)",
"series": freq,
"prefix": 10**-9,
}


# ===========================================================================
# SVE predication effectiveness (V3-specific)
# ===========================================================================


@skip_if_missing
def sve_pred_empty_pct(grouped_df):
"""SVE predicated ops with no active lanes (wasted work)."""
Expand Down Expand Up @@ -946,8 +1147,15 @@ def main(
sve_pred_empty_pct(grouped_df),
sve_pred_full_pct(grouped_df),
sve_pred_partial_pct(grouped_df),
# --- CMN uncore (SLC / memory bandwidth) ---
cmn_mem_read_bw_MBps(grouped_df),
# --- CMN mesh (uncore) metrics ---
dmc_mem_bw_MBps(grouped_df),
cmn_mem_bw_MBps(grouped_df),
cmn_mem_local_bw_MBps(grouped_df),
cmn_mem_remote_bw_MBps(grouped_df),
cmn_mem_read_pct(grouped_df),
cmn_mc_retry_pct(grouped_df),
cmn_sf_hit_rate(grouped_df),
cmn_mesh_freq_ghz(grouped_df),
]

filtered_metrics = list(itertools.filterfalse(lambda x: x is None, metrics))
Expand Down