Skip to content

Commit 1ea38fe

Browse files
committed
run-pact.sh: replace cgroup fast-tier cap with first-touch (fixes #4)
The runner disabled demotion and then capped the workload with a cgroup memory.high. With demotion off and no swap, anonymous memory is unreclaimable, so crossing the cap throttled the workload in D-state during graph loading. Use first-touch placement (numactl -C <cpus> --, no cgroup) and create the fast/slow split by shrinking node 0 with a memmap= boot parameter, documented in setup/README.md. Also kill PACT by process group (not machine-wide), truncate per-run logs, restore the THP policy, and correct the bc-kron RSS reference.
1 parent 168b66a commit 1ea38fe

4 files changed

Lines changed: 208 additions & 130 deletions

File tree

run/README.md

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,48 +32,62 @@ Example:
3232
./run-pact.sh bc_kron_8t # any workload name defined in workloads.sh
3333
```
3434

35+
An optional 2nd positional argument, `skip_setup`, skips the cold-start cache
36+
flush (`drop_caches`) and the `vmtouch` slow-tier preload - use it to re-run a
37+
workload without re-priming the machine:
38+
39+
```bash
40+
./run-pact.sh bc_kron_8t skip_setup
41+
```
42+
3543
What the script does:
3644
- Loads the PACT kernel modules from [`../setup/kernel/`](../setup/kernel/)
3745
(or aborts, telling you to build them).
38-
- Preloads the workload's data into the slow tier with `vmtouch`.
39-
- Caps the workload's fast-tier (DRAM) footprint with a cgroup v2 `memory.high`
40-
set to `<workload>_rss * FAST_TIER_RATIO` - this is what creates the
41-
fast/slow split (see below).
42-
- Pins the PACT runtime to CPU 1 and the workload to the other CPU cores.
46+
- Preloads the workload's graph file into the slow-tier page cache with
47+
`vmtouch --membind 1`.
48+
- Launches the workload CPU-pinned (`numactl -C <cores> --`) with default
49+
first-touch memory allocation - PACT is the page placer, so the workload's
50+
memory is not bound to any node.
4351
- Launches `../src/pact` (under `sudo`; PACT needs root for PEBS/PMU) with the
44-
default PEBS / migration / binning parameters.
52+
default PEBS / migration / binning parameters and no core pinning (PACT's
53+
`--monitor-cpu` / `--migration-cpu` default to -1).
4554
- Saves logs to `results/<workload>/pact/`.
4655

4756
### How the tiering experiment works (read this)
4857

49-
PACT only has work to do when the workload's **hot pages start on the slow
50-
tier** - then PACT samples the remote-DRAM accesses (PEBS), scores their
51-
performance-criticality, and promotes the hot ones to the fast tier. For a run
52-
to exercise PACT you therefore need a genuine fast/slow split:
53-
54-
- **Fast-tier ratio.** `FAST_TIER_RATIO` (default `0.5` = a **1:1 split**) and
55-
`<workload>_rss` (peak RSS in MB, in `workloads.sh`) set the cgroup
56-
`memory.high = rss * ratio`. With `0.5`, half the working set fits in DRAM and
57-
the rest spills to the slow tier. Override per run, e.g.
58-
`FAST_TIER_RATIO=0.33 ./run-pact.sh bc_kron_8t` for a 1:2 split. (You can also
59-
set an absolute `FAST_TIER_MB`.)
60-
- **`_rss` is the ANON working set, and is graph/input dependent.** Re-measure
61-
for your graph: `/usr/bin/time -v ./bc -f kron.sg -i1 -n1` → "Maximum
62-
resident set size" (kbytes/1024 = MB). If `_rss` is unset the run is
63-
**uncapped** (no split, nothing to migrate) and prints a warning.
58+
PACT only has work to do when the fast tier is **too small to hold the whole
59+
working set**, so part of it starts on the slow tier. PACT then samples the
60+
slow-tier accesses (PEBS), scores their performance-criticality, promotes the
61+
critical pages to the fast tier, and the kernel demotes cold pages to make room.
62+
63+
- **The split comes from a physically small fast tier - `run-pact.sh` does NOT
64+
create it.** You must shrink node 0 with a `memmap=` boot parameter BEFORE
65+
running (setup step 1b; see [`../setup/README.md`](../setup/README.md)). For a
66+
1:1 split, node-0 usable DRAM = workload RSS / 2. If you skip this, the whole
67+
RSS fits in local DRAM, nothing lands on the slow tier, and PACT is a no-op
68+
(`pebs_samples = 0`, `Promotions = 0`).
69+
- **First-touch placement, not a bind.** The workload launches CPU-pinned but
70+
with default first-touch allocation (`numactl -C <cores> --`, no `--membind`
71+
or `--preferred`) - the paper's policy for application transparency. Memory
72+
fills the small fast tier first, then spills to the slow tier; PACT is the
73+
page placer from there.
74+
- **Not a cgroup cap.** Do not emulate a small fast tier with a cgroup
75+
`memory.high`/`memory.max`: a memcg limit caps total usage rather than
76+
fast-tier residency, and with demotion disabled the kernel cannot shrink an
77+
anonymous working set, so the workload throttles in unreclaimable D-state
78+
during its allocation phase.
6479
- **Permissions.** `perf_event_paranoid` must be `<= 0` or PACT collects **zero
6580
PEBS samples** even as root. `setup/env/prepare_environment.sh` sets it to
6681
`-1`; if you skip env prep, do `sudo sysctl kernel.perf_event_paranoid=-1`.
6782
- **Checking it worked.** A healthy run shows, in `results/<wl>/pact/pact_debug.log`,
68-
`pebs_samples` climbing into the millions and non-zero `Promotions
69-
(successful)`, and `numastat -p <bc-pid>` shows the workload's node-0 (fast)
70-
footprint growing over time. If `pebs_samples=0`, the hot set never reached
71-
the slow tier (cap too loose, or run too short) or paranoid is too high.
83+
`PAC Updates` and `Promotions (successful)` in the millions and non-zero
84+
`Demotions`, and `numastat -p <bc-pid>` shows a non-zero node-1 (slow)
85+
footprint that PACT drains over time. If `PAC Updates = 0`, nothing reached
86+
the slow tier (node 0 not shrunk, or run too short) or paranoid is too high.
7287

7388
### Tunables (environment variables)
7489

75-
`FAST_TIER_RATIO` / `FAST_TIER_MB` (the split, see above), `pebs_period`,
76-
`migration_limit`, `bin_count`, `bin_width`, `PACT`, `VMTOUCH`,
90+
`pebs_period`, `migration_limit`, `bin_count`, `bin_width`, `PACT`, `VMTOUCH`,
7791
`run_setup_config`, `enable_thp`. Example:
7892

7993
```bash
@@ -109,14 +123,22 @@ large). Build/obtain each from its upstream source:
109123
```bash
110124
cd gapbs && make
111125
# -g <scale>: 2^scale vertices, -k <degree>: avg degree, -b: serialized .sg
112-
./converter -g 25 -k 16 -b benchmark/graphs/kron.sg
126+
# Paper's bc-kron = scale 27, degree 16 (134.2M vertices, ~18 GB, ~19.5 GB RSS):
127+
./converter -g27 -k16 -b benchmark/graphs/kron.sg
113128
export GAPBS_DIR=$PWD
114129
```
115130

