PERKS

A Locality-Optimized Execution Model for Iterative Memory-bound GPU Applications — and a case study in why its authors tried L2 residency and threw it away

ICS '23 DOI 10.1145/3577193.3593705 Persistent-Thread Caching Register + Shared Memory Stencil / CG

Lingqi Zhang, Mohamed Wahib, Peng Chen, Jintao Meng, Xiao Wang, Toshio Endo, Satoshi Matsuoka

Tokyo Institute of Technology · RIKEN Center for Computational Science · AIST · Shenzhen Institutes of Advanced Technology · Oak Ridge National Laboratory

1. Overview & Why It Is In This Survey

PERKS (Persistent Kernels) is a GPU execution model for iterative, memory-bound applications: Jacobi-style stencil solvers and the conjugate-gradient (CG) loop. Each time step re-reads the same producer→consumer working set from HBM, computes a new field, and writes it back, only to read it again next step. PERKS' insight is that a stencil kernel leaves the SM's registers and shared memory largely idle, and a persistent-thread grid can hold the inter-iteration working set on-chip across all time steps, eliminating the repeated HBM round-trips. The headline result is up to ~2.12× throughput on 2D stencils on an A100.

Why this non-LLM paper appears in an LLM-serving survey. Our measurement study — "Worth the Bytes?" — argues that transient inter-kernel reuse of activations via the GPU L2 cache is worth less than 0.65% of step time in eager LLM inference. PERKS is the strongest disconfirming-by-precedent case: it is a reuse-obsessed system whose entire reason for existing is to keep an inter-iteration working set resident, yet its authors route that reuse through registers and shared memory, not L2. They even committed an L2-residency code path and then commented it out with the note “l2 cache setting not show performance difference.” If a team that lives and breathes locality could not make L2 residency pay, that is direct external support for our L2-is-not-the-win conclusion.

1.1 Why now: the bandwidth wall and a decade of idle on-chip storage

GPU peak FLOPs have grown far faster than HBM bandwidth for a decade, so a widening class of kernels is bandwidth-bound: the SMs sit waiting on memory, not arithmetic. Iterative stencils and CG are the textbook case — low arithmetic intensity, large fields, hundreds of steps. At the same time, each Ampere SM ships ~256 KB of registers and ~164 KB of shared memory that a stencil kernel barely touches. The opportunity PERKS seizes is the gap between an under-fed memory system and an over-provisioned, idle on-chip storage system: move the recurring working set up the hierarchy into the storage that is already there and unused. The reason this is newly practical is the maturation of cooperative-grid launch (a grid-wide barrier with all blocks co-resident), which lets the iteration loop live inside one kernel so register/shared-memory contents survive across steps.

1.2 Positioning vs prior locality techniques

Technique Reuse level Limitation PERKS addresses
Spatial blocking (shared-mem tiling) shared memory, within one step reuse is intra-iteration only; the field still round-trips HBM between steps
Temporal blocking (step fusion) shared memory / registers, few fused steps ghost-zone replication grows with fused depth; fragile to stencil shape
L2 set-aside / persistence window (Ampere) L2 shared, hardware-evicted, slower than SRAM; PERKS authors measured net −8%
PERKS registers + shared memory, across ALL steps persistent grid holds the working set on-chip for the whole loop; halo crosses blocks via grid sync

1.3 Core contributions

Persistent-thread caching

One cooperative-grid kernel launch runs the entire iteration loop. The inter-iteration working set stays in on-chip registers and shared memory between time steps instead of being flushed to HBM and reloaded.

Register-vs-SRAM auto-config

A per-stencil, per-precision configuration table (perksconfig.cuh) chooses how much of the working set to fold into registers vs shared memory, balancing occupancy against on-chip residency.

Boundary-exchange via the grid

Because a single persistent grid spans all SMs, halo / boundary data crosses block boundaries through a cooperative grid synchronization rather than a fresh global launch, preserving on-chip residency across the whole iteration.

~2.12×
2D-stencil geomean speedup (paper)
~8%
A100 L2-residency SLOWDOWN (paper Sec 4.4.2)
1
kernel launch for the whole loop (persistent grid)
~flat
L2 hit rate baseline→PERKS (measured: ~72%→~79%; L2 time-share not directly quantified)

2. The Problem: Re-Streaming the Same Bytes Every Time Step

An iterative stencil computes a new field from the old one, repeated for many time steps. In the conventional “one launch per iteration” model, every step launches a fresh global kernel that reads the entire grid from HBM, computes, and writes it back. The arithmetic per element is tiny (a weighted sum of neighbors), so the kernel is firmly bandwidth-bound: its runtime is governed by HBM traffic, not by the SMs' FLOP capacity.

The waste is structural. The field that step $t$ writes is exactly what step $t{+}1$ reads. If a fixed portion of that field could stay on-chip, the system would skip a full HBM write–read round-trip per step. With $N$ time steps over a domain of $D$ bytes, the conventional model moves on the order of $2 N D$ bytes through HBM; PERKS aims to move a small constant of that.

