news 2026/7/22 14:29:17

DataWhale组队学习笔记--llm-algo-leetcode(五)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DataWhale组队学习笔记--llm-algo-leetcode(五)

文章目录

  • vLLM PagedAttention(vLLM 分页注意力)
    • 1. 痛点:传统 KV Cache 的内存刺客
    • 2. 破局思路:引入操作系统的虚拟内存
    • 3. PagedAttention 的工作机制
    • 4. PagedAttention 带来的核心红利
  • SGLang RadixAttention(SGLang基数注意力)
    • 1. 核心痛点:重复 Prefill 与跨请求浪费
    • 2. 核心原理:用基数树(Radix Tree)自动管理 KV Cache
    • 3. 典型应用场景
    • 4. 对比:PagedAttention vs. RadixAttention

vLLM PagedAttention(vLLM 分页注意力)

在大语言模型(LLM)的部署和推理优化中,vLLM 提出的PagedAttention(分页注意力机制)是一个里程碑式的突破。它直接解决了大模型在生成长文本时,显存(GPU Memory)被严重浪费的核心痛点。

以下是 PagedAttention 的核心原理及其实战机制的拆解:

1. 痛点:传统 KV Cache 的内存刺客

在自回归(Autoregressive)生成过程中,大模型需要保存之前生成的每个 Token 的 Key 和 Value 张量,以避免每次生成新词时重复计算。这些被缓存的数据就叫KV Cache

传统推理框架在管理 KV Cache 时面临严重的内存浪费问题:

  • 预分配导致内部碎片:系统通常会为了以防万一,预先为每个请求分配一段连续的、达到模型最大允许长度的显存空间(例如 2048 或 4096 tokens)。但很多请求实际生成的长度远达不到最大值,多出来的显存就被白白占用了。
  • 无法高效共享:在并行采样(如 Beam Search)中,同一个 Prompt 会产生多个不同的输出分支。传统的连续内存分配无法让这些分支共享 Prompt 阶段的 KV Cache,导致一份前缀被复制了多份。

据统计,传统的连续内存管理机制会导致高达60% - 80%的 KV Cache 显存被浪费。

2. 破局思路:引入操作系统的虚拟内存

vLLM 团队从操作系统的虚拟内存分页机制(Virtual Memory Paging)中汲取了灵感。

在操作系统中,程序认为自己拥有一块连续的“逻辑内存”,但实际上这些内存在物理上是被切分成一个个固定大小的“页(Pages)”,并可以分散存储在物理内存的各个角落。系统通过“页表(Page Table)”来记录逻辑地址到物理地址的映射关系。

3. PagedAttention 的工作机制

PagedAttention 将这种分页思想直接移植到了 LLM 的注意力机制计算中:

  1. KV Blocks(KV 块):系统不再为每个请求分配一整段连续的 KV 显存,而是将其划分为一个个固定大小的块(Block)。每个 Block 包含固定数量的 Token 的 KV 向量(例如,每个 Block 只装 16 个 Token)。
  2. 非连续物理存储:这些 Block 在 GPU 的显存中不需要连续存放,哪里有空闲空间就分配在哪里。
  3. Block Table(块表):vLLM 维护着一个中心化的块表,负责将每个请求连续的“逻辑 KV 块”映射到显存中散落的“物理 KV 块”。

当模型进行 Attention 计算时,PagedAttention 算法会在底层自动查找块表,跨越这些非连续的物理块,准确地提取出需要的 KV 向量完成矩阵乘法。这个过程对上层模型是完全透明的。

4. PagedAttention 带来的核心红利

  • 几乎消除显存浪费:因为是按需动态分配(用完一个 Block 再分配下一个),显存的内部浪费被严格控制在最后一个没有装满的 Block 内(通常浪费率低于 4%)。
  • 吞吐量(Throughput)翻倍:节省下来的海量显存,可以用来同时容纳更多并发请求的 KV Cache,从而显著增大 Batch Size。在相同的硬件下,vLLM 的吞吐量通常能达到传统框架的2 - 4 倍
  • 写时复制(Copy-on-Write)实现内存共享:对于多个请求共享同一个 Prompt,或者执行 Beam Search 这种有共同前缀的场景,不同请求在 Block Table 中只需指向相同的物理块。只有当它们各自生成不同的后续 Token 时,系统才会分配新的物理块(写时复制)。这使得复杂推理场景下的显存占用呈指数级下降。

