4646cpus=$( IFS=,; echo " ${cores[*]} " )
4747
4848# --- PACT Configuration (override via env, defaults match config.h) ---
49- VMTOUCH=" ${VMTOUCH:-/ usr / bin / vmtouch} "
49+ VMTOUCH=" ${VMTOUCH:- vmtouch} "
5050PACT=" ${PACT:- ../ src/ pact} "
5151pebs_period=" ${pebs_period:- 400} "
5252migration_limit=" ${migration_limit:- 4096} "
5353bin_count=" ${bin_count:- 20} "
5454bin_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>
7982run_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>
8487enable_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.
87106OUTDIR=" ./results/${WORKLOAD} /pact"
88107mkdir -p " $OUTDIR "
89108OUTDIR=$( realpath " $OUTDIR " )
109+ : > " $OUTDIR /vmstat.txt"
110+ : > " $OUTDIR /numastat.log"
90111
91112echo " === PACT Run: $WORKLOAD ==="
92113echo " 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.
149170ensure_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"
205228done) &
206229pid_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 ---
245232echo " === 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
251238fi
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.
257247numactl_args=" numactl -C ${cpus} --"
258248echo " 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 &
270252WORKLOAD_PID=$!
271253sleep 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"
361348echo " 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
364353clean_up
365354
366355cat /proc/vmstat > " $OUTDIR /after_vmstat.log"
0 commit comments