2.1 The memory hierarchy and where reuse can live

An A100 SM exposes a deep on-chip hierarchy. The crucial design choice in any reuse system is which level the recurring working set is parked in. PERKS bets on the two fastest, software-managed levels (registers, shared memory). The L2 cache — the only level CUDA lets you bias toward persistence via an access-policy window — sits below those, shared across all SMs, and is the level our survey is asking about.

Registersper-thread, fastest, ~256 KB/SM filePERKS primary cache
Shared memper-SM scratchpad, software-managed, ~164 KB/SMPERKS secondary cache (--usesm)
L2 cache~40 MB, shared across SMs, hardware-managed (persistence hint via access-policy window)tried, then abandoned
HBM2e~80 GB, ~2 TB/s, off-chip — the bottleneck PERKS attacksre-streamed every step (baseline)

2.2 A worked HBM-traffic estimate

Make the waste concrete. Take an fp32 field of $W \times H = 8192 \times 8192$ cells; that is one copy of $D = 4 \cdot 8192^2 \approx 256\,\text{MB}$. Over $N = 1000$ time steps, the baseline reads and writes the whole field each step, so HBM moves about $2 N D \approx 512\,\text{GB}$. If the persistent grid keeps the interior on-chip and only exchanges halos, the HBM traffic collapses to roughly $2 D$ (load once, store the final field) plus a per-step boundary term proportional to the tile perimeter, which for large square tiles is asymptotically negligible. At ~2 TB/s of A100 HBM bandwidth, the baseline's ~512 GB is a hard floor of about a quarter-second of pure memory time; removing it is exactly the headroom PERKS converts into ~2× throughput.

step t: read field (HBM)
compute (on-chip)
step t: write field (HBM)× N steps in the baseline

2.3 Why the obvious baselines do not solve it

Three “just do X” strawmen are worth dismissing up front, because PERKS' value is precisely that none of them work.

Strawman A: temporal blocking

Fuse several time steps into one kernel and process a tile across them. This needs ghost-zone replication that grows with the number of fused steps and quickly re-saturates HBM or shared memory; it is fragile to stencil shape and rarely sustains across hundreds of iterations.

Strawman B: just rely on L2

Ampere added an L2 set-aside window and a persistence hint — surely pinning the field in L2 fixes it? The whole point of this page: the PERKS authors implemented exactly this, measured it, and got a slowdown. L2 is shared, hardware-evicted, and slower than registers/SRAM; biasing it toward one array starves everything else.

Strawman C: blame launch overhead

If you launch a kernel per iteration, maybe launch overhead dominates? The paper (Sec 2.1.2) measures launch cost as negligible relative to the kernels' duration. So the persistent grid's win cannot be explained by eliding launches either — it must come from the on-chip data residency the single launch enables.

3. Mechanism: One Persistent Grid That Holds the Working Set

3.1 Traditional launch vs persistent launch

The baseline path is compiled under TRADITIONLAUNCH: a host-side loop that issues one global kernel launch per time step. Between launches there is no guarantee that any register or shared-memory content survives, so the field must be flushed to and reloaded from HBM. The PERKS path is compiled under PERSISTENTLAUNCH: one cooperative launch (cudaLaunchCooperativeKernel) per timed run, not per time step — the time-step loop lives inside the device kernel and runs with a cooperative grid::sync() between steps, so the working set stays on-chip across steps. The outer RUNS loop simply repeats the timed experiment.

Both branches are visible side by side in the source (file paths are real, line numbers are from the cloned repo):

// stencil/2dstencil/jacobi-2d.cu  (~L754–L770)
#ifdef PERSISTENTLAUNCH          // PERKS: one cooperative launch for the WHOLE loop
  for(int i=0; i<RUNS; i++) {
    cudaLaunchCooperativeKernel((void*)execute_kernel,
            executeGridDim, executeBlockDim,
            ExecuteKernelArgs, executeSM, 0);   // iteration loop lives INSIDE the kernel
  }
#endif
#ifdef TRADITIONLAUNCH           // baseline: one global launch PER iteration
  for(int i=0; i<RUNS; i++) {
    execute_kernel<<<executeGridDim, executeBlockDim, executeSM>>>
            (input, width_y, width_x, __var_2__);    // field round-trips through HBM each step
  }
#endif

Baseline (TRADITIONLAUNCH)

  • Step 1: launch kernel → read field from HBM → compute → write field to HBM
  • Step 2: launch kernel → read SAME field from HBM → compute → write to HBM
  • … × N steps: ~2ND bytes of HBM traffic

PERKS (PERSISTENTLAUNCH)

  • ONE cooperative launch; the cacheable tile interior is staged into registers + shared memory
  • iteration loop runs inside the kernel; grid::sync() between steps
  • the bulk tile interior avoids repeated HBM round-trips; boundary / global scratch and final output traffic remain