代码实现:

importtorchfromtypingimportList
classRequest:def__init__(self,request_id:int,prompt_len:int):self.request_id=request_id self.seq_len=prompt_len self.block_table:List[int]=[]classKVCacheManager:def__init__(self,num_blocks:int,block_size:int,head_dim:int):self.num_blocks=num_blocks self.block_size=block_size self.head_dim=head_dim# TODO 1: 模拟预分配一块大显存池self.physical_kv_cache=torch.zeros(num_blocks,block_size,head_dim)# 跟踪哪些物理块被占用了self.free_blocks:List[int]=list(range(num_blocks))defallocate_for_prefill(self,req:Request):""" 请求刚进来时 (Prefill阶段),为它的 Prompt 长度分配所需的全部 Block """# TODO 2: 计算需要的 block 数量(向上取整)needed_blocks=(req.seq_len+self.block_size-1)//self.block_size# TODO 3: 从 free_blocks 中弹出对应数量的 block 索引iflen(self.free_blocks)<needed_blocks:raiseRuntimeError("OOM")for_inrange(needed_blocks):block_id=self.free_blocks.pop(0)req.block_table.append(block_id)defallocate_for_decode(self,req:Request):""" 自回归生成时 (Decode阶段),检查序列长度。 如果当前最后一个 Block 满了,则按需分配 1 个新 Block。 """req.seq_len+=1# TODO 4: 判断是否需要新的 Blockis_new_block_needed=(req.seq_len%self.block_size)==1ifis_new_block_needed:ifnotself.free_blocks:raiseRuntimeError("OOM")block_id=self.free_blocks.pop(0)req.block_table.append(block_id)defget_physical_cache(self,req:Request)->torch.Tensor:""" 根据块表,把不连续的物理块"拼凑"成逻辑上连续的 KV Cache """# TODO 5: 根据 req.block_table 的索引,从物理池中提取对应的块blocks=[self.physical_kv_cache[block_id]forblock_idinreq.block_table]cat_blocks=torch.cat(blocks,dim=0)# 只截取真实 seq_len 长度返回returncat_blocks[:req.seq_len]
# 运行此单元格以测试你的实现deftest_paged_attention_manager():try:# Case 1: 典型 Prefill + Decode + Cache 拼装manager=KVCacheManager(num_blocks=10,block_size=4,head_dim=64)print("初始化内存池...")req1=Request(request_id=1,prompt_len=6)manager.allocate_for_prefill(req1)assertlen(req1.block_table)==2,"长度 6 的请求应分配 2 个 Block!"assertlen(manager.free_blocks)==8,"池中应该剩下 8 个空闲块!"print(f"✅ Prefill 测试通过!Req1 分配的块表:{req1.block_table}")manager.allocate_for_decode(req1)assertlen(req1.block_table)==2,"生成第 7 个 token 时不应该分配新块!"manager.allocate_for_decode(req1)manager.allocate_for_decode(req1)assertlen(req1.block_table)==3,"生成第 9 个 token 时应当分配了第 3 个新块!"assertlen(manager.free_blocks)==7,"池中应该剩下 7 个空闲块!"print(f"✅ Decode 动态分配测试通过!Req1 最新块表:{req1.block_table}")forblock_id,valueinzip(req1.block_table,[1.0,2.0,3.0]):manager.physical_kv_cache[block_id].fill_(value)cache=manager.get_physical_cache(req1)assertcache.shape==(9,64),f"拼装出来的连续 Cache 形状不对,应为 (9, 64),实为{cache.shape}"asserttorch.all(cache[:4]==1.0),"第 1 个 Block 未正确拼装!"asserttorch.all(cache[4:8]==2.0),"第 2 个 Block 未正确拼装!"asserttorch.all(cache[8:]==3.0),"第 3 个 Block 的截断拼装不正确!"print("✅ Cache 拼装测试通过!多块物理缓存被正确恢复为逻辑连续序列。")# Case 2: 恰好跨越 block 边界时,Decode 应该分配新块,并正确截断最后一块manager2=KVCacheManager(num_blocks=4,block_size=4,head_dim=8)req2=Request(request_id=2,prompt_len=4)manager2.allocate_for_prefill(req2)assertlen(req2.block_table)==1,"长度 4 的请求应只分配 1 个 Block!"manager2.allocate_for_decode(req2)assertlen(req2.block_table)==2,"长度 5 的请求应分配第 2 个 Block!"manager2.physical_kv_cache[req2.block_table[0]].fill_(7.0)manager2.physical_kv_cache[req2.block_table[1]].fill_(8.0)cache2=manager2.get_physical_cache(req2)assertcache2.shape==(5,8),f"拼装出来的连续 Cache 形状不对,应为 (5, 8),实为{cache2.shape}"asserttorch.all(cache2[:4]==7.0),"边界块的前 4 个 token 不正确!"asserttorch.all(cache2[4:]==8.0),"边界块的最后 1 个 token 不正确!"print("✅ 边界分配与截断测试通过!")# Case 3: OOM 分支必须抛出 RuntimeErroroom_manager=KVCacheManager(num_blocks=1,block_size=4,head_dim=8)oom_req=Request(request_id=3,prompt_len=5)try:oom_manager.allocate_for_prefill(oom_req)exceptRuntimeErrorase:assert"OOM"instr(e),"OOM 异常信息不正确!"print("✅ OOM 测试通过!")else:raiseAssertionError('显存池不足时应该抛出 RuntimeError("OOM")!')print("\n✅ All Tests Passed! PagedAttention 内存管理逻辑验证通过。")exceptNotImplementedError:print("请先完成 TODO 部分的代码!")raiseexcept(AttributeError,NameError,TypeError,ValueError,AssertionError,RuntimeError)ase:ifisinstance(e,AttributeError):print("代码未完成,无法找到必要的属性")elifisinstance(e,NameError):print("代码可能未完成,导致变量为 NoneType。")elifisinstance(e,TypeError):print("代码可能未完成,导致变量为 NoneType。")elifisinstance(e,ValueError):print("代码可能未完成,导致了张量维度错误")elifisinstance(e,AssertionError):print("代码可能未完成,导致了断言失败")elifisinstance(e,RuntimeError):print("代码可能未完成,导致了运行时错误")else:print("代码可能未完成,导致了断言失败")raiseNotImplementedError("请先完成 TODO 部分的代码!")fromeexceptExceptionase:print(f"❌ 测试失败:{e}")raisetest_paged_attention_manager()

结果:

SGLang RadixAttention(SGLang基数注意力)

如果说 vLLM 的PagedAttention解决了“单请求内部(Intra-request)显存碎片化”的问题,那么 SGLang 提出的RadixAttention(基数注意力机制)则更进一步,解决了“跨请求/多轮交互间(Inter-request)KV Cache 的自动复用与生命周期管理”的难题。


1. 核心痛点:重复 Prefill 与跨请求浪费

在大模型实际应用场景中,大量请求都包含重叠的前缀(Overlapping Prefixes)

  • 多轮对话(Multi-turn Chat):第 2 轮对话的输入包含第 1 轮的 Prompt 和 Answer。
  • Agent / 复杂工作流:在 Tree-of-Thought(思维树)或 Monte Carlo 搜索中,多个分支共享相同的推导历史。
  • 固定 System Prompt / Few-shot 示例:万级请求共享同一段很长的规则说明或示例。

在传统推理引擎中,哪怕请求之间有 90% 的 Token 完全相同,新请求到来时系统依然要对其前缀重新做一次Prefill(预填充计算),不仅浪费算力,还会导致首包延迟(TTFT, Time-To-First-Token)居高不下。


2. 核心原理:用基数树(Radix Tree)自动管理 KV Cache

SGLang 没有采用复杂的全局 Hash 表,而是引入了计算机科学中经典的Radix Tree(基数树/压缩前缀树)来作为 KV Cache 的索引结构。

在 RadixTree 中:

  1. 边(Edges)与节点(Nodes):保存连续的 Token 序列(如"You are a helpful assistant...")。
  2. 指针与物理映射:节点直接关联底层物理显存中的 KV Cache 块(通常结合了类似 PagedAttention 的 Block 机制)。
  3. 动态生命周期
  • 自动匹配(Prefix Matching):新 Prompt 进来时,在树中从根节点向下做最长前缀匹配。匹配到的部分直接复用 KV Cache,彻底跳过这部分的 Prefill 计算,模型只需要对“新后缀”进行计算。
  • 动态分裂(Split & Insert):当新请求在某个节点中途出现分叉时,原有节点会自动拆分为一个公共父节点和两个子节点。
  • LRU 淘汰(LRU Eviction):当 GPU 显存满载时,系统会根据 LRU(最近最少使用)策略,优先删除树的叶子节点(Leaf Nodes)对应的 KV Cache,并回收显存,直到空间足够。

