| name | mpk-internals |
| description | Reference guide for the MPK compilation-to-runtime pipeline. Use when asked how MPK works internally, how compilation/code generation works, what happens at runtime, or when debugging the megakernel scheduler. |
MPK Internals: Compilation-to-Runtime Pipeline
This document traces the full lifecycle of an MPK megakernel from Python graph construction through CUDA compilation to persistent kernel execution.
Pipeline Overview
Phase 1: Python Graph Building
PersistentKernel.compile()
→ layer methods build KNGraph/TBGraph
→ kn_graph.generate_task_graph()
|
v
Phase 2: C++ Code Generation (runtime.cc)
Graph::generate_task_graph()
→ register_mugraph() — builds task/event lists
→ print_task_graph() — emits CUDA code + JSON
|
v
Two artifacts:
test.cu — _init_persistent_kernel(), _execute_task(), Python C ext
task_graph.json — task descriptors, events, dependencies
|
v
Phase 3: CUDA Compilation
nvcc test.cu → test.so (Python extension module: __mirage_launcher)
|
v
Phase 4: Runtime Initialization
init_persistent_kernel()
→ loads JSON, allocates GPU queues, builds RuntimeConfig
|
v
Phase 5: Runtime Execution
launch_persistent_kernel()
→ prepare_kernel (reset queues)
→ worker_kernel + scheduler_kernel (persistent loop)
→ workers fetch tasks, wait on events, call _execute_task()
→ schedulers process events, enqueue tasks to workers
Phase 1: Python Graph Building
Key file: python/mirage/mpk/persistent_kernel.py
Entry point: PersistentKernel.compile()
The compilation method does the following in order:
-
Generate task graph — calls self.kn_graph.generate_task_graph(num_gpus, my_gpu_id) which bridges through Cython (python/mirage/_cython/core.pyx, generate_task_graph()) into C++. Returns {"cuda_code": str, "json_file": str}.
-
Write files — writes test.cu (CUDA code + HARD_CODE Python extension wrapper) and task_graph.json to a temp directory.
-
Compile — builds the nvcc command via get_compile_command() and calls subprocess.check_call().
-
Load module — uses importlib.util.spec_from_file_location() to dynamically load the compiled .so as Python module __mirage_launcher. Extracts init_func, launch_func, init_request_func, finalize_func.
-
Initialize runtime — calls init_func(...) with meta-tensor pointers, worker/scheduler counts, and serving config.
How layers build the graph
Each layer method (e.g., rmsnorm_layer, linear_layer, moe_w13_fp8_layer) does:
- Create a
TBGraph with CyTBGraph(grid_dim, block_dim, forloop_range, reduction_dimx)
- Call
tb_graph.new_input(dtensor, partition, forloop_dim, store_in_dmem) for each input and output
- Call
self.kn_graph.customized([tensors...], tb_graph) to register the operator
- Call
self.kn_graph.register_task(tb_graph, "task_name") which dispatches to C++ Graph::register_task()
HARD_CODE: the Python C extension wrapper
The HARD_CODE constant (top of persistent_kernel.py) is a C string appended to the generated CUDA code. It defines a Python extension module with four functions:
init_func — parses Python args, calls C++ init_persistent_kernel()
launch_func — takes a CUDA stream pointer, calls launch_persistent_kernel(stream)
init_request_func — calls init_request_resources() (for online serving)
finalize_func — calls finalize_persistent_kernel()
Layer API: TBGraph Partition Scheme
Each layer method (e.g., rmsnorm_layer, linear_layer, moe_w13_linear_layer) builds a TBGraph that describes how the global tensors are sliced into per-task tiles. This section explains every parameter.
CyTBGraph constructor
tb_graph = TBGraph(CyTBGraph(grid_dim, block_dim, forloop_range, reduction_dimx))
| Parameter | Meaning |
|---|
grid_dim | (x, y, z) — number of task instances in each dimension. Total tasks = x * y * z. |
block_dim | (threads, 1, 1) — threads per task. Must be (128,1,1) Ampere, (256,1,1) Hopper/Blackwell. |
forloop_range | Number of forloop iterations (always 1 in MPK — see note below). |
reduction_dimx | Tile size for the reduction dimension (always 64 in MPK). |
tb_graph.new_input() — registering a tensor
tb_graph.new_input(dtensor, input_map, forloop_dim, store_in_dmem)
Called for every tensor the task touches — both inputs and outputs. The first num_inputs calls register inputs; the remaining register outputs. This ordering must match num_inputs/num_outputs in graph.cc's task_config tuple.
input_map: the partition tuple
A 3-element tuple (mx, my, mz) that maps grid dimensions → tensor dimensions:
input_map.x value | Meaning |
|---|
-1 | grid_dim.x does not partition this tensor. Every task sees the full extent of every dimension. |
0 | grid_dim.x partitions tensor dimension 0. Task at grid position gx sees the slice [gx * dim[0]/grid_x : (gx+1) * dim[0]/grid_x] along dim 0. |
1 | grid_dim.x partitions tensor dimension 1. Same slicing logic on dim 1. |
2 | grid_dim.x partitions tensor dimension 2. |
input_map.y and input_map.z work identically for grid_dim.y and grid_dim.z.
In short: the value tells you which tensor dimension that grid axis splits. -1 means "don't split by this grid axis."
forloop_dim (vestigial in MPK)
In the Mirage superoptimizer, forloop_dim and forloop_range together control tiled reduction loops within a TBGraph. However, in MPK forloop_range is always 1, which makes forloop_dim a no-op — the dimension division (dim / 1) and stride multiplier (* 1) have no effect regardless of what value you pass.
MPK task kernels handle their own internal tiling and reduction directly in CUDA (e.g., looping over the K dimension in a matmul). The TBGraph forloop mechanism is not used. You'll see various forloop_dim values in existing layer methods (e.g., 1, 2, -1), but they're all equivalent when forloop_range=1. By convention, existing code sets forloop_dim to the "reduction dimension" of the operation, but this is cosmetic.
store_in_dmem
True — the per-task tensor slice lives in device (global) memory. Should be set to True for all MPK tensors.
Annotated example: moe_w13_linear_layer
def moe_w13_linear_layer(self, input, weight, moe_routing_indices,
moe_mask, output, grid_dim, block_dim):
tb_graph = TBGraph(CyTBGraph(grid_dim, block_dim, 1, 64))
tb_graph.new_input(input, (-1, -1, -1), 1, True)
tb_graph.new_input(weight, (-1, 1, -1), 2, True)
tb_graph.new_input(moe_routing_indices, (-1, -1, -1), -1, True)
tb_graph.new_input(moe_mask, (-1, -1, -1), -1, True)
tb_graph.new_input(output, (-1, 2, -1), -1, True)
self.kn_graph.customized([input, weight, moe_routing_indices, moe_mask, output], tb_graph)
self.kn_graph.register_task(tb_graph, "moe_w13_linear_sm100")
How partitioning connects to task pointers
At runtime, the partition tuple is resolved during task graph generation (src/threadblock/graph.cc). For each task instance (one grid coordinate), the code generator computes a byte offset from the tensor's base pointer:
per_task_ptr = base_ptr
+ blockIdx.x * stride_for(input_map.x)
+ blockIdx.y * stride_for(input_map.y)
+ blockIdx.z * stride_for(input_map.z)
These offsets are baked into the TaskDesc at init time (via JSON → FullTaskDesc → TaskDesc). The task kernel receives pre-offset pointers in task_desc->input_ptrs[i] and task_desc->output_ptrs[i] — this is why tasks are blockIdx-agnostic.
Phase 2: C++ Code Generation
Key file: src/kernel/runtime.cc
Entry point: Graph::generate_task_graph()
This function orchestrates all code generation:
-
register_mugraph() — walks the KNGraph operators and converts each into FullTaskDesc entries. For each KN_CUSTOMIZED_OP, it queries task_config[op] (a tuple of num_inputs, num_outputs, TaskType, variant_id set by Graph::register_task()) to determine the task type and variant. It also creates EventDesc entries for inter-task dependencies and populates first_tasks (the initial ready tasks).
-
print_task_graph() — generates two outputs:
Output 1: CUDA code containing three generated functions:
construct_task_graph() — loads task_graph.json at runtime, parses it into FullTaskDesc/EventDesc vectors, and creates TMA descriptors for Hopper/Blackwell tasks.
_init_persistent_kernel() — sets up tensor pointers from io_configs (torch tensors, cudaMalloc buffers, shuffled tensors, NVSHMEM buffers). Called once during initialization.
_execute_task() — a giant if/else dispatcher that maps (task_type, variant_id) pairs to the actual kernel function calls. Each branch contains the code string generated by the corresponding TaskRegister::register_*_task() function.
Output 2: JSON task graph — serializes all tasks, events, and dependencies (see JSON Schema section below).
Key file: src/kernel/graph.cc
Graph::register_task() maps task name strings to registration functions:
"moe_w13_fp8_sm100" → register_moe_fp8_sm100_task() → TASK_MOE_W13_FP8_SM100
Each registration function (in src/kernel/task_register.cc) reads tensor dimensions from the TBGraph, generates a CUDA code string calling the templated kernel with those dimensions, and returns a variant_id via register_task_variant(). Same code string → same variant_id (deduplication).
Phase 3: CUDA Compilation
Key function: get_compile_command() in persistent_kernel.py
Builds the nvcc command with:
- Includes: Python headers, Mirage headers, CUTLASS, JSON library
- Architecture flags:
-gencode=arch=compute_90a,code=sm_90a (Hopper), compute_100a,code=sm_100a (Blackwell)
- Feature defines:
-DMPK_ENABLE_TMA (Hopper/Blackwell), -DMIRAGE_GRACE_HOPPER or -DMIRAGE_GRACE_BLACKWELL
- Runtime defines:
-DMODE_OFFLINE, -DMPK_MAX_NUM_BATCHED_REQUESTS=N, -DMPK_MAX_NUM_BATCHED_TOKENS=N, -DMPK_MAX_NUM_PAGES=N, -DMPK_PAGE_SIZE=N, -DMPK_MAX_SEQ_LENGTH=N
- Scheduler config:
-DMAX_WORKER_PER_SCHEDULER=N (computed from worker/scheduler ratio)
- Output: shared library (
.so) as a Python extension module
For multi-GPU (NVSHMEM): adds -rdc=true, NVSHMEM/MPI includes and libraries.
Phase 4: Runtime Initialization
Key file: include/mirage/persistent_kernel/persistent_kernel.cuh
init_persistent_kernel() sets up the full runtime state:
-
Meta-tensor mapping — stores 10 meta-tensor pointers in global_runtime_config (step, tokens, input_tokens, output_tokens, num_new_tokens, prompt_lengths, qo_indptr, paged_kv_indptr, paged_kv_indices, paged_kv_last_page_len).
-
NVSHMEM init (if multi-GPU) — calls nvshmemx_init_attr(), creates NVSHMEM teams for cross-GPU communication.
-
Call generated _init_persistent_kernel() — this loads the JSON task graph via construct_task_graph(), allocates GPU memory for intermediate tensors, and populates the all_tasks, all_events, first_tasks vectors.
-
Allocate runtime queues on GPU: