bench: retune spin A/B sweep for saturation (4/6 cores, 16 loadgen)

The old 1/2/4/8 sweep with an 8-core loadgen plateaued ~102k rps at 4 and 8
server cores -- loadgen-bound, ~85% server CPU, so the 4-core (gate-active)
point was never actually saturated. Retune:

- CORES 1 2 4 8 -> 1 2 4 6. The gate is active at 2-4, inert at 1 and >=5, so
  6 is now the saturating inert control and 4-vs-6 brackets the gate boundary
  under real load (8 couldn't be saturated).
- LOADGEN_CPUS 12-19 (8c) -> 8-23 (16c); WRK_THREADS 8 -> 16; WRK_CONNS
  256 -> 512. Gives >=2x loadgen-cores-per-server-core at every sweep point
  (16:1/8:1/4:1/2.7:1) and leaves cores 6-7 for the OS.
- analyzer: gate label now follows the real 2..=4 rule (6 reads inert), and
  the reading guide notes to confirm the 6-core control is CPU- not
  loadgen-bound before trusting it.

Note: at 22/24 logical CPUs in use, loadgen will share SMT siblings with the
server; sibling-disjoint isn't achievable at 16 loadgen cores. Override
LOADGEN_CPUS after checking lscpu -e if that matters.
This commit is contained in:
sandbox-agent
2026-06-14 21:43:15 +00:00
parent 56e490c205
commit 5f342858e7
2 changed files with 31 additions and 14 deletions
+10 -5
View File
@@ -18,7 +18,10 @@ import os
import re
import sys
GATED = {2, 4} # core counts where the RFC 004 spin policy is active
def gate_active(cores):
"""RFC 004 spinner cap is non-zero only at 2..=4 schedulers (1 and >=5
allow 0 spinners), so the policy is active exactly there."""
return 2 <= cores <= 4
def parse_latency_ms(s):
@@ -88,7 +91,7 @@ def main():
b = fnum(before.get(k, {}).get("sustained_rps"))
a = fnum(after.get(k, {}).get("sustained_rps"))
spd = f"{a / b:.2f}x" if (a and b) else ""
gate = "ACTIVE" if cores in GATED else "inert"
gate = "ACTIVE" if gate_active(cores) else "inert"
bs = f"{b:,.0f}" if b is not None else ""
as_ = f"{a:,.0f}" if a is not None else ""
print(f"{srv:<10} {scn:<4} {cores:>5} {bs:>12} {as_:>12} {spd:>8} {gate}")
@@ -107,7 +110,7 @@ def main():
delta = f"{(a - b):+.2f}"
else:
delta = ""
gate = "ACTIVE" if cores in GATED else "inert"
gate = "ACTIVE" if gate_active(cores) else "inert"
bs = f"{b:.2f}" if b is not None else ""
as_ = f"{a:.2f}" if a is not None else ""
print(f"{srv:<10} {scn:<4} {cores:>5} {bs:>10} {as_:>10} {delta:>9} {gate}")
@@ -126,8 +129,10 @@ def main():
print(
"\nReading it: spin is 'worth it' if s3 (and/or s2) RPS rises and/or "
"tail latency drops at cores 2/4 (gate ACTIVE), with cores 1/8 (inert) "
"staying flat. Flat-or-worse at 2/4 = not worth it on real traffic."
"tail latency drops at the gate-ACTIVE cores (2-4), with the inert "
"controls (1 and >=5, e.g. 6) staying flat. Flat-or-worse at 2/4 = "
"not worth it on real traffic. Cross-check that the saturating control "
"(6 cores) is actually CPU-bound, not loadgen-bound, before trusting it."
)
if "axum-mem" in {k[0] for k in keys}:
print(
+21 -9
View File
@@ -20,10 +20,13 @@
# Scenarios (per RECON §6.4): s2 (router+auth, no store) and s3 (memory store,
# the headline). Server is urus-mem for both. We do NOT touch s4/sqlite.
#
# Core sweep (per RECON §6.3): the spin gate is active at 2-4 schedulers and
# inert at 1 and >=5, so 2 and 4 should show the effect and 1 and 8 bracket it
# as controls. Server is pinned to cores 0..N-1; the load generator gets its
# own disjoint cores (LOADGEN_CPUS).
# Core sweep: the spin gate is active at 2-4 schedulers and inert at 1 and
# >=5. We sweep 1/2/4/6 server cores: 2 and 4 are gate-ACTIVE, 1 and 6 are
# inert controls -- 6 also being a *saturating* control, since the old 8-core
# point couldn't be saturated by an 8-core loadgen (it plateaued ~102k rps,
# loadgen-bound). Server is pinned to cores 0..N-1; the load generator gets
# its own disjoint cores (LOADGEN_CPUS), sized ~>=2x the server cores per the
# saturation goal while leaving cores free for the OS.
#
# Usage:
# ./spin_ab_urus.sh # full A/B, defaults
@@ -37,8 +40,10 @@
# Env knobs (all overridable):
# BEFORE / AFTER smarm commit-ishes (default b0c9685 / HEAD)
# SCENARIOS space-separated (default "s2 s3")
# CORES server core counts to sweep (default "1 2 4 8")
# LOADGEN_CPUS loadgen taskset range (default "12-19")
# CORES server core counts to sweep (default "1 2 4 6")
# LOADGEN_CPUS loadgen taskset range (default "8-23" = 16 cores)
# WRK_THREADS loadgen wrk/wrk2 threads (default 16)
# WRK_CONNS loadgen open connections (default 512)
# SERVER memory server backend (default urus-mem)
# PROBE_SEC/WARMUP_SEC/MEASURE_SEC/SAT_RATIO/PREPOP/WRK_CONNS/WRK_THREADS
# passed straight through to runner.sh
@@ -74,9 +79,15 @@ AFTER_DEFAULT="$(git -C "$SMARM_DIR" rev-parse --short HEAD)"
AFTER="${AFTER:-$AFTER_DEFAULT}"
SCENARIOS="${SCENARIOS:-s2 s3}"
CORES="${CORES:-1 2 4 8}"
CORES="${CORES:-1 2 4 6}"
SERVER="${SERVER:-urus-mem}"
LOADGEN_CPUS="${LOADGEN_CPUS:-12-19}"
LOADGEN_CPUS="${LOADGEN_CPUS:-8-23}"
# Loadgen sizing: ~2x loadgen cores per server core. 16 cores covers the
# 6-core server at ~2.7x and the 4-core at 4x. Threads track the core count;
# connections bumped so the probe can actually find the server's peak.
export WRK_THREADS="${WRK_THREADS:-16}"
export WRK_CONNS="${WRK_CONNS:-512}"
QUICK=0
DRY=0
@@ -111,7 +122,8 @@ echo ">>> smarm: $SMARM_DIR"
echo ">>> BEFORE=$BEFORE AFTER=$AFTER"
echo ">>> servers: ${SERVERS_TO_RUN[*]}"
echo ">>> scenarios: $SCENARIOS"
echo ">>> cores: $CORES (loadgen on $LOADGEN_CPUS)"
echo ">>> cores: $CORES"
echo ">>> loadgen: cpus=$LOADGEN_CPUS threads=$WRK_THREADS conns=$WRK_CONNS"
echo ">>> machine: $NPROC cores"
echo ">>> outdir: $OUTDIR"
[[ "$QUICK" == "1" ]] && echo ">>> --quick: probe/warmup/measure = ${PROBE_SEC}/${WARMUP_SEC}/${MEASURE_SEC}s"