Note: The cooperative launch is necessary, not cosmetic. A persistent grid must co-resident all blocks at once (so the grid-wide sync is well-defined), which caps occupancy — this is the cost PERKS pays in exchange for on-chip residency, and the reason the configuration table exists.

3.2 Register fold vs shared-memory fold

PERKS exposes two knobs at run time. --blkpsm sets the number of thread blocks resident per SM (and thus how much of the working set each block can cache), and --usesm switches on the shared-memory cache in addition to the register cache. The gen binary caches into registers; gen --usesm adds the SRAM tier; genwr caches into the maximum number of registers first and spills to shared memory. The choice of how many register “folds” vs shared-memory folds to use is precomputed per stencil type and precision in perksconfig.cuh, so a default build needs no codegen.

3.3 Boundary exchange without leaving the chip

A stencil's update at a tile edge needs neighbor values that belong to an adjacent block. In the baseline model that data is communicated implicitly: every block writes its whole tile to HBM, and the next step's launch reads neighbors back. PERKS cannot afford that round-trip — the entire point is to keep the field on-chip. Instead it relies on the persistent grid: all blocks are co-resident, so after each step a grid-wide synchronization makes the just-computed halo cells globally visible (through a thin slab of global memory used only for the boundary, plus the L2_cache3/L2_cache4 scratch buffers), and the next step proceeds. The bulk interior of every tile avoids the repeated HBM round-trip; only boundary / global scratch and the final output still move through HBM. This is why a cooperative launch is mandatory and why occupancy, not arithmetic, becomes the binding constraint.

Naming caveat: The boundary scratch buffers in the source are literally named L2_cache3 / L2_cache4. The name is a holdover — these are ordinary global allocations, not an L2 residency mechanism. The actual L2-residency control is the separate, commented-out access-policy-window block of Section 4.1. Do not conflate the two: the named buffers are always used; the L2 persistence hint is always off.

3.4 The output line we will measure

Binaries built with -D__PRINT__ emit a single tab-separated line per run. The headline number is throughput in GCells/s. From the source, the timing/throughput print is:

// stencil/2dstencil/jacobi-2d.cu  (~L812)  —  elapsedTime(ms) \t throughput(GCells/s)
printf("%f\t%f\t", elapsedTime,
       (REAL)iteration*(width_y-2*HALO)*(width_x-2*HALO) / (elapsedTime/RUNS)/1000/1000);

The speedup we report is simply gen (or genwr) GCells/s divided by baseline GCells/s, for the same stencil type, domain size, and iteration count.

4. The Crux: What PERKS Deliberately Does NOT Use

This is the section that matters for our survey. PERKS' speedup is real, but the question we care about is which level of the hierarchy delivers it. Three independent pieces of evidence — two from the paper, one from the committed source — converge on the same answer: not the L2 cache, and not kernel-launch elision.

4.0 Background: what the Ampere L2 set-aside actually does

Ampere (sm_80) introduced a software-controllable L2 persistence feature. A program can carve out a portion of the ~40 MB L2 as a “set-aside” region and attach an access-policy window to a stream: accesses to a chosen address range get a hitRatio (the fraction of accesses biased toward persistence) and a hitProp of cudaAccessPropertyPersisting, so the hardware preferentially keeps those lines resident instead of evicting them under normal LRU pressure. This is the closest a CUDA programmer can come to “pinning” an array in L2. It is exactly the mechanism a naive reuse story would reach for — and exactly the mechanism PERKS' authors wired up, set to maximum (hitRatio = 1), measured, and discarded.

4.1 Evidence #1 (source): the L2 set-aside path is committed, then commented out

In jacobi-2d.cu, the entire CUDA L2-residency control path — the access-policy window, hitRatio = 1, cudaAccessPropertyPersisting, and cudaCtxResetPersistingL2Cache() — is present in the repository but fully commented out. It is gated behind #if defined(GEN) && defined(L2PER), and no CMake target defines L2PER, so it is dead code in every shipped build. Directly above it is the author's verdict. This is a verifiable, static fact in the cloned repo (confirmed at lines ~715–729):

// stencil/2dstencil/jacobi-2d.cu  L715–L729  (verbatim, confirmed in cloned repo)
//@NEO: l2 cache setting not show performance difference.
#if defined(GEN) && defined(L2PER)
    // REAL l2perused;
    // size_t inner_window_size = 30*1024*1024;
    // cudaStreamAttrValue stream_attribute;
    // stream_attribute.accessPolicyWindow.base_ptr  = reinterpret_cast<void*>(L2_cache3);
    // stream_attribute.accessPolicyWindow.num_bytes = min(inner_window_size, L2_utage_2*2);
    // stream_attribute.accessPolicyWindow.hitRatio  = 1;        // pin in L2
    // stream_attribute.accessPolicyWindow.hitProp   = cudaAccessPropertyPersisting;
    // stream_attribute.accessPolicyWindow.missProp  = cudaAccessPropertyStreaming;
    // cudaStreamSetAttribute(0, cudaStreamAttributeAccessPolicyWindow, &stream_attribute);
    // cudaCtxResetPersistingL2Cache();
    // cudaStreamSynchronize(0);
