Component overview
Request lifecycle
1
add_prompt
The caller passes a prompt string. The sequence enters the
LLMEngine.add_prompt tokenizes it and wraps the token IDs in a Sequence object, then hands it to the scheduler.waiting deque with status WAITING.2
schedule
Scheduler.schedule() is called at the start of each step. It returns a list of sequences to run and a boolean indicating whether this is a prefill step.Prefill is always tried first. If any waiting sequence can fit within the token budget and KV cache budget, it is moved to running and included in the batch. Only if no prefill is possible does the scheduler schedule decode steps for already-running sequences.3
model_runner.run
ModelRunner.run prepares the input tensors and executes the model:4
postprocess
Scheduler.postprocess appends the new token to each sequence and checks stopping conditions.The generate() method
generate is the top-level entry point for batch inference.
step() until scheduler.is_finished() returns True (both waiting and running queues are empty). Results are collected as sequences finish and returned in the original prompt order.
The step() method
model_runner.call("run", ...) dispatches to the correct method on both rank 0 and worker ranks via shared memory (see Multi-GPU Inference for details).
ModelRunner responsibilities
prepare_prefill
Builds a flat 1-Dinput_ids tensor by concatenating all token IDs from all sequences (excluding already-cached prefix tokens). Also computes:
cu_seqlens_q— cumulative query sequence lengths, e.g.[0, 5, 8, 12]slot_mapping— physical cache slot for each new token to write intoblock_tables— mapping from logical block index to physical block ID (for cross-sequence prefix reuse)
pin_memory=True and cuda(non_blocking=True) are used together so the CPU→GPU transfer happens asynchronously via DMA, overlapping with other CPU work.prepare_decode
For decode, each sequence contributes exactly one token (its most recently generated token). The method produces:input_idsof shape(batch_size,)context_lens— total tokens processed so far per sequenceslot_mapping— the single new cache slot for each sequenceblock_tables— full block table for reading KV history
run_model
CUDA graph optimization
CUDA graphs eliminate per-step kernel launch overhead by recording the entire sequence of CUDA operations once and replaying the recording on subsequent steps. Why only for decode? Prefill has variable input lengths — every batch is a different shape, so the graph would need to be re-captured for each request. Decode always processes exactly one token per sequence, giving a fixed input shape for each batch size. Capture strategy. Graphs are captured for batch sizes[1, 2, 4, 8, 16, 32, ...]. Capture happens in descending order so the memory pool created for the largest graph is reused by smaller ones.
run_model finds the smallest captured graph that is at least as large as the current batch, copies inputs into the pre-allocated buffers, and calls graph.replay().
allocate_kv_cache
Before capturing graphs, the model runner allocates the entire KV cache as one large tensor and distributes slices to eachAttention module.
world_size > 1, each rank computes its own num_blocks from local free memory. A dist.all_reduce(MIN) ensures all ranks agree on the most conservative limit, preventing OOM on the most memory-constrained GPU.
Initialization order
ModelRunner.__init__ because allocate_kv_cache writes the final max_cached_blocks value into the config dict that the scheduler reads.