Mirage Persistent Kernel (MPK)

Compiling an entire LLM decode step into ONE persistent megakernel — and what that proves about whether inter-kernel L2 reuse of activations is worth the bytes.

OSDI 2026 arXiv:2512.22219 Megakernel Kernel Fusion Decode Latency

Xinhao Cheng, Zhihao Zhang, Yu Zhou, Mengdi Wu, … Xupeng Miao, Tianqi Chen, Zhihao Jia

Carnegie Mellon University · Mirage Project

1. Overview — What MPK Is

In low-batch, latency-sensitive LLM decoding, the model is read once per token and almost nothing is reused across tokens.

MPK (Mirage Persistent Kernel) is a compiler and runtime, from the Mirage group at CMU, that attacks exactly these fixed costs.

The Problem

Per-kernel launch/dispatch overhead and inter-kernel HBM round-trips dominate the fixed cost of a low-batch decode step.

The Contribution

A compiler + runtime that auto-fuses a decode step's task graph into ONE persistent megakernel.

The Result

1.2×–6.7× lower decode latency (single-GPU ~1.0–1.7×), bounded by the WEIGHT roofline.

1.1 Where this page sits in “Worth the Bytes?”

Our measurement study makes one claim: transient inter-kernel L2 reuse of activations is worth less than 0.65% of step time, and MPK is the perfect adversary for that claim.

Our measurement (PACE H100, job 9934080). DONE. On a single PACE H100 we reproduce a 2.733× per-token decode speedup (16.047 → 5.872 ms/tok), attributable to launch-overhead elimination + reaching the weight roofline (≈4.84 ms/tok), not L2 activation reuse. Caveat: per-kernel ncu counters were NOT collected (profiler crashed after latency was saved), so attribution rests on a roofline argument.

2. Why MPK Is the Right Adversary

A measurement claim is only as strong as the hardest case it survives, and MPK fuses the entire decode step into one kernel.

2.1 The paper's own attribution

MPK attributes its gain to launch elimination and intra-kernel pipelining, and bounds latency by the WEIGHT roofline — not by activation reuse.

The weight roofline (the floor MPK cannot beat), illustrated:

$$t_{\text{step}} \;\ge\; t_{\text{weight roofline}} \;=\; \frac{B_{\text{weights}}}{\text{BW}_{\text{HBM}}} \;\approx\; \frac{16\,\text{GB}}{1.6\,\text{TB/s}} \;\approx\; 10\ \text{ms (A100)}$$

A single hidden vector is KB-scale; aggregated over all kernel boundaries, the inter-kernel activation traffic is MB-scale per token — either way multiple orders of magnitude smaller than the ~16 GB weight stream, so even perfect inter-kernel L2 reuse removes a sub-percent slice of the HBM traffic that sets the floor.

2.2 The logical chain we test

Premise

MPK is the maximal inter-kernel-fusion system: it removes every kernel boundary inside a decode step.

If the sceptic were right

Then a large share of MPK's speedup should come from activation residency / L2 reuse across the removed boundaries.

But the paper says

The gain is launch elimination + intra-kernel pipelining; the floor is the WEIGHT roofline — activation reuse is never invoked.

So we measure

Reproduce the speedup, then attribute it. If the L2-activation-reuse slice is < 0.65% even here, the claim survives its hardest case.
Important framing. This is NOT a critique of MPK. We use MPK as a measurement instrument: the cleanest realization of “all inter-kernel traffic fused away.”

3. Architecture — From Task Graph to One Kernel

MPK splits into a compiler that lowers a decode step into one megakernel's source, and a runtime that launches and drives it.

3.1 The compile-and-run pipeline

1
Define graph (Python)
rmsnorm_linear_layer, attention, all-reduce
2
Compile to task graph
mpk.compile() emits kernel_0.cu + task/event deps
3
Launch megakernel
One worker_kernel launch (+ scheduler_kernel)
4
Persistent loop runs step
Workers pull tasks; no return to host mid-step