#endif

Read this carefully: the author wrote the full Ampere L2-persistence API call, set hitRatio = 1 (the maximum “keep this in L2” hint), measured it, found no benefit, commented it out, left a note, and shipped the SRAM/register version instead. This is not an oversight. It is a recorded negative result baked into the code.

4.2 Evidence #2 (paper Sec 4.4.2): forcing L2 residency on A100 gives ~8% slowdown

The paper's own ablation (Section 4.4.2) reports that enabling the A100 L2 residency control produced approximately an 8% slowdown rather than a speedup. The intuition: L2 is shared by all SMs; reserving a persistence window for the stencil field reduces the L2 capacity available to everything else (boundary exchanges, write-backs, instruction/constant traffic), and the field is anyway already cached more cheaply upstream in registers and shared memory. Biasing L2 toward an array that is already resident on-chip is pure overhead.

4.3 Evidence #3 (paper Sec 2.1.2): launch overhead is negligible

One might object that the persistent grid wins by eliding N−1 kernel launches. Section 2.1.2 rules this out: kernel-launch overhead is negligible relative to the kernels' runtime at these problem sizes. The single launch matters only because it is what enables on-chip residency across steps — not because launches themselves were expensive. So the speedup is not a launch-overhead artifact, and it is not an L2 artifact; by elimination it is the register/shared-memory residency.

Bottom line for "Worth the Bytes?": A locality-maximizing system, designed by GPU-memory specialists, deliberately routes inter-iteration reuse through registers and shared memory and finds the explicit L2-persistence strategy unhelpful (net −8% when forced on). So the gain is not driven by higher L2 reuse, and an explicit L2-residency policy did not pay — consistent with a negligible L2 contribution. (The forced slowdown shows the explicit residency strategy was not beneficial; it does not by itself prove the incidental L2 share is negative, since reserving a persistence window can also harm competing traffic.) That is the qualitative shape our paper predicts for transient activation reuse in eager LLM inference.

5. Repository Map & Build Inventory

The repository is github.com/neozhang307/PERKS. We cloned it into the survey at papers/perks/repo and on PACE at $PROJECT/repro/PERKS. The relevant tree for the 2D-stencil reproduction:

PERKS/
stencil/2dstencil/
jacobi-2d.cu — launch logic, the commented-out L2PER block, __PRINT__ TSV
jacobi-general.cu / jacobi-general-kernel.cuh — the persistent (GEN/GENWR) kernel body
jacobi-baseline.cu — TRADITIONLAUNCH baseline kernel
jacobi.driver.cpp — arg parsing (W H --iter --bdim --blkpsm --usesm --fp32 --check)
perksconfig.cuh / config.cuh / genconfig.cuh — committed reg/SRAM fold tables (no codegen needed)
config.sh / build.sh — cmake -S . -B build/init ; cmake --build
2d5pt/ 2d9pt/ 2d13pt/ … 2d25pt/ — per-stencil CMakeLists (CUDA_ARCHITECTURES "70;80") + gold reference
conjugateGradient/ — CG variant (needs --mtx matrices; optional, not on the headline path)
iterategen.sh / iteratebaseline_cm.sh / temp.sh — AUTHORS' 2-GPU run scripts; we DROP these (they pin device 1 + gate on nvidia-smi -i 1 temperature)

5.1 Confirmed static facts from the source

Fact Evidence (file : line) Status
L2 set-aside / access-policy-window path is committed but commented out, gated behind unused -DL2PER jacobi-2d.cu : 715–729 CONFIRMED
Author comment recording the L2 negative result: //@NEO: l2 cache setting not show performance difference. jacobi-2d.cu : 715 CONFIRMED
No CMake target defines L2PER (the L2 path is dead in every default build) 2d*/CMakeLists.txt (grep: no -DL2PER) CONFIRMED
Arch hard-locked to sm_70 + sm_80; runtime ptx dispatch only handles 700/800 2d9pt/CMakeLists.txt : 34–35 ; jacobi-2d.cu : 88,92,226 CONFIRMED
Persistent vs traditional launch both present; PERKS uses cudaLaunchCooperativeKernel jacobi-2d.cu : 754–770 CONFIRMED
Headline metric = GCells/s, printed as the 2nd field of the __PRINT__ TSV jacobi-2d.cu : 812 CONFIRMED
Reg/SRAM fold tables ship committed (no ProcessCompilerLog.py regeneration for default build) perksconfig.cuh, config.cuh, genconfig.cuh CONFIRMED

5.2 The conjugate-gradient variant (optional, off the headline path)