3. 典型应用场景

RadixAttention 的最大优势在于“零配置全自动”—— 开发者不需要手动维护复杂的 Cache 清单,系统会在后台自动识别模式并完成 KV 共享。

在以下四种常见模式中,RadixAttention 能带来数倍的吞吐与延迟优化:

  1. Few-shot Learning(少样本提示):多个请求共享相同的示例 Prompt,前缀 Cache 命中率接近 100%。
  2. Multi-turn Chat(多轮交互):随对话轮数增加,历史上下文全部在树上,后续轮次只需 Prefill 用户刚发送的一句话。
  3. Self-consistency(采样一致性):单 Prompt 产生多个采样分支,前缀只计算一次。
  4. Tree-of-Thought(思维树搜索):多路探索任务中,所有子分支自动共享根节点与父节点的搜索历史。

4. 对比:PagedAttention vs. RadixAttention

维度PagedAttention (vLLM)RadixAttention (SGLang)
解决的核心问题解决单请求内物理显存离散化与碎片问题解决跨请求间自动前缀复用与调度问题
索引数据结构扁平的物理块映射表(Block Table)动态层级基数树(Radix Tree)
前缀匹配机制主要是单请求/显式配置的前缀缓存全自动最长前缀匹配(Automatic Prefix Caching)
缓存回收策略请求结束即立即释放(或简单保留)基于树结构的LRU 延迟释放(作为全局 Cache 池)
性能优势点极大提高 Batch Size 与 GPU 利用率极大地降低多轮/Agent 场景的TTFT (首字延迟)

一句话总结:PagedAttention 提供了高效的“物理内存物理块”管理,而 RadixAttention 在其之上盖了一层“逻辑前缀索引树”,两者结合成为了现代大模型推理引擎(如 SGLang、vLLM v1/v2 架构)的标准配置。

代码实现:

importtorch
classTreeNode:def__init__(self,key_tokens):self.key_tokens=key_tokens#这条边上的Token序列(如[101,532,789])self.children=[]#子节点列表self.kv_cache_ptr=None#模拟指向物理KV Cache的指针classSimpleRadixCache:def__init__(self):#根节点是空的self.root=TreeNode([])definsert(self,tokens):node=TreeNode(tokens)self.root.children.append(node)def_lcp_len(self,cached_tokens,prompt_tokens):match_len=0#TODO1:逐个token计算最长公共前缀长度,遇到不相等时立刻停止match_len=0whilematch_len<len(cached_tokens)andmatch_len<len(prompt_tokens):ifcached_tokens[match_len]==prompt_tokens[match_len]:match_len+=1else:breakreturnmatch_lendefmatch_prefix(self,prompt_tokens):best_match_len=0#TODO2:遍历self.root.children,更新最长匹配前缀长度forchildinself.root.children:match_len=self._lcp_len(child.key_tokens,prompt_tokens)ifmatch_len>best_match_len:best_match_len=match_lenreturnbest_match_lendefsplit_prompt(self,prompt_tokens):#TODO3:先找命中长度,再拆出前缀和后缀hit_len=self.match_prefix(prompt_tokens)hit_prefix=prompt_tokens[:hit_len]miss_suffix=prompt_tokens[hit_len:]returnhit_prefix,miss_suffix,hit_len
# 测试你的实现deftest_radix_attention():try:cache=SimpleRadixCache()cache.insert([0,1,2,3])cache.insert([0,1,2,3,4])cache.insert([9,9,9])# 1. 基础 LCP 检查assertcache._lcp_len([1,2,3],[1,2,4])==2,"LCP 计算失败!"assertcache._lcp_len([7,8],[7,8,9,10])==2,"完整前缀匹配失败!"print("✅ 最长公共前缀计算正确!")# 2. 多候选路径下,应该选择最长命中前缀match_len=cache.match_prefix([0,1,2,3,4,5])assertmatch_len==5,"匹配失败!应该命中最长的 5 个 token 前缀。"assertcache.match_prefix([7,6,5])==0,"错误匹配!不该匹配到任何东西。"print("✅ 多路径前缀命中选择正确!")# 3. 前缀拆分验证hit_prefix,miss_suffix,hit_len=cache.split_prompt([0,1,2,3,4,5])asserthit_len==5,"Hit Length 计算错误!"asserthit_prefix==[0,1,2,3,4],"可复用前缀拆分错误!"assertmiss_suffix==[5],"待重算后缀拆分错误!"hit_prefix2,miss_suffix2,hit_len2=cache.split_prompt([7,6,5])asserthit_len2==0,"无命中时 Hit Length 应为 0!"asserthit_prefix2==[],"无命中时前缀应为空!"assertmiss_suffix2==[7,6,5],"无命中时后缀应保持原样!"print("✅ 前缀拆分与回退逻辑正确!")print("\n 所有测试通过!这正是 SGLang 让大模型推理首字响应飞升 10 倍的底层秘密!")exceptNotImplementedError:print("请先完成 TODO 部分的代码!")raiseexcept(AttributeError,NameError,TypeError,ValueError,AssertionError,RuntimeError)ase:ifisinstance(e,AttributeError):print("代码未完成,无法找到必要的属性")elifisinstance(e,NameError):print("代码可能未完成,导致了变量未定义")elifisinstance(e,TypeError):print("代码可能未完成,导致了操作错误")elifisinstance(e,ValueError):print("代码可能未完成,导致了张量维度错误")elifisinstance(e,AssertionError):print("代码可能未完成,导致了断言失败")elifisinstance(e,RuntimeError):print("代码可能未完成,导致了运行时错误")else:print("代码可能未完成,导致了断言失败")raiseNotImplementedError("请先完成 TODO 部分的代码!")fromeexceptExceptionase:print(f"❌ 发生未知异常:{e}")raisetest_radix_attention()

