注:本文已于2025.12.31 发表于知乎和公众号 1. 背景
如果要向一位完全不了解大模型推理技术的开发者介绍这个领域,我应该从哪里讲起?大模型推理的最简流程可以概括为:输入一串文本 → 文本通过词典映射表转换成一串数字序号 → 序号再经过 embedding 层的计算,变成一组能代表语义的浮点数向量 → 这组向量送入推理系统,经过层层的矩阵乘法、加法和各类专用函数的运算,得到新的输出向量 → 对输出向量做概率筛选,选出概率最高的那个数值对应的序号 → 最后再通过词典映射表 “翻译” 回文字,得到最终输出的一个词。图 1 这是对大模型推理最朴素的理解,上述流程看似简单,但背后的推理计算环节对普通开发者而言仍是一个 “黑盒”。如果想更进一步拆解推理引擎的底层加速原理,nano-vllm 会是一个极佳的入门切入点。2. 简介
nano-vLLM 代码量仅约 1200 行,却实现了生产级推理框架的核心技术原型,具体包括:
- 连续批处理(Continuous Batching)
- KV 缓存(Prefix KV Cache / Paged KV Cache)
- 高性能编译与执行优化(Torch Compilation、Triton、CUDA Graph)
- 张量并行(Tensor Parallelism)
该框架极具入门学习价值,本文将先介绍 nano-vLLM 的基本组成架构,再对部分核心技术要点展开深入解析。3. 系统架构
nano-vLLM 的架构非常有层次感。3.1. 整体架构概览
图 2, 来自:https://deepwiki.com/GeeeekExplorer/nano-vllm三层结构
- 接口层:User Interface Layer
- 推理引擎中控层:Inference Engine Layer
- 显存管理和模型执行层:Memory Management & Model Execution Layer
3.2. 类层面架构
从类设计层面观察 nano-vLLM 的架构。 图 3上图中四种颜色代表系统的四个组成部分
- 浅蓝色,入口和推理引擎中控层
- 浅绿色,模型推理
- 浅红色,KV Cache 管理
- 浅紫色,权重加载和矩阵计算的封装
3.3. 码层面划分
源码规划上也较为简洁。目录结构如下:- nanovllm/
- ├── engine
- ├── layers
- ├── models
- └── utils
复制代码
- engine,引擎的入口、中控,同时 KV Cache 比较简单,代码也放在这个目录下。
- layers,模型推理的通用组件,内部包括:linear、layernorm、rotary_embedding、attention、activation 等基础功能的封装,可以被不同模型使用。
- models,模型的实现,依赖 layers 的组件实现不同模型的推理。
- utils,不同层都可能会用到的工具函数。
4. 连续批处理
4.1. 概念理解
(1)定义连续批处理 (Continuous Batching):是一种迭代级(Iteration-level)的调度策略。它以“Token 生成步骤”为调度粒度。通过动态地在每一轮迭代中替换已完成的任务,消除了由于生成长度不一导致的 GPU 计算气泡,极大地提升了系统的吞吐量。(2)朴素理解一个请求需要执行多轮,不同请求需要执行的轮数不同,系统一轮最多只能同时执行一批 N 个请求,当一个批次里的请求参差不齐的完成时,每完成一个请求就将其用新请求替代掉。对比传统批处理和连续批处理:
- 传统批处理 (Static Batching):必须等待 Batch 中生成序列最长的那个请求完成,整个 Batch 才会释放。在此期间,生成序列短请求完成后槽位会空转。
- 连续批处理 (Continuous Batching):请求完成即退出,新请求立即补位,槽位始终满载。
4.2. 最基础的连续批处理
最简单的连续批处理,不考虑 prefill 和 decode 的差异,示例代码:[code]import timeimport threadingimport queueimport random# 1. 初始化线程安全的等待队列waiting_queue = queue.Queue()MAX_BATCH_SIZE = 3# --- 模拟用户请求线程 (生产者) ---def user_request_producer(): request_id = 1 while True: # 模拟用户随机到达:每 1~2 秒来一个新请求 time.sleep(random.uniform(1, 2)) # 每个请求需要的 Token 长度随机(3到8之间) req = {"id": f"REQ-{request_id}", "remain": random.randint(3, 8)} waiting_queue.put(req) print(f"\n[用户端] 送入新请求: {req['id']} (预计长度: {req['remain']})") request_id += 1 if request_id > 5: break# --- 核心推理循环 (消费者/执行器) ---def inference_loop(): running_batch = [] print("--- 推理引擎已启动 ---") iteration = 0 while True: # A. 补位逻辑:只要 Batch 没满且队列里有货,就拉进来 while len(running_batch) < MAX_BATCH_SIZE: try: # 使用 block=False,如果队列空了直接报错进 except,不阻塞推理逻辑 new_req = waiting_queue.get(block=False) running_batch.append(new_req) print(f" >>> [调度] {new_req['id']} 进入 Batch") except queue.Empty: break # B. 推理逻辑:如果当前 Batch 有任务,就执行一次 Step if running_batch: iteration += 1 print("="*20 + f"{iteration=}" + "="*20) # 模拟 GPU 推理耗时 (Step 耗时) time.sleep(1.2) # 当前 Batch 状态展示 active_ids = [f"{r['id']}(剩{r['remain']-1})" for r in running_batch] print(f"[GPU推理] 处理中: {active_ids}") # 每一个请求的剩余长度减 1 finished_this_step = [] for req in running_batch: req["remain"] -= 1 if req["remain"] |