Skip to content

Commit 60f5cd9

Browse files
charles-typfacebook-github-bot
authored andcommitted
Report native CMN mesh (uncore) performance metrics
Differential Revision: D102244354
1 parent a798023 commit 60f5cd9

2 files changed

Lines changed: 287 additions & 22 deletions

File tree

perfutils/collect_neoversev3_perf_counters.sh

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,67 @@ L2_CHI_BUSY='r198,r199,r19A,r19B,r19C'
128128
# sve_pred_full_spec (0x8076), sve_pred_partial_spec (0x8077)
129129
SVE_PRED='r8074,r8075,r8076,r8077'
130130

131-
### CMN-Cypress Uncore PMU Events (SLC / System Level Cache) ---------
132-
### Auto-discover arm_cmn_N devices (one per chiplet on NV3).
133-
### HN-S (Home Node with SLC) events provide true SLC miss rate,
134-
### which is not observable from the core PMU on Neoverse V3.
131+
### Memory bandwidth + CMN uncore telemetry --------------------------------
132+
###
133+
### Doc basis (Arm):
134+
### * Phoenix SoC Architecture Spec (110009_0100), "System Telemetry":
135+
### - Table 15-6 (DMS Performance Telemetry): true memory bandwidth is
136+
### measured at the DDR memory controller ("DMC Bandwidth Measurement").
137+
### On this platform that PMU is exposed as arm_cspmu_mc_<N> (one per DDR
138+
### data sub-channel); config=0 counts DDR data beats (32 B each).
139+
### - Table 15-4 (CMN Performance Events): the HN-S "effectiveness" group
140+
### (slc/sf hit, pocq occupancy, mc_requests, mc_retry).
141+
### * CMN-S3 "Cyprus" mesh (arm_cmn identifier 0x43e = PART_CMN_S3; the Phoenix
142+
### SoC spec names it "CMN-Cyprus Coherent Mesh Network"). HN-S PMU event
143+
### 0x0D = PMU_HN_MC_REQS_EVENT ("requests sent to MC"), with filter
144+
### pmu_sn_home_sel [40:39]: 00=All, 01=SN-bound, 10=Home-bound. Perf exposes
145+
### these as hns_mc_reqs_{local,remote}_{all,sn,home}. (Event encoding is
146+
### shared with the CMN-700 TRM, doc 102308, cmn_hns_pmu_event_sel.)
147+
###
148+
### PRIMARY MEMORY BANDWIDTH = DMC PMU (arm_cspmu_mc, config=0 x 32 B), summed
149+
### over the ACTIVE data channels. This is Arm's documented true-DRAM-bandwidth
150+
### path, is bounded by the physical DDR ceiling, and is a SEPARATE PMU so it
151+
### does not consume the CMN DTC counter budget. Validated to within ~2% of
152+
### mm-mem across read/write/mixed loads.
153+
###
154+
### SECONDARY (locality only) = CMN HN-F mc_reqs with the SN filter
155+
### (hns_mc_reqs_{local,remote}_sn). "_sn" = requests dispatched to the memory
156+
### controller (Slave Node), i.e. actual DRAM accesses -- a bounded MESH
157+
### MC-REQUEST ESTIMATE + local/remote split. We deliberately AVOID the "_all"
158+
### filter for bandwidth: hns_mc_reqs_*_all = _home + _sn (arrival- plus
159+
### dispatch-side counts of overlapping requests), which OVER-READS past the
160+
### physical DRAM ceiling under saturated streaming. We also do not derive
161+
### bandwidth from XP DAT flits (they fire at every mesh crosspoint -> multi-hop
162+
### overcount).
163+
###
164+
### NOTE ON MULTIPLEXING: each CMN DTC exposes only ~4 usable PMU counters, and
165+
### system telemetry daemons (e.g. dynolog) may hold some. Keep the CMN group
166+
### lean so perf does not multiplex/scale the counts. The DMC (arm_cspmu_mc)
167+
### group uses its own counters and is collected separately.
168+
169+
### DMC memory-controller PMU (primary bandwidth).
170+
### Active data-channel instances only; override with CSPMU_ACTIVE if the SoC
171+
### exposes a different active set (12 DIMMs x 2 sub-channels = 24 here).
172+
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}"
173+
DMC_MEM_EVENTS=""
174+
for i in ${CSPMU_ACTIVE}; do
175+
[ -d "/sys/bus/event_source/devices/arm_cspmu_mc_${i}" ] && \
176+
DMC_MEM_EVENTS+="arm_cspmu_mc_${i}/config=0/,"
177+
done
178+
DMC_MEM_EVENTS="${DMC_MEM_EVENTS%,}"
179+
180+
### CMN HN-S effectiveness + MC-request-locality events (lean group).
135181
CMN_SLC_EVENTS=""
136182
for cmn_dev in $(find /sys/bus/event_source/devices/ -maxdepth 1 -name 'arm_cmn_*' -printf '%f\n' 2>/dev/null | sort); do
137183
CMN_SLC_EVENTS+="${cmn_dev}/hns_slc_sf_cache_access_all/,"
138-
CMN_SLC_EVENTS+="${cmn_dev}/hns_cache_miss_all/,"
139-
CMN_SLC_EVENTS+="${cmn_dev}/hns_cache_fill_all/,"
184+
CMN_SLC_EVENTS+="${cmn_dev}/hns_sf_hit_all/,"
185+
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_reqs_local_sn/,"
186+
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_reqs_remote_sn/,"
187+
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_retries_local_all/,"
140188
CMN_SLC_EVENTS+="${cmn_dev}/hns_mc_reqs_local_all/,"
141-
CMN_SLC_EVENTS+="${cmn_dev}/hns_pocq_reqs_recvd_all/,"
189+
CMN_SLC_EVENTS+="${cmn_dev}/hns_qos_pocq_occupancy_read/,"
190+
CMN_SLC_EVENTS+="${cmn_dev}/hns_qos_pocq_occupancy_write/,"
191+
CMN_SLC_EVENTS+="${cmn_dev}/dtc_cycles/,"
142192
done
143193
CMN_SLC_EVENTS="${CMN_SLC_EVENTS%,}"
144194

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