结果:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 14:29:12

小程序商城平台哪家强,商家更该关注匹配度

小程序商城平台哪家强&#xff0c;商家更该关注匹配度所谓小程序公司排行&#xff0c;本质上不是把所有品牌放在一个榜单里硬排&#xff0c;而是按交付类型做选择。SaaS 搭建平台适合快速开店和轻运营&#xff0c;源码开发服务商适合长期系统建设。不同预算、不同团队能力、不同…

作者头像 李华
网站建设 2026/7/22 14:28:17

FreeCAD参数化建模核心思维:从操作到设计的跨越

如果你正在学习 FreeCAD&#xff0c;可能会遇到这样的困境&#xff1a;跟着教程一步步操作时看似顺利&#xff0c;但一旦需要独立设计一个稍微复杂的模型&#xff0c;就不知道从何下手。参数化建模的概念听起来很强大&#xff0c;但实际操作中却常常因为某个步骤的顺序错误&…

作者头像 李华
网站建设 2026/7/22 14:26:20

Transformer模型可视化解析与实践指南

1. Transformer模型可视化入门指南 当第一次接触Transformer架构时&#xff0c;大多数开发者都会被其复杂的数学公式和抽象概念所困扰。作为一名经历过同样困惑的工程师&#xff0c;我深刻理解可视化工具对于理解这类模型的重要性。本文将带你从零开始&#xff0c;通过可视化手…

作者头像 李华
网站建设 2026/7/22 14:24:07

越擎科技专访 | iRobotCAM机器人离线编程软件如何破解大尺寸机器人3D打印落地难题,让复杂制造走向智能化

从机器人“会运动”到“会制造”&#xff0c;工业软件成为机器人3D打印产业化关键近年来&#xff0c;3D打印技术正在从消费级产品制造&#xff0c;快速向工业制造领域纵深发展。如果说桌面级3D打印解决的是“小尺寸、快速成型”的需求&#xff0c;那么机器人3D打印正在探索的是…

作者头像 李华
网站建设 2026/7/22 14:21:10

CMS 分品类需求规格详解:基于 GB 15084-2022 的 I/II/IV/V/VI 类要求

1. 概述与适用范围本文依据 GB 15084-2022《机动车辆 间接视野装置 性能和安装要求》第 6 章视野、第 7 章 CMS 安装及附录 C-F 整理&#xff0c;针对 I&#xff08;内后视&#xff09;、II&#xff08;主外后&#xff09;、IV&#xff08;广角外&#xff09;、V&#xff08;补…

作者头像 李华