3.2 Baseline vs megakernel: the kernel timeline

The baseline issues a long sequence of short kernels; MPK collapses them into one persistent kernel body.

Baseline (one decode step) — many launches, each with an HBM round-trip:

L
RMSNorm
L
QKV proj
L
RoPE
L
Attention
L
O proj
L
RMSNorm
L
MLP up/gate
L
MLP down

L = CPU-driven kernel launch; each blue block reads activations from HBM and writes them back. Repeated per layer.

MPK (one decode step) — ONE launch, one resident kernel:

L
worker_kernel — all layers' compute fused into one persistent kernel, so inter-kernel activation write-backs to HBM are eliminated

A single launch; a dispatch-only scheduler_kernel runs concurrently. No intra-step kernel boundaries.

The key architectural insight. MPK removes the launches and the small activation round-trips between kernels — not the big, unavoidable weight stream. That is why its floor is the weight roofline.

4. Repo Analysis

We work from the mpk branch, cloned into the survey tree. The build pulls three submodules, compiles two Rust crates, runs CMake/nvcc, and cythonizes the bindings.

4.1 Directory map

mirage/ (mpk branch)
demo/qwen3/ — the headline benchmark
demo.py — baseline vs --use-mirage; prints per-token latency, --save-tokens, --profiling
models/ — Qwen3 model definition (imported from cwd)
include/mirage/persistent_kernel/ — the megakernel runtime
persistent_kernel.cuh — launch logic, worker/scheduler kernels
runtime_header.h — RuntimeConfig (num_workers, split_worker_scheduler, …)
python/mirage/
utils.py — get_configurations_from_gpu (SM count → num_workers)
mpk/persistent_kernel.py, mpk/mpk.py — PersistentKernel Python API
src/search/ — Rust crates (abstract_subexpr, formal_verifier)
deps/ — cutlass, z3, json (git submodules; EMPTY until fetched)
setup.py — drives Rust + CMake/nvcc + Cython build
config.cmake — USE_CUDA ON, USE_CUDNN OFF, USE_FORMAL_VERIFIER OFF
analyze.sh, NCU_Usage_Manual.md — sudo-wrapped ncu (unusable on PACE; we replicate its sections directly)

4.2 Key files and their role

File / symbol Role Why it matters here
persistent_kernel.cuh:1799–1859 The launch fork: split path vs single-kernel path. Proves the single-GPU mechanism is ONE compute launch.
persistent_kernel.cuh:1617 split_worker_scheduler = true; Hardcoded true → single-GPU always uses worker + dispatch scheduler.
utils.py:33–53 get_configurations_from_gpu() SM count → workers. A100 (108) → 96; matches README.
demo/qwen3/demo.py:822 / :862 Per-token latency print, baseline / mirage. Our headline number comes from here.
analyze.sh Repo's ncu wrapper — uses sudo + --set full. Unusable on PACE (no sudo); we call ncu directly.

5. Core Mechanism (from the source)

Everything here is a verifiable static fact read directly from the cloned tree.

5.1 split_worker_scheduler is hardcoded true FACT

In init_persistent_kernel, the launch-selecting flag is set unconditionally to true at persistent_kernel.cuh:1617:

// include/mirage/persistent_kernel/persistent_kernel.cuh:1617
global_runtime_config.split_worker_scheduler = true;

That flag forks the launch. The else branch is reserved for the NVSHMEM / multi-GPU path; single-GPU always uses the split path:

