资讯详情

内存即性能:LLVM与KV Cache的短生命周期对象管理之道

📅 2026/10/17 14:18:55 | 华诺云谱 👁 阅读
内存即性能:LLVM与KV Cache的短生命周期对象管理之道
前阵子梳理大模型推理的显存管理脑子里突然冒出一个想法这玩意儿的底层逻辑跟 LLVM 那套基础设施简直是同一个模子刻出来的。LLVM 的 ADT基础数据结构库和内存管理机制解决的是编译器在极短时间内处理海量中间代码的身存亡续问题而大模型推理要面对的同样是在有限的显存里把数百万 token 的生命周期安排得明明白白。两条看似风马牛不相及的赛道在“如何高效地分配、复用、回收短生命周期对象”这件事上撞了个满怀。这篇随笔第一篇我打算把这两条线的底层逻辑摊开揉碎从 LLVM 的侵入式引用计数、BumpPtrAllocator 这类硬核机制讲起再一路剖析到大模型推理的 KV Cache、PagedAttention 与 Continuous Batching看看两边是如何用完全不同的思路解决同一个本质上关于“内存即性能”的命题。适合对编译器后端、LLVM 源码或者大模型推理部署感兴趣的朋友不一定需要你两边的代码都熟但如果你正在做推理优化这篇文章或许能帮你把那些零散的 trick 串成一条线。1. 内容整体设计与思路拆解1.1 为什么把 LLVM 和大模型推理放在一起聊在很多人的认知里LLVM 是编译器世界的元老级基建大模型推理是 AI 落地的新贵两者之间隔着一道不算浅的技术代沟。但说白了它们落在同一个物理硬件上都要面对冯·诺依曼体系结构里最残酷的现实内存墙。CPU 访问主存的速度远比执行一条简单指令要慢GPU 访问显存同样要忍受几百个时钟周期的延迟。谁能把内存访问的次数和总量压下来谁就能获得成倍的性能提升。LLVM 在后端优化的过程中需要不停构建中间表示IR、分析指令依赖、变换基本块结构。每一轮 pass 都会产生大量临时分配的instruction对象、use-def链节点、dominator树节点。这些对象生命周期极短往往只存在于单个pass的执行期间。如果直接用C的标准new/delete或者智能指针去管理malloc和free的系统调用开销会把优化时间拉到不可接受的程度。这里就逼迫 LLVM 设计出一套极其高效的内存管理和对象生命周期方案。大模型推理则是另一番光景。以自回归生成为例每生成一个 token 都要把之前所有 token 的 Key 和 Value 投影结果拿出来做注意力计算。这些历史信息不能丢弃否则上下文就断了。但是显存容量有限光是一个 7B 参数的模型权重就要占去小 14GB 的显存以 FP16 算留给 KV Cache 的空间本就捉襟见肘。于是业界开始精细化地管理这块“缓存城墙”从 kv cache 的动态分配、页式管理到连续批处理带来的缓存复用率提升每一招都是在跟显存容量极限博弈。LLVM 和大模型推理都面临一个核心矛盾边界明确的总容量无穷尽的生命周期请求。一边是有限的内存条一边是有限的显存。看懂一边的解法另一边几乎就是条件反射式的理解。1.2 从源码到部署内存管理机制扮演的角色先给两块场景定个位。LLVM 的内存管理机制服务于编译时的高通量计算。编译器一天到晚干的活就是读入代码、建AST、生成IR、优化、生成汇编整个链条中绝大部分对象生命周期短、创建销毁频繁。对这类场景性能最优的方案不是“精确”管理每个对象而是“批量遗忘”——分配时迅猛释放时一锅端。LLVM 的 BumpPtrAllocator竞技场分配器就是此思路的极致体现。而大模型推理的显存管理服务于运行时的高并发推理请求。多个用户同时请求生成每个人带着一个动态增长的历史上下文。每个请求的 KV Cache 大小随着生成进度不断膨胀你不能在请求开始时拍脑袋定一块固定大小的显存分给它定小了生成到一半就崩定大了其他请求就挤得慌。所以这块的核心不是“批量遗忘”而是“动态共享”——把显存视为一个按页划分的大池子各家请求需要多少页就各取多少页用完就到池子里归还。这就是 PagedAttention 的原始出发点。两种机制看起来南辕北辙但如果你把 LLVM 每次 pass 执行后整块 reset 的行为类比成一个请求完成后的所有 KV 页统一归还会发现它们处于同一条光谱的两端LLVM 是“集中式销毁”PagedAttention 是“分布式归还”。这也说明一个道理好用的机制往往不是凭空发明的而是把已有模式推到极限后的必然产物。2. LLVM ADT 与内存管理机制深度解析2.1 从 C 标准库到 LLVM 自研 ADT到底改了些什么用 C 写过业务代码的朋友对 std::vector、std::list、std::map 肯定不陌生。标准库的容器设计通用面向的是五花八门的场景所以它优先保证正确性和通用性而不是极致性能。LLVM 作为高度专业化的基础设施选择了自己磨一套刀——llvm::SmallVector、llvm::StringRef、llvm::ArrayRef、llvm::SmallPtrSet等等。拿 SmallVector 举例。它的设计目标极其明确在处理大量“小规模但高频”的临时对象时跳过堆分配。SmallVector 内部预留了一块固定大小的栈上缓冲区比如 N 个元素只有当元素数量超过 N 才转到堆上分配。这个“小而快”的特性跟编译器处理基本块中的指令序列简直天作之合——大部分基本块就十几条指令完全不需要堆分配的内存开销。再比如 StringRef它做的事更绝仅仅保存一个指向外部字符数组的指针和长度不拷贝字符串本身。编译器每次从 token 或 AST 节点拼接一段字符串做分析时传一个 StringRef 就能把数据从头传到尾根本不会产生字符串拷贝。这里头蕴含的底层思路是对象之间的数据流动能用引用的就绝不用值传递。这不仅是省一次 memcpy 的事更是改变系统整体内存峰值的关键决策。这里的取舍也很直观。LLVM 放弃 std::vector 的完全通用性换来的是“编译器自身在编译大型源文件时内存不爆炸”的稳定性。如果说标准库是“什么都能装的神奇箱子”SmallVector 就是“专门装基本块指令的贴身口袋”——贴身当然比神装快。2.2 侵入式引用计数与所有权归属设计编译器代码里对象之间互相引用的关系极其复杂一个 user 会引用 instructioninstruction 也会通过 operand 引用 user。用 std::shared_ptr 可以管理这种引用关系但每个 shared_ptr 的引用计数区块在堆上独立分配频繁的增减计数操作会引入原子操作开销和缓存行竞争。LLVM 选择了侵入式引用计数IntrusiveRefCntPtr方案。这套方案的核心在于引用计数直接嵌在对象自身内存里的前几个字节而不是放在外部的控制块中。对象知道自己的引用数外部指针直接用裸指针操作智能指针包装器只负责在赋值、退出作用域时调整对象内部的计数。一个 mallocation 就同时覆盖了对象内存和计数内存没有额外的控制块申请也没有原子操作性能自然上得去。还有一个细节LLVM 里的 Value 基类自带 Use 列表记录了所有引用它的 user。当指令被替换RAUWReplaceAllUsesWith时可以精准地找到所有使用者并批量修正引用关系。这种设计与侵入式计数配合让编译器在进行全局优化时对对象的生命周期完全可控。不会出现既没有使用它、却不敢释放它的“幽灵对象”。这套思路给所有 C 大工程一个很好的范本所有权模型必须根据对象图结构专门设计照搬某种通用指针到头来只会两头堵。2.3 Arena 分配与 BumpPtrAllocator 的工程实践如果说引用计数管的是“对象生命周期”那 BumpPtrAllocator 管的就是“对象内存来源”。lli 或者 opt 在执行一个 pass 时往往会创建成千上万个临时对象。用 malloc 挨个分配不仅慢还容易因为碎片化让内存利用率变低。BumpPtrAllocator 的处理方式是一口气向系统申请一大片连续内存比如 256KB然后在上面按需分配每次分配只需要移动一个指针bumpO(1) 时间完成。这里面有个特别典型的 LLVM 场景pass 执行期间创建一堆临时 Instruction 节点跑完后只要调用一次 alloca.Reset()整块内存被标记为可复用下一轮接着用。这跟手写内存池的套路一致但 LLVM 把它揉进了库的骨髓里任何使用者都能无脑享受它的好处。有一个极其重要的工程细节因为分配器只负责分配不负责回收使用者要自己保证对象之间没有互相持有引用。一旦引用了被 arena 一次性清零的对象就会产生悬垂指针。LLVM 体系下大家约法三章跨 pass 的数据必须通过显式数据流结构传递禁止在 pass 结束后仍持有多余指针。这种做法约束了工程实践中写代码的人也保护了整体架构的安全性。注意BumpPtrAllocator 不是万能解药。如果你的对象生命周期有长有短、交错混合一锅端释放会导致内存利用率下降。它最适合的场景是“阶段性创建、阶段性销毁”这种整齐的节奏。理解这一点比背下接口 API 重要得多。3. 大模型推理的显存与请求调度梳理3.1 自回归生成的内存瓶颈到底卡在哪里大模型推理里最难啃的硬骨头就是自回归中的 KV Cache。以 GPT 类的 transformer decoder 为例生成第 n 个 token 时注意力层需要计算第 n 个 token 的 query 与之前 1 到 n-1 个 token 的 key、value 的匹配度。如果不缓存前序 KV 向量那每生成一个新 token 都得把前面的所有 token 重新过一遍网络计算量呈二次方爆炸——生成一篇文章得把一部电影看上无数遍谁受得了。所以业界普遍的做法是在显存中开辟一块区域存储每个序列历史的 K 和 V 张量这就是 KV Cache。模型大小为 7B、上下文长度为 2048、批次大小为 8 的情况下单个请求的 KV Cache 大约需要2K 和 V × 层数 × 头数 × 头维度 × 序列长度 × 字节数2字节 FP16。粗略一算8 个请求同时跑就可能吃掉 8GB 以上的显存。这还没算输入输出的激活值和临时张量。自回归生成是“生成一个 token扫描一遍历史”KV Cache 就变成访问最频繁的数据。这块的带宽和容量直接决定生成速度。你显存再大如果 KV Cache 的组织方式不够高效每次访问都跨内存页跳来跳去也只能等着被带宽卡脖子。3.2 PagedAttention 的页式思想以及它如何改造显存利用率这里必须请出 vLLM 的 PagedAttention。它的灵感来源极其朴素——操作系统的虚拟内存分页机制把物理内存切成固定大小的块让进程以一个统一的虚拟地址空间访问散布在物理内存各处的页。PagedAttention 干的事就是把 KV Cache 按固定大小的块block划分一个 block 容纳固定数量的 token 的 KV 数据比如 16 个 token 一页。过去比较粗放的做法是给每个请求预先分配一个最大长度的连续显存段。如果请求只生成了 10 个 token但预留了 512 token 的空间块的浪费率就高得吓人。PagedAttention 的页式管理在物理块层面按需分配逻辑上却通过块表block table把这些物理块串起来。请求后到的 token 再动态申请一个新块多一个 token 就多占一块空间分配粒度精确到 16 个 token 的量级。这套机制的另一个大杀器是块级别的显存共享。在做并行采样beam search 或 parallel generation时多个序列可能共享前缀 token。比如两个序列历史的前 1024 个 token 完全相同后面各自扩展到 2048。如果没有显存共享两份重复的 1024 token KV 得存两遍。PagedAttention 的块表支持多个序列的某些逻辑块映射到同一物理块那么公共前缀只用存一份。单这一项优化在某些解码场景下能节省接近 50% 的显存。3.3 Continuous Batching 与请求排队策略的内存效应大家可能听过 continuous batching连续批处理这个词但通常只把它当作提升吞吐的调度策略忽略了它对显存的塑造作用。传统静态 batching 的思路是凑一批 N 个请求一起做完再处理下一批。问题是长尾效应严重一个生成得特别慢的请求会拖住整批请求其他请求的 KV Cache 也一直占着显存不释放。Continuous batching 的做法则激进得多每个 step或几个 step结束后如果某个请求已经完成生成了结束符或达到 max token就立刻把它踢出批次同时从等待队列中拉进一个新的请求填充进这个 tee 空出来的显存位置。请求不再是“签个到就赖着不走”而是每时每刻都在动态进出显存的颗粒度被利用到极致请求的平均排队时间也大幅缩短。从内存机制上看continuous batching 通过动态腾挪让 KV Cache 拥有只在请求存活期间内占用显存的“租期”语义。这种思想的本质与 LLVM pass 结束时整块释放 arena 如出一辙——它们都放弃了对单个对象生命周期的精细追踪改用“阶段”来粗粒度管理从而把调度开销降到最低。4. 两条线的交集内存复用与生命周期设计的通用法则4.1 短生命周期对象的批量释放效率远超细粒度追踪观察 LLVM 的 BumpPtrAllocator 与大模型推理的显存管理有个惊人的共性都是在“批量”层面管理生命周期而不是在“个体”层面。前者在一个 pass 结束之后把整块 arena reset后者在一个请求踢出批次之后把整块逻辑上的 KV Cache 页表标记为可回收。个体层面的析构与释放需要记录每个对象的出生顺序、依赖关系、引用链复杂度和内存碎片率都会飙升。当场景允许“阶段性创建、阶段性销毁”时整块释放的效率是革命性的。这就像搬家时请十几个工人挨箱子拆包再重新打包看起来操作精细其实效率极低倒不如先把所有东西塞进集装箱运到新家再一次性倾倒出来虽然过程粗放但批量吞吐高得吓人。这话听着稀松平常但在系统设计里极其容易被忽略。很多人在写内存池的时候总惦记“给每个对象配一个生命周期钩子”结果体系越写越复杂性能还不如直接调 new/delete。LLVM 和 vLLM 提供的经验是让对象的生命周期尽量对齐到某个“阶段”的边界上阶段切割得越干净内存管理越轻松。4.2 对象布局与访问局部性对性能的决定性影响LLVM 的 SmallVector 把对象紧密排列在连续内存中不仅省了堆分配还能让 CPU 缓存命中率大幅提高。大模型推理的 KV Cache 按块划分、块表索引也是为了让 GPU 的缓存与显存带宽发挥最大威力——K 和 V 按顺序排布计算注意力时执行连续内存访问远比随机跳转来得快。这种对访问局部性的执著往深了说是在跟硬件体系结构的物理规律较劲。CPU 的主要瓶颈是延迟GPU 的主要瓶颈是带宽但它们都非常依赖数据在内存中的排列位置。数据在内存中排列得越紧、越连续缓存效率越高单位时间内能处理的数据量就越大。如果你要优化一个现有的大模型推理服务不一定要立刻上 PagedAttention 那样的重武器先检查 KV Cache 的张量布局是否连续、是否存在多余的内存拷贝或许更能立竿见影。很多时候内存结构比显卡型号更能决定推理吞吐。4.3 显式所有权模型如何让系统边界更可靠LLVM 里所有对象的所有权关系是显式的谁创建、谁释放、谁引用在类型层面就能看出来。比如 Value 的 Use 列表允许你追踪所有使用者Option 指针的语义让编译器能静态检查是否在 pass 结束后还有逃逸指针。这种显式约定降低了并发和异步场景下的内存安全问题发生概率。大模型推理框架也在做类似的事。PagedAttention 中的 block table 把物理块索引显式映射给逻辑序列谁能访问哪块显存、何时归还框架层一清二楚。GPU 不像 CPU 有 MMU 做越界保护显存越界往往不会立刻暴露而是产生 silent corruption这是最可怕的线上事故。有了显式的所有权映射越界问题在逻辑层面就能被拦截。套用一句做系统工程的老话能通过类型系统或结构设计避免的错误就不要指望运行时来兜底。把所有权模型建模对了系统的整体风险就会大幅下降——这在编译器和推理引擎里都是一样的道理。5. 实操过程与核心环节实现一轮从编译到推理的体验5.1 用 LLVM 源码快速验证 BumpPtrAllocator 的魅力理论说破嘴不如跑一把代码来得直观。这里我建议你直接在本地拉一个 LLVM 项目用主干分支即可然后写一个极简的可执行文件看看 BumpPtrAllocator 到底快在哪儿。#include llvm/Support/Allocator.h #include chrono #include cstdio #include vector struct Node { int value; Node* next; }; int main() { constexpr int kCount 10000000; // 标准 new/delete 方案 auto t0 std::chrono::high_resolution_clock::now(); std::vectorNode* nodes; nodes.reserve(kCount); for (int i 0; i kCount; i) { Node* n new Node{i, nullptr}; nodes.push_back(n); } for (Node* n : nodes) delete n; auto t1 std::chrono::high_resolution_clock::now(); double msNew std::chrono::durationdouble, std::milli(t1 - t0).count(); // BumpPtrAllocator 方案 llvm::BumpPtrAllocator alloc; std::vectorNode* arenaNodes; arenaNodes.reserve(kCount); auto t2 std::chrono::high_resolution_clock::now(); for (int i 0; i kCount; i) { Node* n alloc.AllocateNode(); n-value i; n-next nullptr; arenaNodes.push_back(n); } alloc.Reset(); auto t3 std::chrono::high_resolution_clock::now(); double msArena std::chrono::durationdouble, std::milli(t3 - t2).count(); std::printf(new/delete: %.2f ms\n, msNew); std::printf(arena: %.2f ms\n, msArena); return 0; }这段代码的意思很直白同是 1000 万个 Node 对象的创建和销毁new/delete 走的是一趟系统调用一个对象的流程而 arena 只是把指针在预先申请的内存块上“蹭蹭”地往前挪。实测下来BumpPtrAllocator 往往能比 new/delete 快出一个数量级。这里额外的两个观察点第一BumpPtrAllocator 无需在分配时检查是否已有合适的空闲块天然零碎片第二Release 或 Reset 时无需遍历对象直接扔掉整块。注意这只是一个微观基准不代表 BumpPtrAllocator 在任何场景都比 new/delete 好。它赢在短生命周期对象集中释放的节奏上若你的对象需要独立释放、交错创建那还是老实回到 malloc 大家族。5.2 手写一个简化的 PagedAttention 逻辑理解块表管理想在本地验证 PagedAttention 的思想不需要真的搭一个 GPU 推理框架用一个 CPU 上的 Python 模拟足矣。核心在于块表的设计逻辑块号到物理块号的映射。下面的代码把 KV Cache 模拟成一个物理块数组每个物理块可以容纳 block_size 个 token 的 K、V 向量。逻辑序列则持有自己的块表访问第 i 个 token 时先计算它落在哪个逻辑块再通过块表找到物理块。import numpy as np class PhysicalBlock: def __init__(self, block_size, hidden_dim): self.keys np.zeros((block_size, hidden_dim), dtypenp.float32) self.values np.zeros((block_size, hidden_dim), dtypenp.float32) self.used 0 class KVBlockManager: def __init__(self, num_blocks, block_size, hidden_dim): self.pool [PhysicalBlock(block_size, hidden_dim) for _ in range(num_blocks)] self.free_blocks list(range(num_blocks)) self.refcount [0] * num_blocks def alloc_block(self): if not self.free_blocks: raise RuntimeError(out of memory) idx self.free_blocks.pop() self.refcount[idx] 1 return idx def free_block(self, idx): self.free_blocks.append(idx) self.refcount[idx] 0 class Sequence: def __init__(self, block_manager, block_size): self.block_manager block_manager self.block_size block_size self.block_table [] self.length 0 def append_kv(self, key, value): if self.length % self.block_size 0: phy self.block_manager.alloc_block() self.block_table.append(phy) block_index self.length // self.block_size slot_index self.length % self.block_size phy self.block_table[block_index] self.block_manager.pool[phy].keys[slot_index] key self.block_manager.pool[phy].values[slot_index] value self.block_manager.pool[phy].used 1 self.length 1 def kv_at(self, token_pos): block_index token_pos // self.block_size slot_index token_pos % self.block_size phy self.block_table[block_index] return (self.block_manager.pool[phy].keys[slot_index], self.block_manager.pool[phy].values[slot_index])这个简化模型展示的东西是把显存当作可以动态分割和分配的共享资源池。每来一个新 token 就 append 一次按需挂上新的物理块查询第 i 个 token 的 KV 也是一次数组索引就完成。页式管理的精髓——空间分配与逻辑访问解耦——在这个小实现里体现得清清楚楚。实际工业界的实现比我这个模拟复杂得多要处理 GPU 核函数、块分配器并发安全、beam search 场景下的块共享等但核心思想和这里的代码完全一致。5.3 从编译优化到推理部署一次“内存即性能”的完整旅程要想把两套机制串成一个闭环最好的方式是把一个模型用自己的工具链走一遍。我最近干的一件事是用本地编译好的 LLVM 工具链opt llc clang把一个最小的 transformer 推理程序编译成可执行文件然后把它放在基于 PagedAttention 的推理框架里跑起来。编译端看到的是 LLVM 的一次又一次内存 arena 分配与释放每轮 IR 优化 pass 之间的临时对象全部在 arena 上齐生齐灭最终生成紧凑的机器码。推理端看到的是 KV Cache 的物理块动态分配与复用不同请求在不同轮次获得和归还物理页面显存使用量一路被压着走。两层完全不同的抽象在“尽力减少内存分配开销”这个目标上达成了默契。这个旅程虽然有点折腾但对于理解整个系统的协作关系非常有帮助。运行时一个配置文件里的 block_size 设为多少、显存池上限是多少本质上和你编译期选用的 arena 块大小是同类决策——太小会频繁调用底层分配器太大则浪费空间理想值需要通过基准测试去卡。6. 常见问题与排查技巧实录6.1 使用 BumpPtrAllocator 后出现悬垂指针怎么定位这是 LLVM 开发新人最容易踩的坑。BumpPtrAllocator 释法粗犷Reset 之后所有指向该 arena 内存的指针集体失效而这类指针往往还藏在某个容器或 pass 的成员变量里。排查的方法没什么捷径先给全工程加上 ASanAddress Sanitizer再用一小段用例反复触发ASan 会精准报告“use-after-free 发生在哪个对象、哪次释放之后”。ASan 报错之后把报错栈和 Reset 调用的栈对比着看基本十有八九能立刻找到是哪个 pass 的哪个小字段遗忘了生命周期约定。如果还不行另一个办法是在 Reset 之后给整个 arena 填充一个魔法数字比如 0xCD这样悬垂指针读出来的内容就变成 0xCDCDCDCD肉眼也能认出来。注意在 LLVM 新版本里BumpPtrAllocator 可能提供 debug 模式的毒化处理建议开发期一直开着。这个坑的主要成因往往不是复杂逻辑而是顺手把一个对象指针塞到了全局状态里违反了“阶段内使用”的基本纪律。6.2 KV Cache 显存占用暴涨先查哪些参数大模型推理服务跑着跑着显存报警这是线上最常收到的问题之一。很多人第一时间去调模型参数其实绝大多数情况下问题出在 KV Cache 相关的三个点第一max_num_batched_tokens 和 max_batch_size 是否设置过大。连续批处理框架会尽可能往批次里塞请求但每个请求的 KV Cache 占用随上下文长度增长批次一旦超过显存池的承受能力显存就会瞬间打满。第二block_size 太大或太小。block_size 越大块数越少每个块的分配管理开销越省但按块分配的浪费越多block_size 如果设成 1浪费倒没了块表膨胀和分配器压力又会飙升。第三是否忽略了 beam search 场景下的 KV 共享。如果你的在线服务用了 beam search 或 speculative decoding 相关的并行策略而没有启用共享块表优化公共前缀会导致成倍的显存浪费。排查时先用nvidia-smi看总体显存再通过框架自带的 metrics 接口查看显存池使用率和空闲块数量往往一眼就能看出是池子太小还是碎片浪费太严重。6.3 性能不升反降可能是生命周期模型与场景不匹配有一种情况很典型本地微基准测试下BumpPtrAllocator 的表现吊打 new/delete可一旦放进真实的编译流程整体性能却没什么改善有时甚至会变慢。原因在于现代 glibc 的 malloc 对重复创建和释放同样大小对象的场景早已做了优化比如 tcache 和 arena 复用小对象的分配速度不见得慢多少。BumpPtrAllocator 的优势只在“一次性释放巨量对象”的场景下才会放大如果你的对象本来就不用集体释放arena 反而会占用大块连续内存让其他模块的内存申请在物理层面碎片化。大模型推理也同理。如果在线请求的并发度很低、每个请求的上下文又很短PagedAttention 的块表管理开销甚至可能让你觉得不划算。但换个角度这正说明内存机制的选择必须贴合生产环境的画像——没有最佳方案只有最匹配的方案。我在实际使用中发现一个很有意思的共性无论是调整 LLVM 里某个 pass 的分配策略还是调整推理框架里 KV Cache 的块参数建模和调参的工具都极其相似。先用计数器统计对象总数与生命周期分布再决定采用哪种内存策略比一上来就翻源码改配置要可靠得多。内存管理的优化本质上是一种“量而后谋”的工作。最后再分享一个小技巧——在做这些内存相关实验的时候永远要把“内存分配次数”和“内存峰值”两个指标一起记录下来。LLVM 的优化和推理框架的调度都容易做到前者漂亮、后者难看如果你只盯着一个指标调优大概率会在下一个负载下翻车。把两者同时装在心里许多看起来玄乎的显存或内存问题往往迎刃而解。这篇随笔先写到这后续有空我打算把 Arena 分配器在大模型前向计算中的直接应用比如激活值显存管理再单开一篇展开聊聊。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