mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
perf rig: mq65 device profile; EE-thread @THREADPERF@ primary metric in codegen_ab
- devices/mq65.env: MANGMI Air X (Kryo 260: cpu4-7 Gold/A73 part 0x800, cpu0-3 Silver/A53 part 0x801 — note 0x800 is the BIG cluster), 4K pages, ARMv8.0 no-LSE, no fan; the primary density gate for EE codegen work. - codegen_ab.sh: parse @THREADPERF@ "CPU Thread" instructions/cycles as the PRIMARY metric (EE-scoped, undiluted by GS/MTVU, excludes boot; max across same-named threads). Whole-process rows kept as secondary. New fast-fail: a missing @THREADCPU@ line means the VM never booted (GS init failure) — startup alone can retire billions of insns and sail past the 1e8 floor, which is exactly what the first mq65 run did. - scenes: fall back .iso -> .chd (mq65 carries CHD rips). mq65 self-vs-self sanity (uya-gameplay, 600 frames, runs=2): EE-thread insns spread 0.008%, delta -0.01% insns / -0.06% cycles — noise floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
76dd819f70
commit
a1b7129826
+61
-21
@@ -135,6 +135,16 @@ run_one() {
|
||||
# secondary metric keeps effect sizes honest. (2026-07-06 audit.)
|
||||
local ee_s
|
||||
ee_s="$(awk -F': ' '/@THREADCPU@ CPU Thread:/ { gsub(/ s$/,"",$2); if ($2+0 > m) m=$2+0 } END { printf "%.2f", m }' "$runlog")"
|
||||
# EE-thread-SCOPED hardware counters from the runner's @THREADPERF@ lines
|
||||
# (per-tid perf_event_open over the frame window). THE primary metric since
|
||||
# 2026-07-12: deterministic like process insns, but undiluted by GS/MTVU
|
||||
# threads and excluding boot/savestate-load work. Zero when unsupported
|
||||
# (old binary / perf_event_paranoid) — summary falls back to process-wide.
|
||||
# (max across same-named threads: spawned helpers can inherit the "CPU Thread"
|
||||
# comm; the EE core is always the one with the most retired instructions)
|
||||
local ee_ins ee_cyc
|
||||
ee_ins="$(awk '/@THREADPERF@ CPU Thread:/ { for (i=1;i<=NF;i++) if ($i ~ /^instructions=/) { v=$i; sub(/^instructions=/,"",v); if (v+0 > m) m=v+0 } } END { printf "%.0f", m }' "$runlog")"
|
||||
ee_cyc="$(awk '/@THREADPERF@ CPU Thread:/ { for (i=1;i<=NF;i++) if ($i ~ /^cycles=/) { v=$i; sub(/^cycles=/,"",v); if (v+0 > m) m=v+0 } } END { printf "%.0f", m }' "$runlog")"
|
||||
# perf stat writes counts to stderr. Anchor on the EVENT token ($2), so the
|
||||
# "insn per cycle" comment on the instructions line can't be misread as cycles.
|
||||
# On multi-PMU boxes (Apple M2: apple_avalanche_pmu/instructions/u +
|
||||
@@ -158,9 +168,19 @@ run_one() {
|
||||
tail -n 20 "$stat" >&2 || true
|
||||
die "aborting: $tag run$k did not run a real workload"
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "$tag" "$k" "$ins" "$cyc" "${ee_s:-0}" >> "$RESULTS"
|
||||
# A missing @THREADCPU@ line means the VM never actually ran (e.g. GS init
|
||||
# failure: renderer rejected over ssh) — startup alone can retire billions of
|
||||
# instructions on a slow core, sailing past the 1e8 floor while measuring
|
||||
# nothing. The shutdown thread-CPU report only prints after a real run.
|
||||
if ! awk "BEGIN{exit !(${ee_s:-0} > 0)}"; then
|
||||
echo " $tag run$k: FAST-FAIL (no @THREADCPU@ EE-thread line — VM never ran). Check $runlog." >&2
|
||||
tail -n 15 "$runlog" >&2 || true
|
||||
die "aborting: $tag run$k did not boot the VM (GS init failure?)"
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$tag" "$k" "$ins" "$cyc" "${ee_s:-0}" "${ee_ins:-0}" "${ee_cyc:-0}" >> "$RESULTS"
|
||||
awk -v t="$tag" -v k="$k" -v i="$ins" -v c="$cyc" -v tc="$temp_c" -v e="${ee_s:-?}" \
|
||||
'BEGIN{printf " %-4s run%s: insns=%.3fB cycles=%.3fB ipc=%.3f ee_thread=%ss temp=%s°C\n", t, k, i/1e9, c/1e9, i/c, e, tc}'
|
||||
-v ei="${ee_ins:-0}" -v ec="${ee_cyc:-0}" \
|
||||
'BEGIN{printf " %-4s run%s: EE insns=%.3fB cycles=%.3fB | proc insns=%.3fB cycles=%.3fB | ee_thread=%ss temp=%s°C\n", t, k, ei/1e9, ec/1e9, i/1e9, c/1e9, e, tc}'
|
||||
}
|
||||
|
||||
# Interleaved run order (base,new,base,new,...) — see run_one for why.
|
||||
@@ -173,23 +193,31 @@ done
|
||||
python3 - "$OUTDIR" "$RESULTS" "$LABEL" "$SCENE" "$DEVICE" "$RENDERER" "$FRAMES" "$RUNS" "$BASE_BIN" "$NEW_BIN" <<'PY'
|
||||
import statistics, sys, os
|
||||
outdir, results, label, scene, device, rend, frames, runs, base_bin, new_bin = sys.argv[1:11]
|
||||
rows = {"base": {"ins": [], "cyc": [], "ee": []}, "new": {"ins": [], "cyc": [], "ee": []}}
|
||||
rows = {"base": {"ins": [], "cyc": [], "ee": [], "eins": [], "ecyc": []},
|
||||
"new": {"ins": [], "cyc": [], "ee": [], "eins": [], "ecyc": []}}
|
||||
for line in open(results):
|
||||
parts = line.rstrip("\n").split("\t")
|
||||
if len(parts) not in (4, 5): continue
|
||||
if len(parts) not in (4, 5, 7): continue
|
||||
tag, _k, ins, cyc = parts[:4]
|
||||
if tag in rows:
|
||||
rows[tag]["ins"].append(int(ins)); rows[tag]["cyc"].append(int(cyc))
|
||||
if len(parts) == 5 and float(parts[4]) > 0:
|
||||
if len(parts) >= 5 and float(parts[4]) > 0:
|
||||
rows[tag]["ee"].append(float(parts[4]))
|
||||
if len(parts) == 7:
|
||||
if int(parts[5]) > 0: rows[tag]["eins"].append(int(parts[5]))
|
||||
if int(parts[6]) > 0: rows[tag]["ecyc"].append(int(parts[6]))
|
||||
|
||||
def med(xs): return statistics.median(xs) if xs else float("nan")
|
||||
def delta(b, n): return (n / b - 1) * 100 if b == b and n == n and b else float("nan")
|
||||
b_ins, b_cyc = med(rows["base"]["ins"]), med(rows["base"]["cyc"])
|
||||
n_ins, n_cyc = med(rows["new"]["ins"]), med(rows["new"]["cyc"])
|
||||
d_ins = (n_ins / b_ins - 1) * 100 if b_ins else float("nan")
|
||||
d_cyc = (n_cyc / b_cyc - 1) * 100 if b_cyc else float("nan")
|
||||
d_ins, d_cyc = delta(b_ins, n_ins), delta(b_cyc, n_cyc)
|
||||
b_ipc = b_ins / b_cyc if b_cyc else float("nan")
|
||||
n_ipc = n_ins / n_cyc if n_cyc else float("nan")
|
||||
be_ins, be_cyc = med(rows["base"]["eins"]), med(rows["base"]["ecyc"])
|
||||
ne_ins, ne_cyc = med(rows["new"]["eins"]), med(rows["new"]["ecyc"])
|
||||
de_ins, de_cyc = delta(be_ins, ne_ins), delta(be_cyc, ne_cyc)
|
||||
have_ee_hw = be_ins == be_ins and ne_ins == ne_ins # NaN-safe
|
||||
|
||||
# Per-tag run spread (max-min)/median, to flag if determinism slipped.
|
||||
def spread(xs): return (max(xs) - min(xs)) / statistics.median(xs) * 100 if xs else float("nan")
|
||||
@@ -198,28 +226,40 @@ out = os.path.join(outdir, "summary.md")
|
||||
with open(out, "w") as o:
|
||||
o.write(f"# Codegen A/B — {label} · {scene} (renderer={rend})\n\n")
|
||||
o.write(f"- device: **{device}** · runs: {runs} · frames: {frames}\n")
|
||||
o.write(f"- metric: **retired instructions** (deterministic; `perf stat -e instructions,cycles --inherit`)\n")
|
||||
o.write(f"- base: `{base_bin}`\n- new: `{new_bin}`\n\n")
|
||||
o.write("| | base (median) | new (median) | Δ |\n|---|---|---|---|\n")
|
||||
o.write(f"| instructions | {b_ins/1e9:.3f}B | {n_ins/1e9:.3f}B | **{d_ins:+.2f}%** |\n")
|
||||
o.write(f"| cycles | {b_cyc/1e9:.3f}B | {n_cyc/1e9:.3f}B | **{d_cyc:+.2f}%** |\n")
|
||||
if have_ee_hw:
|
||||
o.write("**Primary metric: EE-thread-scoped retired instructions** (@THREADPERF@ per-tid "
|
||||
"perf_event_open over the frame window — undiluted by GS/MTVU threads, excludes boot).\n\n")
|
||||
o.write("| EE thread | base (median) | new (median) | Δ |\n|---|---|---|---|\n")
|
||||
o.write(f"| instructions | {be_ins/1e9:.3f}B | {ne_ins/1e9:.3f}B | **{de_ins:+.2f}%** |\n")
|
||||
o.write(f"| cycles | {be_cyc/1e9:.3f}B | {ne_cyc/1e9:.3f}B | **{de_cyc:+.2f}%** |\n")
|
||||
o.write(f"| IPC | {be_ins/be_cyc:.3f} | {ne_ins/ne_cyc:.3f} | {delta(be_ins/be_cyc, ne_ins/ne_cyc):+.2f}% |\n\n")
|
||||
o.write("| whole process | base (median) | new (median) | Δ |\n|---|---|---|---|\n")
|
||||
o.write(f"| instructions | {b_ins/1e9:.3f}B | {n_ins/1e9:.3f}B | {d_ins:+.2f}% |\n")
|
||||
o.write(f"| cycles | {b_cyc/1e9:.3f}B | {n_cyc/1e9:.3f}B | {d_cyc:+.2f}% |\n")
|
||||
o.write(f"| IPC | {b_ipc:.3f} | {n_ipc:.3f} | {(n_ipc/b_ipc-1)*100:+.2f}% |\n")
|
||||
b_ee, n_ee = med(rows["base"]["ee"]), med(rows["new"]["ee"])
|
||||
if b_ee == b_ee and n_ee == n_ee and b_ee: # NaN-safe
|
||||
d_ee = (n_ee / b_ee - 1) * 100
|
||||
o.write(f"| EE-thread CPU s | {b_ee:.2f} | {n_ee:.2f} | **{d_ee:+.2f}%** |\n")
|
||||
o.write(f"\n- ⚠ the instructions/cycles rows are WHOLE-PROCESS (GS+MTVU+workers included): "
|
||||
f"an EE-only codegen change is diluted by the EE thread's share of process cycles. "
|
||||
f"The EE-thread row is time-based (noisier, clock-sensitive — pinned clocks only) "
|
||||
f"but scoped to the treated thread.\n")
|
||||
o.write(f"| EE-thread CPU s | {b_ee:.2f} | {n_ee:.2f} | {d_ee:+.2f}% |\n")
|
||||
if not have_ee_hw:
|
||||
o.write(f"\n- ⚠ no @THREADPERF@ EE-thread counters in this run (old binary or perf_event_paranoid): "
|
||||
f"the whole-process rows above are the only hardware counts, and an EE-only codegen change "
|
||||
f"is diluted by the GS/MTVU threads' share of process cycles.\n")
|
||||
o.write("\n")
|
||||
o.write(f"- run spread (max−min)/median: base insns {spread(rows['base']['ins']):.3f}%, "
|
||||
f"new insns {spread(rows['new']['ins']):.3f}% "
|
||||
sp = rows['base']['eins'] if have_ee_hw else rows['base']['ins']
|
||||
sn = rows['new']['eins'] if have_ee_hw else rows['new']['ins']
|
||||
o.write(f"- run spread (max−min)/median on the primary insns metric: base {spread(sp):.3f}%, "
|
||||
f"new {spread(sn):.3f}% "
|
||||
f"(want <0.1% — larger means determinism slipped; raise --runs or pin harder).\n")
|
||||
verdict = ("density-only (cycles absorbed by OoO)" if abs(d_cyc) + 0.3 < abs(d_ins)
|
||||
pi, pc = (de_ins, de_cyc) if have_ee_hw else (d_ins, d_cyc)
|
||||
verdict = ("density-only (cycles absorbed by OoO)" if abs(pc) + 0.3 < abs(pi)
|
||||
else "real cycle effect")
|
||||
o.write(f"- read: |Δinsns|={abs(d_ins):.2f}% vs |Δcycles|={abs(d_cyc):.2f}% → **{verdict}**.\n")
|
||||
o.write(f"- read: |Δinsns|={abs(pi):.2f}% vs |Δcycles|={abs(pc):.2f}% → **{verdict}**.\n")
|
||||
print(f"wrote {out}")
|
||||
print(f" instructions: {d_ins:+.2f}% cycles: {d_cyc:+.2f}% (base IPC {b_ipc:.3f} -> new {n_ipc:.3f})")
|
||||
if have_ee_hw:
|
||||
print(f" EE-thread: insns {de_ins:+.2f}% cycles {de_cyc:+.2f}% (proc: insns {d_ins:+.2f}%, cycles {d_cyc:+.2f}%)")
|
||||
else:
|
||||
print(f" instructions: {d_ins:+.2f}% cycles: {d_cyc:+.2f}% (base IPC {b_ipc:.3f} -> new {n_ipc:.3f})")
|
||||
PY
|
||||
echo "done. summary: $OUTDIR/summary.md"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Device profile: MANGMI Air X "MQ65" — SM6115-class SoC running ROCKNIX
|
||||
# (Kryo 260: 2-wide in-order-ish A73 golds + A53 silvers, ARMv8.0 NO-LSE, 4K pages,
|
||||
# Adreno 610/turnip). Sourced by codegen_ab.sh / profile_run.sh ON the device.
|
||||
#
|
||||
# This is the PRIMARY DENSITY GATE for EE-JIT codegen work (GE campaign): the 2-wide
|
||||
# A73 pays for emitted-code volume that the SD865's OoO A77 absorbs (MQ65 baseline
|
||||
# 2026-07-12: JIT-emitted +37% cycles/frame vs aether — scratchpad/mq65-aethersx2-
|
||||
# baseline-2026-07-12/NOTES.md). SD865 stays the throughput/regression gate.
|
||||
#
|
||||
# Topology (verified via /proc/cpuinfo + cpufreq policies, 2026-07-12):
|
||||
# cpu0-3 = Kryo 260 Silver, A53-class (CPU part 0x801), policy0, max 2016 MHz
|
||||
# cpu4-7 = Kryo 260 Gold, A73-class (CPU part 0x800), policy4, max 2112 MHz
|
||||
# NOTE the part-id direction: 0x800 = Gold/A73 (the BIG cores, 4-7) — an earlier
|
||||
# memory note had this backwards. Pin to the golds.
|
||||
#
|
||||
# Per-session protocol (no fan on this device — interleaving is the thermal control;
|
||||
# codegen_ab logs SoC temp per run so a ramp is visible):
|
||||
# pin : for p in /sys/devices/system/cpu/cpufreq/policy{0,4}; do echo performance > $p/scaling_governor; done
|
||||
# restore: ... echo ondemand > $p/scaling_governor ...
|
||||
#
|
||||
# Binary provenance: build-rocknix on the M2 (Release -O3 -g, global IPO,
|
||||
# -march=armv8-a -moutline-atomics — MANDATORY: v8.0 has no LSE and Fedora-host
|
||||
# libs that inline LSE atomics SIGILL here; HOST_PAGE_SIZE=4096 for the 4K kernel).
|
||||
# ROMs on this device are .chd (scene envs fall back .iso -> .chd).
|
||||
#
|
||||
# ⚠ turnip-over-ssh reports Vulkan 1.0 -> GS init fails from scripted launches.
|
||||
# Irrelevant for codegen_ab (--renderer null, no GS init); do NOT try --renderer vk
|
||||
# over ssh on this device — live GS runs need the user at the device.
|
||||
|
||||
export HOME=/storage/pcsx2
|
||||
export LD_LIBRARY_PATH=/storage/pcsx2/lib
|
||||
|
||||
# Runner binaries (4K-page armv8.0 build).
|
||||
BIN_DIR="/storage/pcsx2/bin"
|
||||
|
||||
# perf sampling frequency (Hz) for profile_run.sh. 999 ~ avoids vblank harmonics.
|
||||
FREQ_DEFAULT=999
|
||||
|
||||
# Single standard ARM PMU — plain "cycles" is correct.
|
||||
CYCLES_EVENT="cycles"
|
||||
|
||||
# Pin the emulator to the A73 gold cores (see topology note above).
|
||||
PIN="taskset -c 4-7"
|
||||
|
||||
# ISOs/CHDs on the device SD card.
|
||||
export ISO_ROOT="/storage/roms/ps2"
|
||||
@@ -4,6 +4,7 @@
|
||||
LABEL="SotC — slot 01"
|
||||
ISO_ROOT="${ISO_ROOT:-/home/bmd/dev/ps2/My Sony PlayStation 2 (USA) Collection}"
|
||||
ISO="$ISO_ROOT/Shadow of the Colossus (USA).iso"
|
||||
[[ -f "$ISO" ]] || ISO="${ISO%.iso}.chd" # some devices (mq65) carry CHD rips
|
||||
SAVESTATE="$HOME/.config/YAPS2/sstates/SCUS-97472 (C19A374E).01.p2s"
|
||||
FRAMES=600 # same N every run
|
||||
RENDERERS="vk null" # vk = whole-system (M2 GPU-driver heavy); null = CPU-emulation shape
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
LABEL="R&C UYA — worst gameplay"
|
||||
ISO_ROOT="${ISO_ROOT:-/home/bmd/dev/ps2/My Sony PlayStation 2 (USA) Collection}"
|
||||
ISO="$ISO_ROOT/Ratchet & Clank - Up Your Arsenal (USA) (En,Fr,Es).iso"
|
||||
[[ -f "$ISO" ]] || ISO="${ISO%.iso}.chd" # some devices (mq65) carry CHD rips
|
||||
# slot 02 = in-game combat scene, user-confirmed representative (2026-07-05).
|
||||
SAVESTATE="$HOME/.config/YAPS2/sstates/SCUS-97353 (45FE0CC4).02.p2s"
|
||||
FRAMES=600 # same N every run
|
||||
|
||||
Reference in New Issue
Block a user