// persistent_kernel.cuh:1799–1859  (launch fork)
if (global_runtime_config.split_worker_scheduler) {     // <-- single-GPU path
    // The split kernel does not support NVSHMEM ...
    worker_kernel<<<dim3(global_runtime_config.num_workers,1,1),
                    dim3(WORKER_NUM_THREADS,1,1),
                    MAX_DYNAMIC_SHARED_MEMORY_SIZE,
                    global_runtime_config.worker_stream>>>(global_runtime_config);

    scheduler_kernel<<<dim3(global_runtime_config.num_local_schedulers,1,1),
                       dim3(32,1,1), 0,
                       global_runtime_config.scheduler_stream>>>(global_runtime_config);
    // ... worker_kernel = the compute megakernel; scheduler_kernel = dispatch only
} else {
    // multi-GPU / NVSHMEM path — NOT used on single GPU
    persistent_kernel<<<dim3(num_sms_to_use,1,1), ...>>>(global_runtime_config);
    cudaDeviceSynchronize();
}

worker_kernel runs the compute; scheduler_kernel only dispatches. The megakernel of interest is worker_kernel.

// persistent_kernel.cuh:1385–1394
__global__ __launch_bounds__(WORKER_NUM_THREADS,1)
void worker_kernel(RuntimeConfig config) {
    worker_checker(config);
    execute_worker(config);          // <-- all layers' compute, stays resident
}
__global__ void scheduler_kernel(RuntimeConfig config) {
    scheduler_checker(config);
    execute_scheduler(config, 0);     // <-- dispatch only
}

5.2 num_workers = SM count, minus a few FACT

The worker count is derived from the SM count: 108 SMs (A100) → 96 workers, matching the README.

# python/mirage/utils.py:33–53
def get_configurations_from_gpu(rank):
    props = torch.cuda.get_device_properties(rank)
    sm_cnt = props.multi_processor_count
    if   sm_cnt >= 160: worker = 144
    elif sm_cnt >= 132: worker = 128   # H100 = 132 SMs -> 128 workers
    elif sm_cnt >= 108: worker = 96    # A100 = 108 SMs -> 96 workers (README config)
    elif sm_cnt >= 68:  worker = 64
    ...
    return worker, get_scheduler(sm_cnt, worker)

5.3 The headline number lives in the demo FACT

Both modes time with CUDA events. Baseline loops model.forward (many kernels); MPK calls mpk() once. Speedup = baseline_ms / mirage_ms.

# demo/qwen3/demo.py — baseline (:786–824, many kernel launches per step)
for cur_pos in range(prompt_len, decode_limit):
    logits = model.forward(input_ids=..., step=step, stream=stream)  # N kernels/step
    next_token = logits.argmax(dim=-1)
    ...
run_time = starter.elapsed_time(ender)
per_tok_ms = run_time / max(prompt_len + tokens_generated, 1)

# demo/qwen3/demo.py — MPK (:843–865, ONE megakernel call)
starter.record()
mpk()                                     # <-- the whole decode, one persistent kernel
ender.record(); torch.cuda.synchronize()
run_time = starter.elapsed_time(ender)
per_tok_ms = run_time / max(prompt_lengths[0].item() + tokens_generated, 1)
Read this for what it is. The source confirms the mechanism is structural: one launch instead of N, plus internal pipelining. Any resident activation is a side-effect of fusion, not the designed lever.

6. Claims vs Implementation

The table checks the paper's structural claims against the cloned source; the speedup is measured at 2.733× (Section 9).

Paper claim Status Code evidence
A decode step runs as ONE persistent kernel on a single GPU. YES split_worker_scheduler=true (cuh:1617) → one worker_kernel + one dispatch scheduler_kernel.
Fusion eliminates per-kernel launch/dispatch overhead. YES Baseline loops model.forward (N kernels); MPK calls mpk() once.
Single-GPU speedup is ~1.0–1.7×. 2.733× Measured on H100 (Qwen3-8B fp16, batch 1): 16.047 → 5.872 ms/tok, above the paper's quoted single-GPU band. The data support the 2.733× value; we do not claim a verified mechanism for why it exceeds the band (model, arch, and config differ from the paper's runs).
Latency is bounded by the WEIGHT roofline. YES MPK 5.872 ms/tok ≈ 1.21× the H100 weight roofline (≈4.84 ms/tok); baseline ≈ 3.3×.
The gain comes from inter-kernel activation / L2 reuse. NOT CLAIMED Paper never claims this; this is OUR hypothesis to bound.