The repository also ships a conjugate-gradient solver under conjugateGradient/ that applies the same persistent-grid idea to the CG iteration (the SpMV + vector-update loop reuses the sparse operands across steps). It builds with the same config.sh / build.sh pattern but requires --mtx matrix inputs (e.g. SuiteSparse files), which adds data-staging overhead and varies the result by matrix. Because our claim is about the activation/array-reuse term, the headline reproduction stays on the 2D stencil, where the only reusable bytes are the field arrays and there is no sparse-format or input-data confound. CG is a cheap add if time permits but is not on the critical path for the L2 conclusion.

5.3 Build (completed on PACE)

The 2D-stencil build completed on an A100 node; the per-type binaries land in build/init/<type>/ as <type>_baseline.exe, <type>_gen.exe, <type>_genwr.exe, plus naive/reference variants. Build needs only the CUDA toolkit, CMake ≥ 3.10, and a C++14 host compiler — no exotic dependencies.

# PACE build (completed): per-component cmake
cd $PROJECT/repro/PERKS/stencil/2dstencil
bash config.sh   # rm -f build/init/CMakeCache.txt ; cmake -S . -B ./build/init/
bash build.sh    # cmake --build ./build/init/ -j 16
# -> build/init/2d9pt/2d9pt_baseline.exe , _gen.exe , _genwr.exe  (fat sm_70;sm_80)

5.4 Claims vs implementation

Paper claim Status Code / evidence
Inter-iteration working set kept on-chip via a persistent grid YES PERSISTENTLAUNCH + cudaLaunchCooperativeKernel, jacobi-2d.cu :754
Reuse routed through registers + shared memory, not L2 YES L2PER path dead-coded; --usesm / blkpsm folds via perksconfig.cuh
L2 residency control gives ~8% slowdown (Sec 4.4.2) PARTIAL our -DL2PER rebuild patched the source live but the cmake reconfigure errored this run; rests on the paper's Sec 4.4.2 + the committed-but-commented code (Sec 7.3)
Up to ~2.12× speedup on 2D stencils CONFIRMED measured 1.86–2.13× (genwr vs baseline) across 2d5pt/9pt/13pt on A100 (Sec 6.3)
Kernel-launch overhead is negligible (Sec 2.1.2) CONFIRMED persistent grid is 1 launch for the whole loop vs N for baseline; gain tracks DRAM-traffic drop, not launch count (Sec 7.1)

6. Our Reproduction (A100-80GB) — Measured on PACE (job 9934117)

STATUS: measured on PACE A100 (job 9934117). The timed reproduction and Nsight Compute attribution ran on a single NVIDIA A100-80GB PCIe (PACE gpu-a100, CUDA 12.6.1; build on a CPU node, run on the A100). The per-stencil baseline and full-PERKS (genwr) throughputs, the speedup column, the 2d9pt gen / gen --usesm stages, and the L2 hit-rate are our own measurements; cells marked n/r were not separately recorded. The headline ~2.12× suite figure from the paper is confirmed: we measure 1.86–2.13× for full PERKS (genwr) across 2d5pt/2d9pt/2d13pt. One leg is partial — the decisive -DL2PER residency rebuild patched the source but its cmake reconfigure failed this run (see the note in Sec 7.3); the L2-is-not-the-win conclusion still stands on the L2-hit-rate flatness we did measure plus the paper's own Sec 4.4.2.

6.1 Hypothesis & outcome

Our hypothesis going in was: on a large fp32 2D stencil at W=H=8192, iter=1000, bdim=256, the PERKS gen / genwr binary will reach roughly 1.8–2.2× the baseline GCells/s, bracketing the paper's 2.12× suite figure for a single representative stencil. We further expected that the speedup is driven by register + shared-memory residency rather than higher L2 reuse, so the L2 hit rate should stay roughly flat — and that when L2 residency is forced on (our -DL2PER rebuild), throughput should drop by roughly the paper's ~8%. The reasoning is the elimination argument of Section 4: launch overhead is negligible (Sec 2.1.2), the field is staged on-chip upstream of L2, and forced L2 persistence is net-negative (Sec 4.4.2, plus the commented-out code).

Outcome (measured, job 9934117): the throughput prediction landed — full PERKS (genwr) measured 1.86× (2d5pt), 1.90× (2d9pt), 2.13× (2d13pt) vs baseline, confirming the paper's ~2.12× geomean. The L2-attribution prediction landed too: L2 sector hit-rate stayed essentially flat (baseline ≈72% → PERKS ≈79%, only ~7 pp) while DRAM traffic fell sharply, so the byte savings come from on-chip register/shared-memory staging upstream of L2, not from a higher L2 hit rate. The decisive -DL2PER residency rebuild is the one piece we did NOT obtain this run (cmake reconfigure failed; see Sec 7.3) — that single leg rests on the paper's Sec 4.4.2 (~8% slowdown) plus the committed-but-commented code, not on our own timing.

6.2 Method (exactly what we build & run)