116-
Pick `scale`/`degree` so the graph's resident size matches your fast-tier
117-
budget (the paper uses a Kronecker graph sized to exceed local DRAM so it
118-
spills to the slow tier). `-g 25 -k 16` is the Graph500 default starting
119-
point; adjust to your node.
131+
The `bc_kron_8t_rss` value in `workloads.sh` is a reference figure you use to
132+
size the fast tier by hand (`memmap` = `_rss`/2 for a 1:1 split); the runner
133+
does not read it. It assumes this scale-27 graph, so if you generate a
134+
different scale, re-measure the RSS and update `_rss` (see the comment in
135+
`workloads.sh`) and your `memmap`.
136+
137+
The graph must be sized to **exceed** your fast tier so it spills to the slow
138+
tier (that is what gives PACT work). Scale 27 (~19.5 GB RSS) with a ~10 GB
139+
fast tier is the 1:1 point used above. On a machine with more or less DRAM,
140+
pick `scale` so the RSS is about twice your intended fast-tier size, then set
141+
`memmap` and `_rss` to match.
120142

121143
- **SPEC CPU 2017** (`603.bwaves_s`, `649.fotonik3d_s`) - a **licensed**
122144
benchmark; install it yourself and point `SPEC_DIR` at the run tree. The
@@ -169,3 +191,12 @@ topology (`numactl --hardware`; node1 should be CPU-less). Absolute slowdown
169191
numbers depend on the graph size, tier latency, and DRAM budget, so compare
170192
PACT against a baseline run on the *same* machine rather than to a fixed
171193
target.
194+
195+
## Quick-start checklist
196+
197+
- [ ] Booted into the PACT kernel (`uname -r` shows `6.3.0`) - [`../setup/kernel/`](../setup/kernel/)
198+
- [ ] Kernel modules built (`tierinit.ko`, `kswapdrst.ko`) - [`../setup/kernel/`](../setup/kernel/)
199+
- [ ] Machine prepared - [`../setup/env/`](../setup/env/)
200+
- [ ] `../src/pact` binary compiled - [`../src/`](../src/)
201+
- [ ] Workload name and data paths set (see `workloads.sh` / env vars above)
202+
- [ ] Run `./run-pact.sh <workload_name>`