Strengths (as a measurement instrument)

  • Maximal fusion — strongest setting for activation reuse to appear.
  • Self-attributing: the paper's own roofline predicts a small share.
  • Single-GPU repro needs no NVSHMEM/MPI (we used an H100).
  • Emits kernel_0.cu; perfetto export crashed this run, so attribution is roofline-only (no usable trace).

Limitations / repro caveats

  • Build is heavy; submodule fetch + cargo + CMake all need a networked compute node.
  • ncu may report aggregate counters — still a valid upper bound.
  • The profiler crashed in MPK's perfetto-trace export (after latency was saved), so no per-kernel counters AND no usable trace were collected; the as-run attribution is ROOFLINE-ONLY (DRAM-byte roofline, Section 9.3).
  • ncu serializes the concurrent kernels; use CUDA-event latency for timing.

7. Our Reproduction — Hypothesis & Method

DONE — measured on PACE H100 (job 9934080). We ran on a single H100 (not the A100 planned below). Measured speedup 2.733× (16.047 → 5.872 ms/tok). Section 7.1 keeps the a-priori hypothesis; Sections 9.2 + 9.3 carry the measured H100 numbers. ncu counters not collected (profiler crash) → roofline-based attribution.

7.1 Hypothesis — how we expect MPK's gain to decompose

Our a-priori prediction (written before the run) follows directly from the architecture. At batch 1 the decode step streams ~16 GB of weights once, which sets a weight-bandwidth floor neither engine can beat. The baseline pays that floor PLUS the launch/dispatch of N kernels per step PLUS the HBM round-trips of intermediate activations. MPK removes the launches and the activation round-trips and pipelines the resident work, but still pays the weight floor. So the speedup is the ratio of (floor + removable overhead) to (floor + residual). Within the removable part, the launch elimination should dominate; the activation/L2 reuse slice should be a sliver. A single hidden vector passed between two kernels at batch 1 is KB-scale (~hidden_size × 2 bytes ≈ a few KB); aggregated over all kernel boundaries in a step, the inter-kernel activation traffic is only O(MB) per token — either way orders of magnitude below the GB-scale weight stream. The H100 run bore this out: MPK lands at 1.21× the 4.84 ms/token weight roofline while the baseline sits at 3.3× it (Section 9.3).

Predicted attribution of the MPK speedup PRED

Launch / dispatch
Weight roofline
SRAM
Prefetch
Launch/dispatch elimination Reaching the weight roofline (not improved weight locality) SRAM / shared staging Prefetch / latency hiding L2 activation reuse (<0.65%, the target)

Illustrative a-priori hypothesis. The red L2 slice is bounded below 0.65% by the Section 9.3 roofline byte-ratio argument — not by ncu counters (not collected; profiler crash). The realized attribution is the Section 9.3 roofline table.

7.2 Method — what we build and run on PACE

Hardware (as run): one PACE H100 (sm_90, 132 SMs → 128 workers), built with CUTLASS kernels (sm_90a, TMA). Model: Qwen3-8B fp16, batch 1, greedy, max_num_batched_tokens=8. Reported metric is steady-state per-token decode latency.

The two headline runs

# cd $REPO/demo/qwen3  (demo imports models.* from cwd)
# BASELINE — native Triton/FlashInfer, many kernel launches per step
# both runs decode to natural EOS (baseline stops at 64 tokens, MPK at 326) —
# do NOT cap with --max-new-tokens, or you cannot reproduce the 326-token MPK run
python demo.py --prompt "<fixed prompt>" \
       --save-tokens $RESULT_DIR/outputs/baseline.json