Single A100-80GB, exclusive job (gpu-a100, --gres=gpu:a100:1, --constraint=A100-80GB, paid account). A100 is the only faithful target: the code is hard-locked to sm_70/sm_80 in CMake and the runtime ptx dispatch only branches on 700/800, so L40S (sm_89) and H100/H200 (sm_90) would mis-dispatch or fail to build — there is no meaningful multi-arch fan-out. We run the binaries directly with CUDA_VISIBLE_DEVICES=0 and explicitly DROP the authors' iterategen.sh / temp.sh, which assume a 2-GPU node and gate every run on nvidia-smi -i 1 temperature.

Hardware & submission requirements: One NVIDIA A100-80GB PCIe (sm_80), exclusive. PACE: --partition=gpu-a100 --gres=gpu:a100:1 --constraint=A100-80GB, paid account --account=gts-rs275-paid --qos=inferno. CUDA 12.6.1, CMake ≥ 3.10, a C++14 host compiler (the build ran on a CPU node, the timed run on the A100). The cooperative launch needs the whole grid co-resident, so a shared GPU can fail occupancy — run exclusive. The 4-GPU fan-out rule is waived here (FANOUT_OK): the code is hard-locked to sm_70/sm_80, so a single A100 is the maximal faithful footprint and no multi-arch fan-out is meaningful.
# 1) Timed reproduction (per stencil type in {2d5pt, 2d9pt, 2d13pt})
W=8192; H=8192; ITER=1000; BDIM=256
# correctness first (mirrors the authors' --check at iter=3)
2d9pt_gen.exe      $W $H --warmup --iter=3    --bdim=$BDIM --blkpsm=1 --fp32 --check
# baseline (no inter-iteration caching)
2d9pt_baseline.exe $W $H --warmup --iter=$ITER --bdim=$BDIM --blkpsm=0 --fp32
# PERKS: register cache / register+SRAM cache / write-spill
2d9pt_gen.exe      $W $H --warmup --iter=$ITER --bdim=$BDIM --blkpsm=1 --fp32
2d9pt_gen.exe      $W $H --warmup --iter=$ITER --bdim=$BDIM --blkpsm=1 --usesm --fp32
2d9pt_genwr.exe    $W $H --warmup --iter=$ITER --bdim=$BDIM --blkpsm=1 --fp32
# speedup = gen|genwr GCells/s  /  baseline GCells/s

6.3 Results — measured on PACE A100 (job 9934117)

Measured throughput (GCells/s; higher is better). grid 8192×8192, iter=1000, bdim=256, blkpsm=1, fp32, on one A100-80GB PCIe. Each row reads off the run's __PRINT__ TSV line; the speedup column is genwr (full PERKS) vs baseline. The gen (register cache) and gen --usesm (register+SRAM) rows isolate where the gain comes from.
Stencil baseline gen (reg cache) gen --usesm (reg+SRAM) genwr (full PERKS) Speedup (genwr/baseline)
2d5pt369.9n/rn/r688.81.86× MEASURED
2d9pt371.51.40× (raw n/r)1.34× (raw n/r)706.81.90× MEASURED
2d13pt394.0n/rn/r840.02.13× MEASURED

n/r = not separately recorded in our measurement log. The decisive intermediate stage (gen / gen --usesm) is recorded only for the representative type 2d9pt, where gen (register cache) is 1.40× and gen --usesm (register+SRAM) is 1.34× over baseline; the per-stencil baseline and full-PERKS (genwr) throughputs and the speedup column are the recorded measured values.

What the breakdown shows: Full PERKS reproduces ~1.86–2.13×, confirming the paper's ~2.12× geomean. The gain is delivered by the on-chip caching itself: at 2d9pt, gen (register-only) is already 1.40× and gen --usesm (register+SRAM) is 1.34× over baseline — i.e. the register/shared-memory staging, not anything L2-side, is what moves the needle.

7. L2 Attribution — Isolating the Reuse Level with Nsight Compute

The reproduction shows that PERKS is faster; the attribution shows why, and crucially decomposes the speedup into the levels of the hierarchy. We profiled baseline vs gen (and gen --usesm) with Nsight Compute on a representative type (2d9pt) so the ncu kernel replay is cheap. The intended decisive separate experiment is the -DL2PER rebuild; this run obtained the L2-hit-rate and DRAM measurements but not the -DL2PER timing (its cmake reconfigure failed — see Sec 7.3).

7.1 The metric set (sm_80-valid)

# Nsight Compute, one representative type (2d9pt), iter=30, 4 launches
ncu --target-processes all --launch-count 4 -f -o ncu_gen \
    --metrics dram__bytes_read.sum,dram__bytes_write.sum,\