run/run-pact.sh

Lines changed: 69 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -46,35 +46,38 @@ done
4646
cpus=$(IFS=,; echo "${cores[*]}")
4747

4848
# --- PACT Configuration (override via env, defaults match config.h) ---
49-
VMTOUCH="${VMTOUCH:-/usr/bin/vmtouch}"
49+
VMTOUCH="${VMTOUCH:-vmtouch}"
5050
PACT="${PACT:-../src/pact}"
5151
pebs_period="${pebs_period:-400}"
5252
migration_limit="${migration_limit:-4096}"
5353
bin_count="${bin_count:-20}"
5454
bin_width="${bin_width:-1000.0}"
5555

56-
# --- Fast-tier size / split ratio (cgroup v2 memory.max) ---
57-
# A tiering experiment must CONSTRAIN the fast tier (local DRAM), otherwise the
58-
# whole workload fits in DRAM and there is nothing to demote/promote. We cap the
59-
# workload's memory with a cgroup v2 memory.max so that only a fraction of its
60-
# RSS fits in the fast tier and the rest spills to the slow (CXL-like) tier.
56+
# --- Tier placement (paper methodology: first-touch, PACT is the placer) ---
57+
# The workload's memory is allocated FIRST-TOUCH (default NUMA policy): we
58+
# CPU-pin it but do NOT bind its memory to any node. The paper's allocation
59+
# policy is first-touch for application transparency (PACT decides placement
60+
# online, the application does nothing). PACT samples slow-tier accesses and
61+
# promotes the performance-critical pages to the fast tier, enabling kernel
62+
# demotion only when Algorithm 2's balance check calls for it.
6163
#
62-
# <workload>_rss : the workload's peak RSS in MB (defined in workloads.sh)
63-
# FAST_TIER_RATIO : fraction of RSS allowed in the fast tier (0<r<=1).
64-
# 0.5 == a 1:1 split (half fast, half slow). Default 0.5.
65-
# FAST_TIER_MB : optional absolute override (MB); takes precedence over RSS*ratio.
64+
# The fast/slow SPLIT comes from a physically small fast tier, NOT from
65+
# binding the workload's memory. Boot the machine with node 0's usable DRAM
66+
# reduced to the desired fast-tier size via a node-0 memmap= kernel parameter,
67+
# so first-touch fills the fast tier and the kernel then places the overflow
68+
# on the slow tier.
6669
#
67-
# When neither <workload>_rss nor FAST_TIER_MB is set, the cgroup limit is
68-
# skipped (with a warning) and the run is uncapped (no enforced split).
69-
FAST_TIER_RATIO="${FAST_TIER_RATIO:-0.5}"
70-
rss_var="${WORKLOAD}_rss"
71-
workload_rss="${!rss_var:-}"
72-
CGROUP_NAME="${CGROUP_NAME:-pact_${WORKLOAD}}"
70+
# Do NOT use a cgroup memory.high/memory.max cap to emulate a small fast
71+
# tier: a memcg limit caps TOTAL usage (not fast-tier residency), and with
72+
# demotion disabled the kernel has no way to shrink an anonymous working
73+
# set, so the workload throttles in unreclaimable D-state during its
74+
# allocation phase (reported as a hang on graph loading, issue #4).
7375

