Pushing the Performance Envelope of DNN-based Recommendation Systems Inference on GPUs — a software-only cross-kernel L2-residency technique for embedding rows, and why its own ablation makes it a small incremental slice.
Rishabh Jain, Vivek M. Bhasi, Adwait Jog, Anand Sivasubramaniam, Mahmut T. Kandemir, Chita R. Das
The Pennsylvania State University · University of Virginia
This page is part of the measurement study “Worth the Bytes?”, which argues that transient inter-kernel L2 reuse of activations in eager-mode LLM inference is worth less than 0.65% of step time. DLRM L2-pinning (L2P) is the most direct counterpart to that mechanism we could find in published systems work.
A dedicated pre-kernel issues PTX prefetch.global.L2::evict_last on hot rows, then a stream sync, then the consumer reads those rows from L2 — the same cross-kernel L2-residency phenomenon, though the data semantics differ (persistent curated rows vs transient activations).
The paper's headline is up to 62% embedding-kernel / 48% e2e, but on the medium-reuse profile L2P adds only ~13.5% on top of register-prefetch+occupancy tuning.
Reuse here is hand-engineered from a 60K hot-index list. We have no clean pin-charged net wall-clock result; the usable evidence is the L2-layer Nsight attribution (L2P ≈ RPF in steady state) plus the paper's own ~13.5%-incremental figure — consistent with our <0.65% bound.
Deep-learning recommendation models (DLRM) interleave dense MLPs with very large embedding tables. At inference the dominant cost is the EmbeddingBag gather-reduce, a stream of irregular HBM reads that is latency-bound on the memory system.
Embedding tables keep growing under tight SLAs. A single EmbeddingBag with pooling factor 150 over twelve 500K×128 fp32 tables issues hundreds of thousands of scattered row reads per batch, most of which miss every cache and go to HBM.
The working set far exceeds the A100's 40 MB L2, and shared memory is tiny. The repo even ships shared-memory and L1-prefetch variants that the paper does not adopt — evidence the SRAM route was tried and found wanting.
Popularity is skewed, so a small hot slice dominates lookups, but the baseline kernel cannot keep it resident across launches. The paper's three software-only techniques raise occupancy, hide latency in-kernel, and keep the hot slice warm in L2 across kernels — the last being our mechanism.
-maxrregcount 42.lv0/lv1 — intra-kernel latency hiding, no cross-kernel state.prefetch_pin_L2 kernel issuing prefetch.global.L2::evict_last on hot rows. This is the cross-kernel L2-residency mechanism our study measures.L2P uses two kernels separated by a stream sync, inside one ATen op. A producer pins hot rows in L2; a consumer EmbeddingBag kernel reads them. The producer computes nothing — it only issues prefetch hints that change L2 replacement state.
From opt_designs/l2_pinning/EmbeddingBag.cu, lines 131–148. The evict_last hint biases the replacement policy toward later eviction (advisory, not guaranteed):
template <typename scalar_t, typename index_t>
__global__ void prefetch_pin_L2(const scalar_t *weight, int64_t featureSize,
int64_t weight_stride0,
int64_t const* d_hot_idx_vec, size_t pinning_size) {
int Xidx = blockIdx.x * blockDim.x + threadIdx.x;
int Yidx = threadIdx.y;
int stride = gridDim.x * blockDim.x;
if (Yidx < featureSize) {
for (int i = Xidx; i < pinning_size; i += stride) {
int64_t const lv_hot_idx = d_hot_idx_vec[i]; // a hot row id
const scalar_t *weightFeat = weight + Yidx;
// insert the L2 prefetch inline PTX code
asm volatile (
"prefetch.global.L2::evict_last [%0];" ::"l"(&weightFeat[lv_hot_idx * weight_stride0])
:);
}
}
}
The hot-index list is read at runtime from DATASET_FILE_PATH (lines 447–471); each reuse profile points at its own 60,000-line hot-index file.
The launch sequence, lines 494–517. The timer position is the single most important measurement detail on this page:
} else {
// cudaEventRecord(start, stream); <-- commented out
// reduce_L2_size<<<grid, 256, 0, stream>>>(d_dummy_vec, size_dummy);
dim3 block_2 = dim3(2, 128);
prefetch_pin_L2<scalar_t, index_t><<<grid, block_2, 0, stream>>>( // PRODUCER: pin hot rows
weight.const_data_ptr<scalar_t>(), featureSize, weight.stride(0),
d_hot_idx_vec, pinning_size);
cudaStreamSynchronize(stream); // wait for the pin to finish
// cudaEventRecord(stop, stream); <-- commented out
// cudaEventSynchronize(stop); <-- commented out
cudaEventRecord(start, stream); // <== TIMER STARTS HERE, AFTER THE PIN
EmbeddingBag_updateOutputKernel_sum_mean<scalar_t, index_t><<<grid, block, 0, stream>>>(
/* CONSUMER: reads the now-warm rows from L2 */ ... );
cudaStreamSynchronize(stream);
cudaEventRecord(stop, stream);
cudaEventSynchronize(stop);
cudaEventElapsedTime(&time, start, stop); // time = CONSUMER ONLY
}
cudaEvent timer brackets only the consumer kernel; the pin kernel and its sync run before cudaEventRecord(start). The reported latency measures the L2-hit benefit while excluding the cost of producing it. A fair accounting must re-charge the pin time.
Ampere exposes two ways to bias L2 retention: the cudaStreamAttrValue access-policy-window (APW) API, or the per-instruction hint prefetch.global.L2::evict_last (warms a line and biases it toward later eviction). The core L2 trick is the per-instruction hint — finer-grained for a scattered hot-index list — but the measured wrapper also re-establishes an L2 access-policy window per batch (see §5) on top of re-reading the 60K hot-index file, so the artifact as run is PTX prefetch PLUS per-batch APW setup. The hint is advisory: demand traffic between pin and consume can evict hot rows before reuse, bounding the realized benefit.
L2P targets the middle level — the device-wide L2 — so a later, independent kernel launch hits it:
Each technique is a hand-written replacement of aten/src/ATen/native/cuda/EmbeddingBag.cu, recompiled into a from-source PyTorch v2.1.0. The optimization lives entirely in the CUDA kernel and the build flags.
| Symbol | File : lines | Role |
|---|---|---|
prefetch_pin_L2 |
l2_pinning/EmbeddingBag.cu : 131–148 |
Producer: emits the L2 prefetch PTX on each hot row. |
EmbeddingBag_updateOutputKernel_sum_mean |
l2_pinning/EmbeddingBag.cu : 153 |
Consumer: the gather-reduce that benefits from warm L2. |
reduce_L2_size |
l2_pinning/EmbeddingBag.cu : 116–127 |
Abandoned cache-thrash kernel (call site commented out). |
lv0, lv1 |
prefetching/EmbeddingBag_regpref.txt : 141–188 |
RPF: register temporaries holding the prefetched value (intra-kernel). |
cudaEventRecord(start) |
l2_pinning/EmbeddingBag.cu : 502 |
Timer start — AFTER the pin + sync. Consumer-only timing. |
The hotness sweep is what makes this artifact a stress test for our claim:
| Profile | Access trace (lines) | Hot-index file (rows) | Expected reuse |
|---|---|---|---|
| reuse_high | 3,933,215 | 60,000 | High |
| reuse_medium | 429,951 | 60,000 | Medium |
| reuse_low | 379,910 | 60,000 | Low |
| random | 3,999,999 | 60,000 | ~None |
The Python side is thin: save_emb.py regenerates test_500K_12.pt (~3 GB), load_emb.py builds the EmbeddingBag and replays the trace, and the swapped CUDA kernel prints its own cudaEvent latency. parser_Kernel_ET.py averages over 12×12 launches and drops the first (cold-miss) sample — so the reported number already assumes a warm-ish cache, itself favorable to the reuse story.
The page's whole point is to isolate the L2-residency contribution. The ladder builds five variants across four profiles; differencing adjacent rungs attributes each technique's marginal effect to a hardware cause. Only the non-pinning rungs (1–3) give a clean wall-clock difference; the L2P rungs (4–5) are artifact-dominated (§5), so the L2P attribution below is read NOT from §4.1 latency but from the Nsight L2 comparison (§7) plus the paper's ablation:
| Difference | Attributed cause | L2 reuse? |
|---|---|---|
| OptMT − baseline | Occupancy / register budget | NO |
| RPF − OptMT | In-kernel register prefetch | NO |
| L2P − OptMT via §7 | Cross-kernel L2 residency (read from §7, not §4.1 wall-clock) | YES |
| combined − RPF via paper | L2P increment over RPF (paper's ~13.5%, not our wall-clock) | YES |
Variants 1–3 (no pinning path) are clean FACTs measured directly. Variants 4–5 (the L2P / combined pinning path) are dominated by a per-batch pinning-setup harness ARTIFACT and are NOT reported as L2P inference latency — see §5 and read the L2P contribution from the Nsight L2 attribution in §7.
| Variant | reuse_high | reuse_medium | reuse_low | random |
|---|---|---|---|---|
| 1 baseline | 3.616 fact | 4.699 | 5.680 | 5.875 |
| 2 + OptMT | 3.598 fact | 4.719 | 5.660 | 5.855 |
| 3 RPF + OptMT | 3.693 fact | 4.513 | 5.052 | 5.162 |
| 4 L2P + OptMT | 99.8 artifact | 102.4 | 5.901† | 102.5 |
| 5 RPF + L2P + OptMT | 97.5 artifact | 100.6 | 5.239† | 101.6 |
† The reuse_low cells in variants 4–5 are fast only because l2_quick.sh's hot-path list for that profile has a leading-space path bug → the hot-index file fails to open → pinning is silently skipped. Not a real L2P measurement either.
The clean rungs (1–3) show the validating shape: a strong reuse gradient (baseline hot 3.616 ms vs random 5.875 ms, ~1.6×, consistent with cache/hotness effects — the L2-specific share is attributed by §7, not this wall-clock), OptMT alone essentially flat (3.616 → 3.598 ms hot), and RPF+OptMT helping the cache-UNfriendly profiles (low −11%, random −12%) by hiding the DRAM latency L2 cannot when there is no reuse.
Recall the confirmed fact from §2.2: the timer excludes the pin kernel. The repo's reported L2P speedup is therefore
$$\text{Speedup}_{\text{repo}} = \frac{T_{\text{baseline}}}{T_{\text{consumer}}^{\text{L2P}}}$$
which charges nothing for producing the warm L2 state. The pin kernel is not free: it touches all 60,000 hot rows in HBM, incurring real traffic and launch+sync overhead. In load_emb.py's harness that setup cost is re-paid every batch and cannot be cleanly separated into a one-time pin time — so rather than report a per-batch setup artifact as a net pin-charged wall-clock, we read the L2P contribution from the Nsight L2-layer attribution (§7) plus the paper's own ~13.5% ablation figure.
Across every profile the pin setup and the consumer launch cannot be cleanly separated into a one-time pin time plus an amortized steady-state consumer time, so there is no honest net (pin-charged) wall-clock speedup to tabulate — the values would all be per-batch setup artifacts.
In this harness the pin setup is re-paid every batch and cannot be separated into a one-time pin time, so any net wall-clock speedup here would be a setup-cost artifact. The L2-layer answer is in §7: at zero pressure, L2P adds essentially nothing over RPF (91.7% vs 91.8% L2 hit on hot).
We hypothesized the genuine cross-kernel L2-residency contribution is small and hotness-dependent; the measurements confirm both. (1) The L2 payoff is entirely hotness-dependent: ~92% hit / ~7.8 MB DRAM on reuse_high vs ~30% hit / ~145 MB DRAM (≈18× HBM) on random (FACT, §7). (2) L2P adds essentially nothing over RPF at the L2 layer in this no-polluter microbench (91.7% vs 91.8% hit) — consistent with the paper's ~13.5%-incremental figure (BOUND) and a parallel to our ‘dirty blob survives intact at zero pressure’ result.
Single A100-80GB (sm_80), single-GPU, no TP. We keep the artifact's arch scope (TORCH_CUDA_ARCH_LIST="8.0", -maxrregcount 42); the repo has no H100 flags, so A100 is the only bit-faithful target. Each of the five variants is a separate from-source compile.
These are read directly from the artifact and need no GPU run:
prefetch.global.L2::evict_last inline PTX on hot rows (l2_pinning/EmbeddingBag.cu:144).After patching the hardcoded paths and regenerating the model:
# 0. regenerate the gitignored model (12 x 500K x 128 fp32, ~3 GB)
cd scripts && python save_emb.py
# 1. baseline: build with NO maxrregcount, then sweep the 4 profiles
cp opt_designs/baseline/EmbeddingBag_timed.cu \
pytorch/aten/src/ATen/native/cuda/EmbeddingBag.cu
TORCH_CUDA_ARCH_LIST="8.0" python setup.py install
bash quick.sh > dummy; grep 'Latency of Embedding Bag kernel' dummy \
| cut -d' ' -f6 | cut -d'm' -f1 > log; python parser_Kernel_ET.py
# 2-3. OptMT / RPF: rebuild with CMAKE_CUDA_FLAGS="-maxrregcount 42", rerun quick.sh
# 4. L2P: rebuild with l2_pinning .cu, run l2_quick.sh (it sets DATASET_FILE_PATH)
cp opt_designs/l2_pinning/EmbeddingBag_timed.cu \
pytorch/aten/src/ATen/native/cuda/EmbeddingBag.cu
CMAKE_CUDA_FLAGS="-maxrregcount 42" python setup.py install
bash l2_quick.sh > dummy; ... # 60K hot rows pinned per profile
# 5. combined (RPF+L2P+OptMT): rebuild with combined .cu, run l2_quick.sh
Layer A (latency diffs) tells us how big each rung is; Layer B (Nsight Compute) tells us why. The validating test is to profile RPF+OptMT vs. L2P+OptMT on the hot and the random profile — comparing register-prefetch (RPF+OptMT) against explicit pinning (L2P+OptMT) as two alternative consumer-side strategies, i.e. whether explicit L2 pinning beats register-prefetch at the L2 layer (baseline is NOT the Nsight comparand) — and watch two metrics: the L2 sector hit rate on the consumer kernel, and the HBM bytes read. If explicit L2P delivers more cross-kernel L2 reuse than register-prefetch, the hit rate must be higher and HBM reads lower on hot; and the hotness sweep itself (hot vs random) shows whether the L2 payoff exists at all.
| ncu metric | What it isolates |
|---|---|
lts__t_sector_hit_rate.pct | L2 hit rate — the headline reuse signal. |
lts__t_sectors_lookup_hit.sum / ...miss.sum | Absolute L2 hit/miss sectors. |
dram__bytes_read.sum | HBM read bytes — the traffic L2 reuse removes. |
smsp__inst_executed_op_shared_ld.sum | Shared-memory loads — ~0, rules out SRAM. |
sm__warps_active.avg.pct_of_peak_sustained_active | Occupancy — isolates OptMT. |
launch__registers_per_thread | Confirms maxrregcount 42 (= 42 for OptMT/RPF). |
gpu__time_duration.sum | Per-kernel time — cross-checks kernel durations; the L2P contribution itself is read from the L2-layer metrics, not from a wall-clock pin time. |
| Variant × profile | L2 hit% first→steady | DRAM read first→steady |
|---|---|---|
| reuse_high · RPF+OptMT | 74.0 → ~91.8 fact | 27.5 → ~7.8 MB |
| reuse_high · L2P+OptMT | 74.6 → ~91.7 fact | 27.5 → ~7.8 MB |
| random · RPF+OptMT | 30.4 → 30.5 fact | 144.6 → 144.9 MB |
| random · L2P+OptMT | 30.4 → 30.6 fact | 144.8 → 144.9 MB |
Raw: results/dlrml2pin_a100-80_9935235/ncu/*.csv. Warm replay across 4 launches; the first→steady columns show the warming trend.
Hot rows reach ~92% L2 hit / ~7.8 MB DRAM; random sits at ~30% hit / ~145 MB DRAM (≈18× more HBM). L2 reuse buys nothing without reuse — the embedding-table analogue of our activation bound.
L2P adds essentially nothing over RPF at the L2 layer here: 91.7 vs 91.8% hit, DRAM essentially equal. Consistent with the paper's ~13.5%-incremental figure and a parallel to our ‘dirty blob survives at zero pressure’ result — pinning matters only under cross-kernel pollution the tight loop does not impose.
This contrast — ~92% steady-state L2 hit on reuse_high collapsing to ~30% with ≈18× HBM traffic on random, and L2P ≈ RPF at the L2 layer — validates the thesis: the inter-kernel L2 benefit is a property of the access pattern, not the kernel pair, and explicit pinning adds nothing over register-prefetch without cross-kernel pollution.
Because the techniques live entirely in one swappable CUDA file, we can check each paper claim against the source. The status column reflects what the code does, cross-checked against our PACE A100 measurements where relevant.
| Paper claim | Status | Code evidence |
|---|---|---|
| L2P pins hot rows in L2 via a hardware prefetch hint. | YES | prefetch.global.L2::evict_last inline PTX (line 144). |
| L2P is a software-only, drop-in EmbeddingBag replacement. | YES | One file swapped into aten/.../EmbeddingBag.cu + a build flag; no driver/HW changes. |
| Reported kernel latency reflects the L2P benefit. | PARTIAL | True for the consumer, but the timer EXCLUDES the pin kernel + sync (lines 497–502). |
| L2P delivers up to 62% / 48% reduction. | PARTIAL | Best-case, consumer-only. Our ncu shows the L2 payoff is entirely hotness-dependent (~92% hot vs ~30% random) and L2P ≈ RPF at the L2 layer (91.7% vs 91.8%); the headline reflects warm best-case reuse, not the L2P increment alone. |
| L2P benefit vanishes without reuse. | YES | Confirmed by our ncu hot-vs-random test: pinning a random index list gives only ~30% L2 hit / ~145 MB DRAM (no benefit) vs ~92% / ~7.8 MB for hot rows. |
| RPF hides latency by prefetching (distinct from L2P). | YES | Stages into registers lv0/lv1 intra-kernel (regpref.txt:141–188); not L2 reuse. |
Cross-kernel reuse can be attempted at every level of the GPU memory hierarchy. Placing L2P among the alternatives clarifies why it is the right object of study for an inter-kernel L2 claim.
| Reuse level | Crosses kernel? | Representative | Why bounded |
|---|---|---|---|
| Registers | NO | RPF (lv0/lv1) | Dies at kernel exit. |
| Shared memory (SRAM) | NO | EmbeddingBag_shmem (tried) | Tiny, per-block, re-staged each kernel. |
| L2 cache | YES | L2P (evict_last) — OUR FOCUS | Persistent but small vs working set; pays only for re-demanded rows. |
| HBM / explicit staging | YES | Hot-row caching tables | Software-managed; outside our L2 question. |
Our study expresses the L2-residency contribution as a share of step time. For DLRM L2P we read that contribution from the measured L2 layer rather than from a wall-clock pin time.
Because the L2P wall-clock is a pinning-setup artifact (§5), we do NOT compute a single pin-charged L2 share; we read the L2-layer contribution from the Nsight attribution (§7) and the paper's ~13.5% figure. The argument is corroborating, not a proof: L2P is among the most favorable cases imaginable, yet even here explicit pinning adds essentially nothing over RPF at the L2 layer (91.7% vs 91.8%) and the L2 payoff collapses with hotness (~92% → ~30% hit, ≈18× HBM on random). For naturally-transient eager-LLM activations this is consistent with a small, conditional contributor — supporting the plausibility of the <0.65% bound.
| Quantity | Value |
|---|---|
| Steady-state L2 hit, reuse_high | ~92% (RPF & L2P alike); ~7.8 MB DRAM/batch fact |
| Steady-state L2 hit, random | ~30%; ~145 MB DRAM/batch (≈18× HBM) fact |
| Measured L2-layer delta, L2P vs RPF | ≈0 (91.7% vs 91.8% hit) fact |
| Paper consumer-side L2P increment (med reuse) | ~13.5% (paper-reported; performance/latency, not an L2 hit-rate delta) bound |
| Net pin-charged wall-clock L2 share | not reported (harness artifact, §5) artifact |
| vs the <0.65% eager-LLM bound | corroborates the bound's plausibility bound |