153203
PERF_PID=
154204
wrapup() {
155-
kill -INT "$PERF_PID"
205+
kill -INT "$PERF_PID" 2>/dev/null
156206
}
157207

158208
trap wrapup SIGINT SIGTERM
@@ -175,10 +225,17 @@ collect_counters() {
175225
fi
176226
interval_ms="$((interval * 1000))"
177227
# Core PMU events in a single multiplexed group, plus CMN uncore if available.
228+
# The DMC (arm_cspmu_mc) group is a SEPARATE PMU with its own counters, so it
229+
# is added as an independent -e group and does not compete with the CMN DTC
230+
# counter budget.
178231
events="-e ${CPU_GROUP_MUX}"
232+
if [[ -n "$DMC_MEM_EVENTS" ]]; then
233+
events+=" -e ${DMC_MEM_EVENTS}"
234+
fi
179235
if [[ -n "$CMN_SLC_EVENTS" ]]; then
180236
events+=" -e ${CMN_SLC_EVENTS}"
181237
fi
238+
182239
if [[ -n "$outfile" ]]; then
183240
perf_stat "$events" "$interval_ms" > "$outfile"
184241
else

perfutils/generate_arm_neoversev3_perf_report.py

Lines changed: 222 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#
44
# This source code is licensed under the MIT license found in the
55
# LICENSE file in the root directory of this source tree.
6-
# (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
76

87
"""
98
ARM Neoverse V3 performance report generator.
@@ -165,11 +164,11 @@ def _align(grouped_df, ev_a, ev_b):
165164

166165

167166
def _sum_cmn_event(grouped_df, event_suffix):
168-
"""Sum a CMN HN-S event across all chiplets (arm_cmn_0, arm_cmn_1, ...).
167+
"""Sum a CMN HN-S event across all mesh instances (arm_cmn_0, arm_cmn_1, ...).
169168
170-
CMN-Cypress exposes one arm_cmn_N PMU per chiplet. This helper aggregates
171-
a given event across all discovered chiplets so metrics reflect the full
172-
system-level cache.
169+
Arm CMN exposes one arm_cmn_N PMU per mesh instance (one per die on
170+
multi-die parts). This helper aggregates a given event across all
171+
discovered mesh instances so metrics reflect the full system-level cache.
173172
"""
174173
total = None
175174
for name, group in grouped_df:
@@ -185,6 +184,32 @@ def _sum_cmn_event(grouped_df, event_suffix):
185184
return total.reset_index(drop=True)
186185

187186

187+
def _sum_cspmu_config0(grouped_df):
188+
"""Sum the DMC memory-controller PMU data-beat counter (config=0) across all
189+
active arm_cspmu_mc_<N> channels.
190+
191+
Each arm_cspmu_mc_<N> instance is one DDR data (sub)channel; config=0 counts
192+
DDR data beats (32 B each). Summing the active channels gives the physical
193+
DRAM bandwidth (Arm "DMC Bandwidth Measurement", Phoenix SoC spec Table 15-6).
194+
"""
195+
total = None
196+
for name, group in grouped_df:
197+
if (
198+
isinstance(name, str)
199+
and name.startswith("arm_cspmu_mc_")
200+
and "config=0" in name
201+
):
202+
if total is None:
203+
total = group.counter_value.copy()
204+
else:
205+
vals = group.counter_value
206+
vals.index = total.index
207+
total = total + vals
208+
if total is None:
209+
raise KeyError("arm_cspmu_mc/config=0")
210+
return total.reset_index(drop=True)
211+
212+
188213
# ===========================================================================
189214
# Core throughput metrics
190215
# ===========================================================================
@@ -788,28 +813,204 @@ def dispatch_stall_mcq(grouped_df):
788813

789814

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

794845

795846
@skip_if_missing
796-
def cmn_mem_read_bw_MBps(grouped_df):
797-
"""Memory read bandwidth from CMN MC request counters.
847+
def dmc_mem_bw_MBps(grouped_df):
848+
"""PRIMARY memory bandwidth (read+write) from the DMC memory-controller PMU.
849+
850+
Arm's documented "DMC Bandwidth Measurement" (Phoenix SoC spec Table 15-6).
851+
Sums arm_cspmu_mc/config=0 (DDR data beats, 32 B each) over the active data
852+
channels. Bounded by the physical DDR ceiling; the ground-truth bandwidth.
853+
"""
854+
beats = _sum_cspmu_config0(grouped_df)
855+
dur = get_duration_series(grouped_df.get_group("instructions"))
856+
beats.index = dur.index
857+
bw_series = (beats * 32).div(dur)
858+
return {
859+
"name": "DMC Memory Bandwidth (MBps)",
860+
"series": bw_series,
861+
"prefix": 10**-6,
862+
}
863+
864+
865+
@skip_if_missing
866+
def cmn_mem_bw_MBps(grouped_df):
867+
"""Mesh MC-request bandwidth ESTIMATE (read+write) from CMN HN-S counters.
798868
799-
Each hns_mc_reqs_local_all is a cache-line (64B) request to the memory
800-
controller, analogous to Grace's SCF cmem_rd_data.
869+
Sums local + remote HN-S -> memory-controller requests (SN filter) across
870+
all mesh instances; each is a 64 B cache-line request dispatched to a memory
871+
controller. This is a bounded ESTIMATE (it undercounts pure-write ~24% due to
872+
write-combining at the MC and is not a physical data-beat count) -- the DMC
873+
PMU metric above is the authoritative bandwidth. Reported for cross-check and
874+
because it provides the local/remote locality split the DMC PMU cannot.
801875
"""
802-
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_all")
876+
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_sn")
877+
try:
878+
mc_reqs = mc_reqs + _sum_cmn_event(grouped_df, "hns_mc_reqs_remote_sn")
879+
except KeyError:
880+
pass
803881
dur = get_duration_series(grouped_df.get_group("instructions"))
804882
mc_reqs.index = dur.index
805883
bw_series = (mc_reqs * 64).div(dur)
806884
return {
807-
"name": "CMN Memory Read Bandwidth (MBps)",
885+
"name": "CMN MC-Req Bandwidth (MBps, est)",
808886
"series": bw_series,
809887
"prefix": 10**-6,
810888
}
811889

812890

891+
@skip_if_missing
892+
def cmn_mem_local_bw_MBps(grouped_df):
893+
"""Local mesh MC-request bandwidth (est) — SN-filter requests homed on the
894+
local mesh instance's memory controllers."""
895+
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_sn")
896+
dur = get_duration_series(grouped_df.get_group("instructions"))
897+
mc_reqs.index = dur.index
898+
bw_series = (mc_reqs * 64).div(dur)
899+
return {
900+
"name": "CMN Local MC-Req Bandwidth (MBps, est)",
901+
"series": bw_series,
902+
"prefix": 10**-6,
903+
}
904+
905+
906+
@skip_if_missing
907+
def cmn_mem_remote_bw_MBps(grouped_df):
908+
"""Remote mesh MC-request bandwidth (est) — SN-filter requests routed to
909+
another mesh instance's memory controllers (cross-mesh / cross-node)."""
910+
mc_reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_remote_sn")
911+
dur = get_duration_series(grouped_df.get_group("instructions"))
912+
mc_reqs.index = dur.index
913+
bw_series = (mc_reqs * 64).div(dur)
914+
return {
915+
"name": "CMN Remote MC-Req Bandwidth (MBps, est)",
916+
"series": bw_series,
917+
"prefix": 10**-6,
918+
}
919+
920+
921+
@skip_if_missing
922+
def cmn_mem_read_pct(grouped_df):
923+
"""Approximate read share of memory traffic from HN-S PoCQ occupancy.
924+
925+
The HN-S has no read/write split on MC requests, but the QoS PoCQ
926+
occupancy counters (read vs write) track how long read- vs write-class
927+
requests sit in the point-of-coherency queue, giving a usable read/write
928+
mix proxy. Reported as the read fraction of (read + write) occupancy.
929+
"""
930+
read_occ = _sum_cmn_event(grouped_df, "hns_qos_pocq_occupancy_read")
931+
write_occ = _sum_cmn_event(grouped_df, "hns_qos_pocq_occupancy_write")
932+
write_occ.index = read_occ.index
933+
total = read_occ + write_occ
934+
return {
935+
"name": "CMN Memory Read Mix %",
936+
"series": read_occ.div(total),
937+
"prefix": 100,
938+
}
939+
940+
941+
@skip_if_missing
942+
def cmn_mc_retry_pct(grouped_df):
943+
"""Memory-controller retry rate — retried MC requests / total MC requests.
944+
945+
Maps to the HN-S "mc_retry" metric. A high retry rate indicates
946+
the memory controller is backpressuring the mesh (DRAM-bound).
947+
"""
948+
retries = _sum_cmn_event(grouped_df, "hns_mc_retries_local_all")
949+
try:
950+
retries = retries + _sum_cmn_event(grouped_df, "hns_mc_retries_remote_all")
951+
except KeyError:
952+
pass
953+
reqs = _sum_cmn_event(grouped_df, "hns_mc_reqs_local_all")
954+
try:
955+
reqs = reqs + _sum_cmn_event(grouped_df, "hns_mc_reqs_remote_all")
956+
except KeyError:
957+
pass
958+
retries.index = reqs.index
959+
return {
960+
"name": "CMN MC Retry %",
961+
"series": retries.div(reqs),
962+
"prefix": 100,
963+
}
964+
965+
966+
@skip_if_missing
967+
def cmn_sf_hit_rate(grouped_df):
968+
"""Snoop-filter hit rate — hns_sf_hit_all / hns_slc_sf_cache_access_all.
969+
970+
Maps to the HN-S "sf_hit_ratio". Complements the existing SLC
971+
(L3) hit-rate metric with coherence-directory effectiveness.
972+
"""
973+
hit_s = _sum_cmn_event(grouped_df, "hns_sf_hit_all")
974+
access_s = _sum_cmn_event(grouped_df, "hns_slc_sf_cache_access_all")
975+
hit_s.index = access_s.index
976+
return {
977+
"name": "CMN Snoop Filter Hit Rate %",
978+
"series": hit_s.div(access_s),
979+
"prefix": 100,
980+
}
981+
982+
983+
@skip_if_missing
984+
def cmn_mesh_freq_ghz(grouped_df):
985+
"""Mesh clock frequency (GHz) derived from the DTC cycle counter.
986+
987+
dtc_cycles increments at the mesh clock; dividing by the wall-clock
988+
sample duration recovers the effective mesh frequency. Useful for
989+
detecting mesh DVFS throttling under load.
990+
"""
991+
cyc = _sum_cmn_event(grouped_df, "dtc_cycles")
992+
# dtc_cycles is summed across mesh instances; use the per-instance average.
993+
n_cmn = 0
994+
for name, _ in grouped_df:
995+
if isinstance(name, str) and name.endswith("/dtc_cycles/"):
996+
n_cmn += 1
997+
dur = get_duration_series(grouped_df.get_group("instructions"))
998+
cyc.index = dur.index
999+
if n_cmn > 1:
1000+
cyc = cyc / n_cmn
1001+
freq = cyc.div(dur) # cycles per second
1002+
return {
1003+
"name": "CMN Mesh Frequency (GHz)",
1004+
"series": freq,
1005+
"prefix": 10**-9,
1006+
}
1007+
1008+
1009+
# ===========================================================================
1010+
# SVE predication effectiveness (V3-specific)
1011+
# ===========================================================================
1012+
1013+
8131014
@skip_if_missing
8141015
def sve_pred_empty_pct(grouped_df):
8151016
"""SVE predicated ops with no active lanes (wasted work)."""
@@ -946,8 +1147,15 @@ def main(
9461147
sve_pred_empty_pct(grouped_df),
9471148
sve_pred_full_pct(grouped_df),
9481149
sve_pred_partial_pct(grouped_df),
949-
# --- CMN uncore (SLC / memory bandwidth) ---
950-
cmn_mem_read_bw_MBps(grouped_df),
1150+
# --- CMN mesh (uncore) metrics ---
1151+
dmc_mem_bw_MBps(grouped_df),
1152+
cmn_mem_bw_MBps(grouped_df),
1153+
cmn_mem_local_bw_MBps(grouped_df),
1154+
cmn_mem_remote_bw_MBps(grouped_df),
1155+
cmn_mem_read_pct(grouped_df),
1156+
cmn_mc_retry_pct(grouped_df),
1157+
cmn_sf_hit_rate(grouped_df),
1158+
cmn_mesh_freq_ghz(grouped_df),
9511159
]
9521160

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

0 commit comments

Comments
 (0)