7476
# --- Environment Setup Flag ---
7577
# When "true", run full machine preparation (uncore frequency pinning + CXL
76-
# config via check_cxl_conf) before the run, mirroring run-pact-old.sh's setup
77-
# phase. Default is "false" so a plain run does NOT touch machine-wide config.
78+
# config via prepare_environment.sh) before the run. Default is "false" so a
79+
# plain run does NOT touch machine-wide config (you are expected to have run
80+
# setup/env/prepare_environment.sh yourself).
7881
# Override via env: run_setup_config=true ./run-pact.sh <workload>
7982
run_setup_config="${run_setup_config:-false}"
8083

@@ -83,10 +86,28 @@ run_setup_config="${run_setup_config:-false}"
8386
# untouched. Override via env: enable_thp=true ./run-pact.sh <workload>
8487
enable_thp="${enable_thp:-false}"
8588

89+
# Save the pre-run THP policy so clean_up can restore it (the /sys file reports
90+
# the choices with the active one in [brackets], e.g. "always [madvise] never").
91+
# When enable_thp=true we switch it to "always" for the run and put the prior
92+
# policy back on exit, rather than leaving THP forced off.
93+
prev_thp=""
94+
prev_thp_defrag=""
95+
if [ "$enable_thp" = "true" ]; then
96+
thp_line=$(cat /sys/kernel/mm/transparent_hugepage/enabled 2>/dev/null || true)
97+
prev_thp=$(printf '%s\n' "$thp_line" | grep -oE '\[[a-z]+\]' | tr -d '[]' || true)
98+
thp_defrag_line=$(cat /sys/kernel/mm/transparent_hugepage/defrag 2>/dev/null || true)
99+
prev_thp_defrag=$(printf '%s\n' "$thp_defrag_line" | grep -oE '\[[a-z]+\]' | tr -d '[]' || true)
100+
fi
101+
86102
# --- Output Directory ---
103+
# Fixed per-workload path. The monitor logs below (vmstat.txt, numastat.log)
104+
# are appended to during the run, so truncate them up front - otherwise a
105+
# re-run interleaves its samples with the previous run's.
87106
OUTDIR="./results/${WORKLOAD}/pact"
88107
mkdir -p "$OUTDIR"
89108
OUTDIR=$(realpath "$OUTDIR")
109+
: >"$OUTDIR/vmstat.txt"
110+
: >"$OUTDIR/numastat.log"
90111