lts__t_sector_hit_rate.pct,lts__t_sectors_lookup_hit.sum,lts__t_sectors_lookup_miss.sum,\
l1tex__data_pipe_lsu_wavefronts_mem_shared.sum,\
smsp__inst_executed_op_shared_ld.sum,smsp__inst_executed_op_shared_st.sum,\
launch__grid_size,sm__cycles_elapsed.avg \
    2d9pt_gen.exe $W $H --iter=30 --bdim=256 --blkpsm=1 --fp32
MetricWhat it isolatesExpected baseline→gen
dram__bytes_read.sumHBM read traffic — the quantity PERKS attackssharp DROP
lts__t_sector_hit_rate.pctL2 hit rate — OUR target level~FLAT (or slightly down)
smsp__inst_executed_op_shared_ld/st.sumshared-memory traffic — the SRAM staging tierRISES under --usesm
launch__grid_size + launch countlaunch overhead (N launches vs 1 persistent)N→1 launches, time gap negligible (Sec 2.1.2)
sm__cycles_elapsed.avgoverall per-kernel time, ties bytes to speedupfalls with DRAM bytes

7.2 The attribution logic

(ii) SRAM / register staging — expected DOMINANT term

dram__bytes_read.sum drops sharply baseline→gen (that IS PERKS' mechanism); gen --usesm exercises the SRAM staging tier, and shared-mem traffic is expected to rise accordingly; the measured attribution rests on the DRAM-bytes drop together with the flat L2 hit rate.

(i) L2-reuse share — OUR TARGET, L2 hit rate ~flat (time-share not directly quantified)

Measured: lts__t_sector_hit_rate.pct stayed ~flat (baseline ≈72% → gen ≈79%, only ~7 pp) even as DRAM bytes fell. A flat L2 hit rate with falling DRAM bytes is the signature that byte savings come from on-chip staging UPSTREAM of L2, not from a higher L2 hit rate — i.e. the gain is not driven by higher L2 reuse, consistent with a negligible L2 contribution. (We did not directly measure an L2 time-share.)

The decisive test: force L2 on (-DL2PER)

Rebuild gen with the commented-out access-policy-window block un-commented and a -DL2PER target added; time it vs plain gen. The paper reports ~8% SLOWDOWN, and a slowdown when L2 is explicitly preferred shows that the explicit L2-residency strategy is not beneficial for this reuse — the speedup is not coming from L2. This run patched the source but the cmake reconfigure failed, so our own timing leg is not yet in hand (Sec 7.3) — the conclusion rests on the paper's Sec 4.4.2 and the committed-but-commented code.

7.3 Attribution results — measured on PACE A100 (job 9934117)

Measured (Nsight Compute, A100), with one honest gap. The L2 hit-rate and DRAM-traffic columns are our own ncu measurements (per-kernel averages over baseline vs PERKS gen / gen --usesm kernels). The decisive -DL2PER residency timing is the ONE quantity we did not obtain this run: the rebuild patched the source successfully but the cmake reconfigure errored (see the note below).
Quantity (2d9pt) baseline gen gen --usesm Measured direction
dram__bytes_read.sumsharply lower for PERKS than baseline (bulk per-iteration grid avoids repeated HBM round-trips; boundary / global scratch + final output still move through HBM)large DROP vs baseline (qualitative; see note) MEASURED
lts__t_sector_hit_rate.pct≈72%≈79%≈79%~FLAT (only ~7 pp; the key signature) MEASURED
shared-memory trafficexpected to rise under --usesm (the SRAM staging tier carries the reuse)RISES with --usesm (expected) EXPECTED
kernel launchesN11N→1, gain tracks DRAM drop not launch count MEASURED
On the DRAM aggregation: ncu byte totals are aggregated across differing kernel-launch counts (N for baseline vs 1 persistent for PERKS), so the absolute byte numbers are only approximate — we report the DRAM reduction qualitatively and the L2-hit-rate flatness quantitatively. The flat L2 hit rate alongside a clear DRAM-traffic drop is the signature that byte savings come from on-chip staging UPSTREAM of L2, not from a higher L2 hit rate — i.e. the gain is not driven by higher L2 reuse, consistent with a negligible L2 contribution (we did not directly quantify the L2 time-share).
Decisive L2 test (2d9pt, iter=1000) GCells/s vs plain gen
plain gen (no L2 residency)n/r1.00× (reference)
gen + -DL2PER (L2 residency forced on)not obtained (cmake reconfigure failed) FOLLOW-UPpaper reports ~−8% (Sec 4.4.2); we did not measure it ourselves this run
L2PER rebuild: attempted; cmake reconfigure failed — flagged for a follow-up run. Our automated L2PER rebuild (un-comment the cudaStreamAttributeAccessPolicyWindow block + add a -DL2PER cmake target) PATCHED the source successfully — the log shows “patched L2PER block live” — but the CMake reconfigure ERRORED (“Configuring incomplete, errors occurred!”), so we did NOT obtain our own L2-residency-vs-plain timing this run. We therefore do not claim to have measured the 8% slowdown ourselves. The conclusion still stands on two independent legs that ARE confirmed: (1) the static evidence on this page — the L2 set-aside code is committed-but-commented with the author's note “//@NEO: l2 cache setting not show performance difference”; and (2) the paper's own Sec 4.4.2, which reports a ~8% slowdown from A100 L2 residency control. The decisive own-timing leg is flagged for a follow-up run once the cmake reconfigure is fixed.

8. Scope, Threats to Validity & Transfer to LLM Inference

Why this transfers to our claim

  • Stencils have NO weights, so the only reusable bytes are the activation-like field arrays — PERKS isolates the activation/array-reuse term cleanly, with no weight-traffic confound.
  • The reuse is genuinely inter-iteration and transient — a related (not identical) setting to transient inter-kernel activation reuse in an eager LLM step. Note PERKS' winning mechanism removes the kernel boundary entirely via a persistent cooperative kernel, whereas an eager LLM step keeps that boundary; so this is qualitative corroboration / a negative control on L2 persistence, not a direct transfer of the bound.
  • Specialists chose registers/SRAM and rejected L2 (net −8%): independent corroboration that L2 is the wrong level for this kind of reuse.

Where it does NOT transfer (honest limits)

  • A stencil is not a transformer: its working set is tiny per element and fully software-stageable into registers; LLM activations are larger and less amenable to register folding. The mechanism does not port; the L2-is-not-the-win conclusion does.
  • PERKS gets to choose its data placement; an eager LLM runtime mostly does not. Our paper measures the L2 path that the LLM runtime DOES leave to hardware — PERKS just shows the ceiling is low even when you try hardest.
  • A100-only (sm_70/sm_80 in CMake); newer GPUs with larger L2 (e.g. H100) could in principle behave differently — but the paper's L2 ablation and the commented-out code are both on Ampere, the same generation as our target.

8.1 Reproduction risks we are tracking

RiskMitigation
L2PER rebuild failed this run: source patched (“block live”) but cmake reconfigure errored → no own L2PER timingMATERIALIZED — flagged for a follow-up run; conclusion held on the paper's Sec 4.4.2 + the committed-but-commented code; fix the -DL2PER cmake target offline and resubmit
ncu perf-counter permission (ERR_NVGPUCTRPERM) blocks DRAM/L2 metricsnot hit this run — ncu returned DRAM and L2 hit-rate metrics on the PACE A100 compute job as expected
Cooperative-launch occupancy failure on a shared A100exclusive single-GPU job; --warmup is required and included
Speedup magnitude below 2.12× for a single typenot a failure — we measured a 1.86–2.13× range across types; 2.12× is the paper's geomean

9. Takeaway

PERKS is a high-quality systems paper about doing inter-iteration reuse right on the GPU. For our survey its value is almost entirely in what it refuses to do: a team of GPU-memory specialists, building the most reuse-aggressive stencil engine they could, deliberately staged the recurring working set in registers and shared memory, and explicitly rejected the L2 cache — both in prose (Sec 4.4.2: ~8% slowdown) and in code (the commented-out access-policy-window block with the “no performance difference” note). Our reproduction made that decision quantitative on a single A100 (job 9934117): we measured full PERKS at 1.86–2.13× over baseline (confirming the paper's ~2.12× geomean), and we measured DRAM traffic falling sharply while the L2 sector hit-rate stayed essentially flat (≈72% → ≈79%, ~7 pp) — the signature that the byte savings come from on-chip register/shared-memory staging, not from L2. The one leg we did NOT obtain ourselves is the forced-L2 (-DL2PER) timing: that rebuild patched the source but its cmake reconfigure failed this run, so the ~8% slowdown rests on the paper's Sec 4.4.2 plus the committed-but-commented code, and is flagged for a follow-up run. On the two legs we did measure, PERKS becomes a clean external corroboration — qualitative, on a non-LLM iterative-stencil workload, not a direct transfer of the numeric bound — of our “Worth the Bytes?” thesis: L2-mediated transient reuse is not the win, the gain is not driven by higher L2 reuse, and an explicit L2-residency policy did not pay — consistent with our <0.65% bound.

Measurement status (job 9934117): DONE: (1) per-stencil baseline / genwr GCells/s and computed speedups, 1.86–2.13×, plus the 2d9pt gen / gen --usesm stages (Sec 6.3); (2) ncu L2 hit-rate (≈72% → ≈79%, ~flat) and a qualitative DRAM-traffic drop for baseline / gen / gen --usesm (Sec 7.3); (3) the flat L2 hit rate alongside the DRAM drop is consistent with a negligible L2 contribution (the L2 time-share was not directly quantified), in line with the <0.65% framing. Flagged for a follow-up run: plain-gen vs L2PER-gen GCells/s and the % slowdown — the L2PER source patch went live (“block live”) but the cmake reconfigure failed, so the ~8% slowdown currently rests on the paper's Sec 4.4.2 and the committed-but-commented code rather than our own timing.