# TREATMENT — single fused megakernel; mpk() does the whole decode
python demo.py --use-mirage --prompt "<fixed prompt>" \
       --save-tokens $RESULT_DIR/outputs/mirage.json

# speedup = baseline.latency_ms_per_token / mirage.latency_ms_per_token  (measured 2.733x on H100)

Profiling artifacts (repo's own entrypoint)

# --profiling attempts a tg4perfetto trace + task_graph_0.json + kernel_0.cu
#  in our run perfetto export crashed, so no usable trace was collected (attribution is roofline-only)
# (a short --max-new-tokens here is only an illustrative cap for a cheap profiled run,
#  NOT the headline runs above, which decode to natural EOS)
python demo.py --use-mirage --profiling --max-new-tokens 4 \
       --prompt "<fixed prompt>" --trace-name $RESULT_DIR/mpk_trace
Hardware & account (as run). PACE gpu-h100, sm_90 (132 SMs → 128 workers), paid account, single GPU (FANOUT_OK). The H100 roofline (≈4.84 ms/token) grounds Section 9.3. A100 is a valid secondary arch we did not need.

8. L2 Attribution — Isolating the Activation-Reuse Slice (Planned ncu Method)

Planned method — NOT executed. Planned ncu method, written in the subjunctive. It was NOT executed: MPK's profiler crashed during perfetto export (after latency was saved), so the per-kernel ncu counters below were never collected and no L2 hit rate was measured. The attribution actually reported is the ROOFLINE argument in Section 9.3; the L2 slice is BOUND, not FACT.

The plan was to split the measured MPK speedup into five buckets and isolate the fifth: (i) kernel-launch/dispatch elimination, (ii) SRAM / shared-memory staging, (iii) prefetch / latency hiding, (iv) reaching the weight-HBM-traffic roofline (weight-roofline-bound — MPK does NOT reduce weight traffic), and (v) inter-kernel L2 ACTIVATION reuse — and to show that (v) is < 0.65% of step time. Because the single-GPU megakernel-of-interest is worker_kernel (Section 5.1), and the repo's analyze.sh sudo-wraps ncu in a way PACE forbids, we would invoke Nsight Compute directly with explicit metrics that replicate its memory sections. (This counter-based pass did not complete; see the banner above.)

8.1 The Nsight metrics that separate the buckets

Bucket Nsight metric(s) What it isolates
DRAM (weight) traffic dram__bytes_read.sum, dram__bytes_write.sum, dram__bytes.sum If MPK's DRAM bytes ≈ 16 GB weights, the weight traffic is unchanged — MPK is weight-roofline-bound (reaches the floor without reducing weight traffic), not winning on activation reuse.
L2 reuse (the target) lts__t_sector_hit_rate.pct, lts__t_sectors_lookup_hit.sum / ...lookup_miss.sum L2 hit rate on activation footprints — bounds the slice.
SRAM / shared l1tex__t_sector_hit_rate.pct, l1tex__data_bank_conflicts_pipe_lsu_mem_shared_op_ld.sum On-chip staging and bank conflicts.
Launch / occupancy launch__grid_size, launch__block_size, launch__waves_per_multiprocessor, sm__warps_active... One grid (MPK) vs many grids (baseline).
Timing / throughput gpu__time_duration.sum, sm__throughput..., gpu__compute_memory_throughput... Cross-check; CUDA-event latency is authoritative for wall-clock.

8.2 The two profiling runs we would have issued

# C1: MPK — profile the single compute megakernel worker_kernel (one launch)
ncu --target-processes all --kernel-name "regex:worker_kernel" --launch-count 1 \
    --metrics "$NCU_METRICS" --csv --export $RESULT_DIR/ncu/mpk_worker_kernel.ncu-rep \
    python demo.py --use-mirage --max-new-tokens 4 --prompt "<fixed prompt>"

# C2: baseline — capture one steady decode step's full Triton/FlashInfer kernel mix
ncu --target-processes all --launch-count 60 --launch-skip 40 \
    --metrics "$NCU_METRICS" --csv --export $RESULT_DIR/ncu/baseline_kernels.ncu-rep \
    python demo.py --max-new-tokens 4 --prompt "<fixed prompt>"

8.3 The attribution arithmetic the counters would feed

(a) Launch share

(# baseline kernels/step × per-launch overhead) would be read from C2, vs MPK's single launch.

(b) Weight-roofline share

Would compare MPK dram__bytes.sum to the ~16 GB/step weight roofline. If MPK DRAM ≈ weights, the weight traffic is unchanged — MPK is weight-roofline-bound, not winning on activation reuse.

(c) Activation / L2 reuse share — THE TARGET

Would be bounded above by (baseline act. DRAM − MPK act. DRAM)/step ÷ total step bytes. (Not collected; realized bound is the Section 9.3 roofline byte-ratio.)

$$\text{share}_{\text{L2-act}} \;\le\; \frac{B^{\text{base}}_{\text{act}} - B^{\text{mpk}}_{\text{act}}}{B_{\text{step}}} \;\approx\; \frac{O(\text{MB})}{O(\text{GB})} \;\ll\; 0.65\%$$

(d) SRAM / prefetch residual

The residual would be read from l1tex hit rate + bank-conflict counters + occupancy.
Why the upper bound is robust. If ncu reports aggregate (not per-task) counters for the persistent megakernel — likely, since worker_kernel internally loops the whole decode — then worker_kernel's total DRAM is still a hard UPPER bound on activation traffic. An upper bound on the bytes is sufficient for an upper bound on the reuse share, which is all the “Worth the Bytes?” claim needs. As it happened the counter pass never completed (the profiler crashed in perfetto export, taking the trace with it), so the realized fallback is ROOFLINE-ONLY: the DRAM-byte / weight roofline of Section 9.3.

9. Results (updating as PACE runs land)

Measured on PACE H100 (job 9934080). 2.733× reproduced; gain attributed to launch-overhead elimination + the weight roofline, not L2 reuse. FACT cells are measured; the L2-reuse row is a roofline-derived BOUND because per-kernel ncu counters were not collected.
2.733×
FACT MPK speedup (H100)
5.872 ms
FACT MPK per-token latency
≈4.84 ms
FACT Weight roofline / token (H100)
< 0.65%
BOUND L2 activation-reuse share

9.1 Headline reproduction

Quantity Baseline MPK Source
Per-token decode latency (ms) 16.047 FACT 5.872 FACT demo.py latency_ms_per_token
Speedup (base / MPK) 2.733×  FACT 16.047 / 5.872
Tokens generated to natural EOS 64 326 differs → per-token latency is the fair metric
Kernels launched / decode step N (many) 1 (worker_kernel) FACT cuh:1813
Honest caveat on the token counts. The two runs hit natural EOS at different token counts (baseline 64, MPK 326), so we compare steady-state PER-TOKEN decode latency, not wall-clock totals. The 2.733× is 16.047 / 5.872 ms/tok.

9.2 Attribution (the headline of this study)

Bucket Our evidence (H100) Verdict Basis
Launch / dispatch elimination N kernels/step → 1 launch; 16.047 → 5.872 ms/tok. dominant FACT latency + cuh:1813
Weight-traffic roofline 5.872 ms/tok ≈ 1.21× the 4.84 ms/tok weight floor. near weight roofline FACT 16.2 GB / 3.35 TB/s
KV-cache reads + compute + scheduler residual ≈ 1.03 ms/tok above the floor. small residual BOUND MPK ms/tok − roofline
Inter-kernel L2 ACTIVATION reuse ~16 GB weights vs O(MB) activations/token; saving bounded by act/total. < 0.65% BOUND roofline byte ratio (no ncu)
Roofline attribution, not ncu counters. We did not obtain per-kernel ncu counters — the profiler crashed during MPK's perfetto export after latency was saved. Our attribution rests on a ROOFLINE argument (Section 9.3), not ncu counters, and we say so plainly.

9.3 The roofline arithmetic (H100) FACT

The 2.733× comes from launch-overhead elimination + intra-kernel pipelining moving the step down to the weight roofline — not from L2 activation reuse.

$$t_{\text{weight roofline}} = \frac{B_{\text{weights}}}{\text{BW}_{\text{HBM}}} = \frac{16.2\,\text{GB}}{3.35\,\text{TB/s}} \approx 4.84\ \text{ms/token (H100)}$$

QuantityValueInterpretation
Weight roofline / token4.84 msthe floor neither engine can beat
MPK measured / token5.872 ms≈ 1.21× roofline → near the bound
Baseline measured / token16.047 ms≈ 3.3× roofline → launch/dispatch-bound
Inter-kernel activations / tokenO(MB)vs ~16 GB weights → << 1% of step bytes

Baseline (16.047 ms/tok) is ~3.3× roofline → launch/dispatch-bound; MPK (5.872 ms/tok) is ~1.21× the weight roofline. Even 100%-L2-served inter-kernel activations save < act/total ≪ 1% — so the gain is launch elimination + reaching the weight roofline, not L2 reuse, corroborating our <0.65% bound.

10. How to Reproduce

10.1 Build (one-time, on a compute node with network)

# shallow clone leaves deps/ empty — submodule fetch is MANDATORY
cd $REPO    # mirage, mpk branch
git submodule update --init --recursive       # cutlass, z3, nlohmann/json
module load cuda/12.6.1                        # fallback 12.1.1; provides nvcc + ncu
conda create -y -p $ENV python=3.11 && conda activate $ENV
pip install "torch>=2.4" && pip install -r requirements.txt
export CUDACXX=$(which nvcc)
pip install -e . -v                            # Rust crates + CMake/nvcc + Cython
python -c "import mirage as mi; print('mirage OK')"

10.2 Run (headline + profiling + attribution)

cd $REPO/demo/qwen3
# headline runs decode to natural EOS (baseline 64 tokens, MPK 326) — no --max-new-tokens cap
python demo.py --prompt "$P" --save-tokens $OUT/baseline.json
python demo.py --use-mirage --prompt "$P" --save-tokens $OUT/mirage.json
# speedup = baseline.latency_ms_per_token / mirage.latency_ms_per_token
# profiled / ncu runs use a short --max-new-tokens 4 cap (cheap; illustrative, not the headline)
python demo.py --use-mirage --profiling --trace-name $OUT/mpk_trace --max-new-tokens 4 --prompt "$P"
ncu --kernel-name "regex:worker_kernel" --launch-count 1 --metrics "$NCU_METRICS" \
    --csv --export $OUT/ncu/mpk_worker_kernel.ncu-rep python demo.py --use-mirage --max-new-tokens 4 --prompt "$P"

10.3 Key configuration

ParameterValueNote
--modelQwen/Qwen3-8Bfp16, ~16.2 GB weights → ~4.84 ms/tok roofline on H100 (~10 ms on A100)
--use-mirageon / offoff = baseline; on = megakernel
--max-new-tokensEOS / 4headline runs decode to natural EOS (uncapped: baseline 64, MPK 326); 4 is a short cap for cheap profiled/ncu runs
--temperature0.0greedy → deterministic output
num_workers128 (H100)auto from 132 SMs (H100, as run); 96 on A100
Counter permissions. The repo's analyze.sh runs ncu under sudo, which is unavailable on PACE. We call ncu directly. As run, the profiling pass crashed in MPK's perfetto-trace export (after the headline latency was already saved), so neither per-kernel cache counters nor a usable trace were collected; the realized attribution is ROOFLINE-ONLY (the DRAM-byte / weight roofline of Section 9.3), not a trace-based or counter-based one.