91112
echo "=== PACT Run: $WORKLOAD ==="
92113
echo " CPUs: $cpus ($omp_threads threads)"
@@ -116,25 +137,25 @@ clean_up() {
116137
sudo kill -SIGINT "$PACT_PID" 2>/dev/null || kill -SIGINT "$PACT_PID" 2>/dev/null || true
117138
sleep 2
118139
sudo kill -KILL "$PACT_PID" 2>/dev/null || kill -KILL "$PACT_PID" 2>/dev/null || true
119-
# Also reap the actual pact process (sudo's child) by name as a backstop.
120-
sudo pkill -KILL -x pact 2>/dev/null || true
140+
# Backstop: reap the real pact process (sudo's child) by killing PACT's
141+
# process group, NOT `pkill -x pact` (which would kill every PACT on the
142+
# machine, including a concurrent experiment).
143+
pgid=$(ps -o pgid= -p "$PACT_PID" 2>/dev/null | tr -d ' ')
144+
[ -n "$pgid" ] && sudo kill -KILL -- "-$pgid" 2>/dev/null || true
121145
fi
122146

123147
kill "${pid_vmstat:-}" 2>/dev/null || true
124148
kill "${pid_numastat:-}" 2>/dev/null || true
125149

126-
# Remove the workload cgroup (must have no live procs first).
127-
if [ -n "${CGROUP_PATH:-}" ] && [ -d "$CGROUP_PATH" ]; then
128-
sudo rmdir "$CGROUP_PATH" 2>/dev/null || true
129-
fi
130-
131150
echo " Disabling demotion_enabled..."
132151
echo 0 | sudo tee /sys/kernel/mm/numa/demotion_enabled >/dev/null 2>&1 || true
133152

134153
if [ "$enable_thp" = "true" ]; then
135-
echo " Disabling transparent huge pages (THP policy = never)"
136-
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled >/dev/null 2>&1 || true
137-
echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag >/dev/null 2>&1 || true
154+
# Restore the THP policy captured before the run (fall back to "never"
155+
# only if it could not be read).
156+
echo " Restoring transparent huge pages policy (enabled=${prev_thp:-never}, defrag=${prev_thp_defrag:-never})"
157+
echo "${prev_thp:-never}" | sudo tee /sys/kernel/mm/transparent_hugepage/enabled >/dev/null 2>&1 || true
158+
echo "${prev_thp_defrag:-never}" | sudo tee /sys/kernel/mm/transparent_hugepage/defrag >/dev/null 2>&1 || true
138159
fi
139160

140161
echo "Cleanup complete"
@@ -148,7 +169,9 @@ trap clean_up EXIT INT TERM
148169
# otherwise we abort and tell the user to build it.
149170
ensure_kernel_module() {
150171
local modname=$1 moddir=$2 ko=$3
151-
if lsmod | grep -qE "^${modname}[[:space:]]"; then
172+
# No `grep -q` here: under `set -o pipefail` an early -q exit can kill
173+
# lsmod with SIGPIPE and turn "module already loaded" into rc=141.
174+
if lsmod | grep -E "^${modname}[[:space:]]" >/dev/null; then
152175
echo " module '$modname' already loaded"
153176
return 0
154177
fi
@@ -205,42 +228,6 @@ touch "$OUTDIR/vmstat.txt"
205228
done) &
206229
pid_vmstat=$!
207230

208-
# --- Phase 2b: Fast-tier limit via cgroup v2 (the split ratio) ---
209-
# Cap the workload's fast-tier (local DRAM) footprint with a cgroup v2
210-
# memory.high. We use memory.high (a SOFT limit) not memory.max (HARD): under
211-
# memory.high the kernel reclaims/demotes the excess to the slow NUMA tier,
212-
# whereas a hard memory.max with no demotion path OOM-kills the workload. PACT
213-
# then migrates pages between tiers (cold->slow, hot->fast). A generous
214-
# memory.max headroom is kept so a transient spike can't OOM the run.
215-
# Skipped if no RSS/limit is known.
216-
CGROUP_PATH=""
217-
fast_tier_mb=""
218-
if [ -n "${FAST_TIER_MB:-}" ]; then
219-
fast_tier_mb="$FAST_TIER_MB"
220-
elif [ -n "$workload_rss" ]; then
221-
# fast_tier_mb = workload_rss * FAST_TIER_RATIO (awk for float ratio)
222-
fast_tier_mb=$(awk -v r="$workload_rss" -v f="$FAST_TIER_RATIO" 'BEGIN{printf "%d", r*f}')
223-
fi
224-
if [ -n "$fast_tier_mb" ] && [ "$fast_tier_mb" -gt 0 ]; then
225-
CGROUP_PATH="/sys/fs/cgroup/${CGROUP_NAME}"
226-
echo "=== Phase 2b: fast-tier cgroup ${CGROUP_NAME}: memory.high=${fast_tier_mb}MB"
227-
echo " (workload_rss=${workload_rss:-?}MB, FAST_TIER_RATIO=${FAST_TIER_RATIO})"
228-
sudo mkdir -p "$CGROUP_PATH"
229-
# Ensure the memory controller is delegated to the new cgroup's level.
230-
echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null 2>&1 || true
231-
# Soft cap drives reclaim/demotion of the excess to the slow tier:
232-
echo $(( fast_tier_mb * 1024 * 1024 )) | sudo tee "$CGROUP_PATH/memory.high" >/dev/null
233-
# Keep memory.max generous (RSS + 25% headroom) so a spike can't OOM:
234-
if [ -n "$workload_rss" ]; then
235-
echo $(( (workload_rss + workload_rss / 4) * 1024 * 1024 )) | \
236-
sudo tee "$CGROUP_PATH/memory.max" >/dev/null 2>&1 || true
237-
fi
238-
echo " memory.high = $(cat "$CGROUP_PATH/memory.high" 2>/dev/null) bytes"
239-
else
240-
echo "WARNING: no <workload>_rss or FAST_TIER_MB set — running UNCAPPED"
241-
echo " (no enforced fast/slow split; PACT may have nothing to migrate)."
242-
fi
243-
244231
# --- Phase 3: Launch Workload ---
245232
echo "=== Phase 3: Launching workload ==="
246233

@@ -250,23 +237,18 @@ if [ "$enable_thp" = "true" ]; then
250237
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/defrag >/dev/null
251238
fi
252239

253-
# CPU-pin the workload but deliberately do NOT --membind it: PACT is the page
254-
# placer. The workload's data is pre-faulted onto the slow tier in Phase 1
255-
# (vmtouch --membind 1) and PACT promotes hot pages up; binding memory here
256-
# would defeat the experiment. Memory is capped by the cgroup above (Phase 2b).
240+
# CPU-pin the workload but deliberately do NOT bind its memory: PACT is the
241+
# page placer, and the paper's allocation policy is first-touch. Memory
242+
# lands on whichever tier first-touch fills (the fast tier, sized physically;
243+
# overflow to slow), and PACT promotes the critical pages. Binding memory
244+
# here would defeat the experiment. workload_cmd references $numactl_args
245+
# (see workloads.sh), so the CPU pinning applies to the workload binary even
246+
# when the command cd's first.
257247
numactl_args="numactl -C ${cpus} --"
258248
echo " Executing: ${workload_cmd}"
249+
echo " (numactl_args = ${numactl_args})"
259250

260-
# Launch inside the fast-tier cgroup (if any): the subshell adds its own PID to
261-
# cgroup.procs before exec'ing the workload, so all the workload's memory is
262-
# accounted against memory.max.
263-
if [ -n "$CGROUP_PATH" ]; then
264-
OMP_NUM_THREADS=${omp_threads} bash -c '
265-
echo $$ | sudo tee '"$CGROUP_PATH"'/cgroup.procs >/dev/null
266-
exec '"${workload_cmd}"'' >"$OUTDIR/workload.output" 2>&1 &
267-
else
268-
OMP_NUM_THREADS=${omp_threads} eval "${workload_cmd}" >"$OUTDIR/workload.output" 2>&1 &
269-
fi
251+
OMP_NUM_THREADS=${omp_threads} eval "${workload_cmd}" >"$OUTDIR/workload.output" 2>&1 &
270252
WORKLOAD_PID=$!
271253
sleep 2
272254

@@ -326,7 +308,12 @@ start_time=$(date +%s)
326308
# PACT needs root (euid 0) to open the CHA/uncore PMU and PEBS counters
327309
# (validate_hardware_access() aborts otherwise). Launch it under sudo; the
328310
# machine is assumed to allow passwordless sudo (CloudLab does).
329-
PACT_CMD="sudo numactl -C 1 $PACT \
311+
#
312+
# No core pinning by default: PACT's CPU-affinity knobs (--monitor-cpu,
313+
# --migration-cpu) default to -1 (unpinned), so the OS schedules the
314+
# coroutine event loop and the migration thread. Pin them explicitly
315+
# (e.g. --monitor-cpu 1 --migration-cpu 1) to reserve a dedicated core.
316+
PACT_CMD="sudo $PACT \
330317
--workload $WORKLOAD_PID \
331318
--pebs-period $pebs_period \
332319
--max-migrations-per-cycle $migration_limit \
@@ -361,6 +348,8 @@ echo "Workload finished. Runtime: ${runtime}s"
361348
echo "Runtime: ${runtime}s" >>"$OUTDIR/workload.output"
362349

363350
# --- Cleanup ---
351+
# Disarm the EXIT trap first so clean_up runs exactly once (not again on exit).
352+
trap - EXIT INT TERM
364353
clean_up
365354

366355
cat /proc/vmstat >"$OUTDIR/after_vmstat.log"

0 commit comments

Comments
 (0)