mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
FX-15: SD865 iTLB A/B — mechanism confirmed, throughput neutral, kept
ITLB_WALK -15% to -35% and L1I_TLB_REFILL -4% to -59% across 4 titles, but cycles/wall flat (A77 baseline iTLB pressure already negligible; 1280-entry L2 TLB covers the live JIT footprint in 4K pages). THP madvise stays default-ON: zero cost, no regression, in-order A53 upside. Adds the reusable A/B harness (env-toggle arms + in-run smaps THP probe). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b9d708f59c
commit
bcb8edaa9f
@@ -154,8 +154,30 @@ Raw logs: `fx-telem-logs/*.emulog.txt` (session scratchpad).
|
||||
dirty subset, not worth an ABI bump. Footnote: VI is the only churning
|
||||
channel (5–7× VF) — future mVU perf work should look at VI pool sizing,
|
||||
not eviction policy.
|
||||
- **FX-15 → pending iTLB A/B** (same binary, `YAPS2_NO_THP=1` toggle,
|
||||
fan pinned).
|
||||
- **FX-15 → applied, kept default-ON** (faaf117bf). SD865 iTLB A/B
|
||||
(fx15-ab-1: 4 titles × 4 ABBA rounds, same eerunner, `YAPS2_NO_THP=1`
|
||||
toggle, fan pinned, `tools/perf/fx15_thp_ab.sh` + `fx15_thp_report.py`):
|
||||
|
||||
| title | ITLB_WALK | L1I_TLB_REFILL | cycles | wall |
|
||||
|---|---|---|---|---|
|
||||
| SotC | −35.4% | −8.7% | +0.10% | +0.36% |
|
||||
| Katamari | −24.9% | −33.8% | +0.08% | +0.00% |
|
||||
| OutRun | −22.5% | −4.3% | −0.46% | −0.39% |
|
||||
| UYA | −14.6% | −58.9% | +0.11% | +1.09% |
|
||||
|
||||
Mechanism confirmed, throughput neutral: baseline A77 iTLB pressure is
|
||||
already negligible (ITLB_WALK 0.008–0.025/kinsn — one walk per 40k–125k
|
||||
insns; the 1280-entry L2 TLB covers the ~8MB live JIT footprint even in
|
||||
4K pages) and STALL_FRONTEND is unchanged, so frontend stalls are
|
||||
icache/branch, not TLB. Kept: zero cost, no regression, and the
|
||||
in-order A53 (RK3562, far smaller TLBs) should benefit more — optional
|
||||
RK wallclock A/B if that device returns to rotation. THP probes: 8 MB
|
||||
of hugepages materialized per title, exactly in the two madvised rec
|
||||
ranges (EE+IOP at 0x111d00000, mVU0+1 at 0x118d00000).
|
||||
|
||||
Campaign CLOSED 2026-07-12. Remaining FX queue: FX-05 (mVU digest DX),
|
||||
FX-14/FX-09 (someday). Device left on the clean (census-off) binary;
|
||||
build-rocknix flags reverted.
|
||||
|
||||
## Smoke result (SotC gameplay savestate, 75s, 2026-07-11)
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
# fx15_thp_ab.sh — interleaved A/B for FX-15 (MADV_HUGEPAGE on the JIT code caches).
|
||||
#
|
||||
# ONE pcsx2-eerunner binary, two arms: A = YAPS2_NO_THP=1 (4K pages, baseline),
|
||||
# B = default (THP madvise on). Same savestate-anchored --liverun workloads as
|
||||
# callret_ab.sh, ABBA-interleaved so thermal/clock drift cancels, perf stat
|
||||
# around each run. Throughput truth = wall seconds per N frames (unlimited
|
||||
# limiter, headless --renderer null); mechanism check = ITLB_WALK (r35) and
|
||||
# L1I_TLB_REFILL (r02) per kinsn.
|
||||
#
|
||||
# During the first B round of each game a background probe snapshots
|
||||
# AnonHugePages from /proc/<pid>/smaps for the JIT arena (VA >= 0x100000000)
|
||||
# into <out>/<tag>.thp — if those are all zero the A/B is null by construction
|
||||
# (madvise didn't take) and the numbers mean nothing.
|
||||
#
|
||||
# Usage:
|
||||
# fx15_thp_ab.sh -b <eerunner> -g <games.cfg> [-f frames] [-r rounds]
|
||||
# [-c cpulist] [-o outdir]
|
||||
# games.cfg lines: name|savestate|iso
|
||||
#
|
||||
# Device notes (SD865/Rocknix): run with -c 4-7 (A77 cluster). Fan pinned to
|
||||
# max and RESTORED to auto on exit. EERUNNER_SYNCMTGS=0 always (bare --liverun
|
||||
# wedges at frame 1 without it).
|
||||
set -u
|
||||
|
||||
FRAMES=2000
|
||||
ROUNDS=4
|
||||
CPUS=""
|
||||
OUT="fx15-thp-ab-$(date +%m%d-%H%M%S)"
|
||||
BIN="" GAMES=""
|
||||
|
||||
while getopts "b:g:f:r:c:o:" opt; do
|
||||
case $opt in
|
||||
b) BIN=$OPTARG ;;
|
||||
g) GAMES=$OPTARG ;;
|
||||
f) FRAMES=$OPTARG ;;
|
||||
r) ROUNDS=$OPTARG ;;
|
||||
c) CPUS=$OPTARG ;;
|
||||
o) OUT=$OPTARG ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -x "$BIN" ] && [ -f "$GAMES" ] || {
|
||||
echo "usage: $0 -b <eerunner> -g <games.cfg> [-f frames] [-r rounds] [-c cpus] [-o outdir]" >&2
|
||||
exit 2
|
||||
}
|
||||
mkdir -p "$OUT"
|
||||
|
||||
# --- device pinning (no-op off-device) --------------------------------------
|
||||
FAN=""
|
||||
restore() {
|
||||
if [ -n "$FAN" ]; then
|
||||
echo 2 > "$FAN/pwm1_enable" 2>/dev/null
|
||||
echo "fan restored to auto ($FAN)"
|
||||
fi
|
||||
}
|
||||
trap restore EXIT
|
||||
for h in /sys/class/hwmon/hwmon*; do
|
||||
[ -f "$h/name" ] && [ "$(cat "$h/name" 2>/dev/null)" = "pwmfan" ] || continue
|
||||
FAN=$h
|
||||
echo 1 > "$FAN/pwm1_enable" && echo 255 > "$FAN/pwm1"
|
||||
echo "fan pinned to max ($FAN)"
|
||||
done
|
||||
for g in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
|
||||
echo performance > "$g" 2>/dev/null
|
||||
done
|
||||
echo "global THP mode: $(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null)" | tee "$OUT/thp-mode.txt"
|
||||
|
||||
# Rocknix device: the eerunner binaries need the bundled libs (libjpeg/lz4).
|
||||
[ -d /storage/pcsx2/lib ] && export LD_LIBRARY_PATH=/storage/pcsx2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
|
||||
|
||||
# TLB event set (A77: 6 programmable + fixed cycles, no multiplexing):
|
||||
# r02 L1I_TLB_REFILL, r35 ITLB_WALK, r34 DTLB_WALK, r23 STALL_FRONTEND
|
||||
EVENTS="cycles,instructions,r02,r35,r34,r23"
|
||||
TASKSET=""
|
||||
[ -n "$CPUS" ] && TASKSET="taskset -c $CPUS"
|
||||
|
||||
probe_thp() { # $1=outfile — sample JIT-arena AnonHugePages of the live run
|
||||
sleep 25
|
||||
local pid
|
||||
pid=$(pgrep -n -f pcsx2-eerunner)
|
||||
[ -n "$pid" ] || { echo "no pid" > "$1"; return; }
|
||||
# JIT arena = 0x1_00000000..: 9-hex-digit VMA start beginning with 1.
|
||||
# (busybox awk: no strtonum)
|
||||
awk '/^[0-9a-f]+-[0-9a-f]+ /{vma=$1; keep=(index($1,"-")==10 && substr($1,1,1)=="1")}
|
||||
/^AnonHugePages:/{if (keep && $2 > 0) print vma, $0}' \
|
||||
"/proc/$pid/smaps" > "$1" 2>/dev/null
|
||||
echo "total-kB $(awk '{s+=$3} END{print s+0}' "$1")" >> "$1"
|
||||
}
|
||||
|
||||
run_one() { # $1=arm(A|B) $2=name $3=state $4=iso $5=round
|
||||
local tag="$2-$1-r$5" envA=""
|
||||
[ "$1" = A ] && envA="YAPS2_NO_THP=1"
|
||||
echo "=== $tag ($(date +%H:%M:%S)) ==="
|
||||
if [ "$1" = B ] && [ "$5" = 1 ]; then
|
||||
probe_thp "$OUT/$tag.thp" &
|
||||
fi
|
||||
env $envA EERUNNER_SYNCMTGS=0 $TASKSET perf stat -e "$EVENTS" \
|
||||
-o "$OUT/$tag.stat" -- \
|
||||
"$BIN" --liverun --renderer null --savestate "$3" --iso "$4" \
|
||||
--frames "$FRAMES" > "$OUT/$tag.log" 2>&1
|
||||
echo "exit=$? $(grep -o 'PerfLog session:.*' "$OUT/$tag.log" | tail -1)"
|
||||
sleep 5 # cool-down between runs
|
||||
}
|
||||
|
||||
for round in $(seq 1 "$ROUNDS"); do
|
||||
# ABBA: odd rounds run A first, even rounds B first.
|
||||
if [ $((round % 2)) -eq 1 ]; then order="A B"; else order="B A"; fi
|
||||
for arm in $order; do
|
||||
while IFS='|' read -r name state iso; do
|
||||
case $name in ''|\#*) continue ;; esac
|
||||
run_one "$arm" "$name" "$state" "$iso" "$round"
|
||||
done < "$GAMES"
|
||||
done
|
||||
done
|
||||
wait
|
||||
echo "done — stats in $OUT/*.stat, THP probes in $OUT/*-B-r1.thp"
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
# fx15_thp_report.py — summarize an fx15_thp_ab.sh output dir.
|
||||
#
|
||||
# Per (game, arm): mean/stdev of wall (PerfLog session line — the throughput
|
||||
# truth), cycles, instructions, IPC, and the TLB mechanism events per kinsn
|
||||
# (r02 L1I_TLB_REFILL, r35 ITLB_WALK, r34 DTLB_WALK, r23 STALL_FRONTEND).
|
||||
# Then B (THP) vs A (YAPS2_NO_THP=1) deltas. Counters are whole-process;
|
||||
# ABBA interleave + identical boot work make the delta meaningful.
|
||||
#
|
||||
# usage: fx15_thp_report.py <outdir>
|
||||
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
EVENTS = ["cycles", "instructions", "r02", "r35", "r34", "r23"]
|
||||
EVENT_LABEL = {
|
||||
"r02": "L1I_TLB_REFILL",
|
||||
"r35": "ITLB_WALK",
|
||||
"r34": "DTLB_WALK",
|
||||
"r23": "STALL_FRONTEND",
|
||||
}
|
||||
|
||||
|
||||
def parse_stat(path):
|
||||
vals = {}
|
||||
for line in path.read_text().splitlines():
|
||||
m = re.match(r"\s*([\d,]+)\s+(\S+)", line)
|
||||
if m and m.group(2) in EVENTS:
|
||||
vals[m.group(2)] = int(m.group(1).replace(",", ""))
|
||||
m = re.match(r"\s*([\d.]+)\s+seconds time elapsed", line)
|
||||
if m:
|
||||
vals["elapsed"] = float(m.group(1))
|
||||
return vals
|
||||
|
||||
|
||||
def parse_wall(logpath):
|
||||
if not logpath.exists():
|
||||
return None
|
||||
m = None
|
||||
for line in logpath.read_text(errors="replace").splitlines():
|
||||
mm = re.search(r"PerfLog session: (\d+) frames in ([\d.]+)s wall", line)
|
||||
if mm:
|
||||
m = float(mm.group(2))
|
||||
return m
|
||||
|
||||
|
||||
def main():
|
||||
outdir = Path(sys.argv[1])
|
||||
runs = defaultdict(list) # (game, arm) -> [dict]
|
||||
for stat in sorted(outdir.glob("*.stat")):
|
||||
m = re.match(r"(.+)-([AB])-r(\d+)\.stat", stat.name)
|
||||
if not m:
|
||||
continue
|
||||
game, arm = m.group(1), m.group(2)
|
||||
vals = parse_stat(stat)
|
||||
wall = parse_wall(stat.with_suffix(".log"))
|
||||
if wall is not None:
|
||||
vals["wall"] = wall
|
||||
if "cycles" in vals:
|
||||
runs[(game, arm)].append(vals)
|
||||
|
||||
games = sorted({g for g, _ in runs})
|
||||
for game in games:
|
||||
print(f"\n=== {game} ===")
|
||||
agg = {}
|
||||
for arm in "AB":
|
||||
rr = runs.get((game, arm), [])
|
||||
if not rr:
|
||||
continue
|
||||
a = {}
|
||||
for k in ["wall", "elapsed", *EVENTS]:
|
||||
xs = [r[k] for r in rr if k in r]
|
||||
if xs:
|
||||
a[k] = (statistics.mean(xs),
|
||||
statistics.stdev(xs) if len(xs) > 1 else 0.0)
|
||||
agg[arm] = a
|
||||
label = "A thp-off" if arm == "A" else "B thp-on "
|
||||
ki = a["instructions"][0] / 1e3
|
||||
per_ki = " ".join(
|
||||
f"{EVENT_LABEL[e]}={a[e][0] / ki:.3f}"
|
||||
for e in ["r02", "r35", "r34"] if e in a)
|
||||
stallpct = 100.0 * a["r23"][0] / a["cycles"][0] if "r23" in a else 0
|
||||
wall = f"wall={a['wall'][0]:.2f}s±{a['wall'][1]:.2f}" if "wall" in a else ""
|
||||
print(f" {label} n={len(rr)} {wall} cycles={a['cycles'][0]/1e9:.3f}G "
|
||||
f"IPC={a['instructions'][0]/a['cycles'][0]:.3f} "
|
||||
f"| per-kinsn: {per_ki} | stall_fe={stallpct:.1f}%cyc")
|
||||
if "A" in agg and "B" in agg:
|
||||
def delta(k):
|
||||
return 100.0 * (agg["B"][k][0] - agg["A"][k][0]) / agg["A"][k][0]
|
||||
parts = [f"wall {delta('wall'):+.2f}%"] if "wall" in agg["A"] and "wall" in agg["B"] else []
|
||||
parts += [f"cycles {delta('cycles'):+.2f}%"]
|
||||
parts += [f"{EVENT_LABEL[e]} {delta(e):+.1f}%"
|
||||
for e in ["r02", "r35", "r34", "r23"]
|
||||
if e in agg["A"] and e in agg["B"]]
|
||||
print(f" B-vs-A: {' '.join(parts)}")
|
||||
|
||||
thps = sorted(outdir.glob("*.thp"))
|
||||
if thps:
|
||||
print("\nTHP probes (arm B, round 1):")
|
||||
for t in thps:
|
||||
total = [l for l in t.read_text().splitlines() if l.startswith("total-kB")]
|
||||
print(f" {t.name}: {total[0] if total else 'no data'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user