为异构推理做好准备:次世代 RTP-LLM 推理引擎设计分享

为异构推理做好准备:次世代 RTP-LLM 推理引擎设计分享
2025年01月10日 15:15 InfoQ

作者 | 杨熙

审校 | 刘侃,Kitty

自从以 ChatGPT 为代表的大语言模型爆发以来,LLM 应用迅速扩展到了阿里 1+6+N 的各个业务线,做为智能引擎中台,就需要一个强大的推理引擎来支撑业务运行。去年,我们团队以 nvidia 开源的 FasterTransformer 为基础,开发出了第一版 rtp-LLM,很好地支持了起步阶段的大模型推理业务。依靠 nvidia 提供的优秀的 kernel 实现,初版的 rtp-LLM 就在 nvidia GPU 上有着不错的性能,功能上对于起步阶段的业务也绰绰有余。

从硬件设备上说,尽管老黄耕耘了十几年的 cuda 是生态和性能上的第一选择,但显然,用户自然希望能用更多种异构硬件来完成推理任务。一方面是摆脱对单一硬件生态的依赖,另一方面在需求不同的场景也可以发挥不同取向的硬件的长处。例如,对于时延要求高的在线业务,GPU 加速卡仍然是必要选择;但是对于大批量没有时延要求的离线推理任务和规模相对较小的模型,线上保有量大的各种 CPU 可以用更低成本完成任务。除了用户之外,许多硬件厂商也都希望参与到这场 AI 狂热中,在这个前景广阔的市场中分一杯羹,毕竟哪个做硬件的能看着老黄的财报不眼红呢?

众所周知,LLM 因为训练、试错成本高,已经很少能进行模型结构上的创新,绝大多数新模型都是在 transformer 的基础上做了小范围的改动。因此,大模型时代与之前的 AI 推理业务相比,有一个重要的趋势是模型结构变得单一并趋于固定。而如果我们细看 GPT 系列的模型结构,会发现它的组成部分实际上也相对简单,其中运算量最大的部分只有矩阵乘和 multi-head attention。模型结构的单一性带来了一个潜在的好处:计算逻辑在不同硬件上的适配和优化变得更容易。

话说回来,由于初版 rtp-LLM 是基于 nvidia 的开源库开发,在设计上和 cuda 硬件强耦合,无法摆脱对 nvidia gpu 的依赖,因而不能支持 cuda 以外的硬件设备,这样的框架显然难以支持未来更加丰富的硬件生态。而在硬件问题之外,随着业务复杂度的增长,越来越多的缺陷也被暴露出来:

  • 框架中的许多计算、调度逻辑由 python 驱动,在诸多业务场景下会成为性能瓶颈。这部分瓶颈由于 GIL 的存在,优化起来十分困难;而 python 层带来的 overhead 在 bert 等小模型场景下,又会显得格外突出。

  • 业务逻辑和计算逻辑混杂在了一起,功能扩展和接口迭代变得十分困难。业务逻辑直接调用了硬件接口,计算逻辑又缺乏抽象,即使一个小的参数改动,也需要牵一发而动全身,从业务逻辑层层传递到计算部分。

  • 计算逻辑的实现本身又十分混乱,每一层算子都维护了自己的硬件状态如 cuda stream、blas handle 等,硬件相关的状态更是被塞得到处都是。这使得现有计算逻辑的维护也变得十分困难。

  • 显存的管理逻辑也是一地狼藉。不同层级的算子申请的显存均为独占显存,显存无法在算子间复用造成了大量浪费;不同 size 的请求会让每个算子的 buffer 反复分配,在 malloc 和 free 上浪费一些时间。

这些点长期来看都会使业务迭代变得十分难受,尤其是没有任何硬件后端上的扩展性。因此,我们本着以硬件接口为第一公民的思想,重构了 rtp-LLM 的模型推理逻辑。本文将从以下几个方面,结合源代码中的模块介绍 rtp-LLM 在计算部分的的设计思想:

  1. 如何设计多层次的算子抽象并最终调用到硬件实现上

  2. 如何设计 KV  Cache 的结构

  3. 如何适配多种量化算法

  4. 如何做到极致的显存管理,让显存里的每个字节都用的明明白白

算子设计和硬件实现

让我们先从算子的设计开始介绍。

考虑一下经典的 GPT 模型结构:头尾的 embedding 和 lm_head 加上 n 个 transformer layer,每个 layer 由 attention layer 和 ffn layer 组成。经典的 ffn layer 比较简单,一般由三个矩阵乘和激活函数组成;attention layer 多了一个略为复杂的 mulit-head attention 结构和以 RoPE 为代表的 positional embedding,除此之外主要仍然由矩阵乘构成。最后,不同的模型在不同的位置还会插入 RmsNorm/LayerNorm 等 normalization 算子和残差连接。

如果从计算的视角开始考虑,组装 GPT 模型的底层算子并不多:Gemm、LayerNorm、addbias,加上 softmax、激活和 position embedding 就足够了。因此,最底层的计算算子比较容易敲定。

但是,如果了解一些硬件对 GPT 模型算子的实现就不难发现,绝大部分硬件厂商都会提供更加高层次的计算库,例如,对经典的 multi-head attention 结构来说,诸如 flash attention 这种 fused 实现在 GPU 上已经非常成熟。而更加激进的开发者,例如 intel 的 xFasterTransformer 库,直接将包含了 qkv 和 output 矩阵乘的完整 attention layer 以及完整的 ffn layer 做成了两个大算子。再比如,大部分模型的 ffn layer 都会执行矩阵乘之后接一个激活函数的操作。从计算逻辑上来说,这是两个独立的操作;但是对硬件实现来说,这两个操作可以 fuse 到一起,对存放在片上 cache 里的矩阵乘的结果直接计算激活结果,可以减少一次显存的读写,提高计算性能。与之类似的不同层次算子 fusion 还有很多,其在不同硬件上的实现方式也各不相同。

在传统的 torch 或 tensorflow 等框架上,要实现不同层级的硬件后端 fusion,往往采用 compile 或 graph 改写的方案完成,这类方案通常需要实现复杂的 compiler 或 graph optimizer。而对于 LLM 推理来说,由于模型结构相对固定,结合模型计算逻辑考虑硬件上的实现方式,我们在 rtp-LLM 中对 gpt 模型做了多层次的抽象,大致的抽象环节如下所示:

在这个层次结构中,除了如 gemm、layernorm 等最基础的算子外,每一抽象层次都有调用下一层算子的默认实现。硬件层面如果实现了完整的基础算子,模型即可通过一层层的抽象调用来完成运算。而如果硬件对于某一层抽象有更高级的实现方式,那么可以覆盖原本的默认实现,使用更高效的硬件 kernel。从上图已经可以看出,gemm+act 就是一个典型的 fuse 算子。实际上这里的 linear 层也是类似的设计,由于 GPT 模型中的每个 hidden state 和 weights 之间的 gemm 都可以插入一层 lora,因此实际的 linear 层都保留了插入 lora 的功能;而 lora 的实现上,又调用了一层 grouped gemm,默认实现是连续调用 n 个 gemm,而如果硬件存在高效实现,则可覆盖原先的实现。

这样的模型层次设计带来一个好处,每一层抽象的默认实现已经都天然完成了算子的 fuse,不需要额外做 graph optimization 或者算子编译,就可以自动在硬件优化存在时调用。当然,一切优点都有其代价,这套模型的描述方式放弃了一定的灵活性,由于没有模型编译或 python 描述的能力,支持新模型就必须用 C++ 描述新的逻辑,工作量相对较大。

除了模型本身的计算逻辑之外,Device 层面还提供了一些对 LLM 框架来说很重要的接口。这些接口不一定直接参与计算逻辑,但是对于实现框架层面复杂的业务逻辑来说十分重要。

  • 最基础的copy接口:通过一个统一接口自动完成任意设备之间的内存拷贝,不但在计算逻辑中常用,框架的基础调度逻辑更离不开。

  • 基于copy接口实现的concatselect等常见内存操作。这部分接口同样支持硬件用高级实现覆盖。

  • copy接口类似的nonBlockingCopy接口:不参与主计算逻辑的拷贝操作,如 KV Cache 写远程、加载 lora weights 到显存等,可以使用这个接口进行。以 cuda 实现为例,这个接口会调用一个新的 stream 进行拷贝,不参与计算的主 stream 流程。

  • sampler接口,用于 LLM 输出结果采样。

  • 多实例通信用的broadcast/allReduce/allGather接口,硬件实现后即可支持多机 TP。硬件层无需关心 weights 切分、多卡调度等逻辑。

  • quantize量化接口:用于量化模型输出的中间状态。

  • 不同硬件上的最佳性能实现会对 weights layout 有不同的要求,因此框架需要提供静态的 weights 改写接口。这部分接口既有 c++ 也有 python,硬件实现可以灵活选择 weights 改写的实现方式。

  • cudaEvent_t类似的DeviceEvent接口,device 可以创建 event 进行同步,这对于拷贝 KV Cache 等操作非常有用。

这部分接口,未来也会随着业务需求继续扩展,在更多功能上起到隔离业务逻辑和硬件实现的作用。通过这一套完善的硬件接口,模型计算和框架层面的任何业务逻辑都不再需要直接和硬件打交道。框架开发人员可以专注于编写业务逻辑,不需要了解任何硬件层面的实现细节;硬件开发人员则无需关心业务逻辑,只需专注于硬件算子的实现和优化。

我们知道,attention 部分的计算逻辑在 LLM 中十分重要,这部分值得展开写一下。一般情况下,LLM 推理分两个步骤:prefill 和 decode。prefill 也叫 context 计算,是首字返回前的预处理逻辑,需要对输入 token 两两之间进行 $O(n^2)$ 的 self-attention 运算,这一阶段的计算结果会填充所有 token 的 kv-cache,所以也叫 prefill,一条请求只需计算一次;decode 则对应首个 token 输出之后增量 token 的输出计算,对从第二个 token 开始的每个 token 都需要计算一次。

对模型的计算来说,prefill 和 decode 请求在 attention 以外的部分并没有这么大的差异。decode 阶段相比起 prefill 来说,单次模型调用中的 self-attention 复杂度从由 $O(n^2)$ 变成了 $O(n)$,计算量是变小的;但是每次增量 token 的 decode 都需要将模型 weights 完整读取一遍,因而带宽要求更高。从硬件视角看,prefill 和 decode 的 attention 计算分别对应了 computation-bound 和 memory-bound 两种性能瓶颈,硬件侧往往会使用不同的实现方式。另外,要追求极致的 LLM 推理性能,发挥不同型号异构硬件的优势,prefill 和 decode 角色分离部署是必不可少的,一个角色实际上只需要承担一种 attention 计算的任务。

综上,为了实现和调用更清晰,我们在 attention 阶段设计了contextAttentiondecoderSelfAttention两种计算接口,分别对应两种 attention 计算。在单机场景下,现代 LLM 推理引擎提供的一个基础功能是将 prefill 和 decode 请求混合在同一个 batch 中进行。框架调度逻辑会将同一个 batch 里的 prefill 和 decode 请求分别排在一起,在 attention layer 的算子中分别 dispatch 到 prefill 和 decode 的算子进行计算。

诸如 tokens、logits、hidden states 和 KV Cache blocks 等算子输入,框架都按照这种方式拼接。需要注意的是,除了 attention 之外的部分并不需要拆分,两种请求的计算逻辑是一致的。

量化功能支持和适配

量化方法是现代 LLM 推理技术中十分重要的一环,推理框架对量化的支持也是绕不开的话题。不同的量化算法不仅在计算逻辑上有所不同,计算需要的输入组成也有所不同。大部分量化方法除了输入值(weights 或 hidden state)本身之外,还需要一份 scale 来标记输入的上下限,并且 scale 的 shape 和输入值的 shape 也不同;有些方法除了 scale 之外还需要一份 zeros 来标记数据的零点值。

在框架上,我们设计了基础的Buffer对象用于算子接口、数据传递和运存分配。Buffer类似于常见的Tensor,但是更轻量:仅提供对象指针和 shape、数据类型等基础 metadata,接口上不提供算子(算子由 device 对象提供)。Buffer对象的语义是一块带了 metadata 的内存,既可以在 cpu 内存上,也可以在显存上;既可以从现有指针创建、不负责生命周期管理,也可以由 device 分配,并负责追踪对象的生命周期,将指向的内存和自己的生命周期绑定。

上文提到,完整表示一块量化后的值可能需要多块内存。如果把量化所需的对象全部写在算子接口里,不但接口会变得臃肿不堪、对象需要层层传递,并且计算逻辑的扩展也将变得复杂。为此,针对量化功能,我们在Buffer的基础上设计了QBuffer,用于存储和传递一块量化之后的数据。QBuffer继承了Buffer对象本身,存储的数据也是量化之后的数值。在 Buffer 的基础上,QBuffer 又包含了可选的 scale 和 zeros 两个子 Buffer,并扩展了 DataType,增加一些具体的量化类型,因此只需一个对象就可以完整表达一块量化之后的数据。

从对象语义的视角看,QBuffer 的存在并没有破坏 Buffer 应有的语义,代表的仍然是一块类似于的 Tensor 的 weight 或者 state 数据。而在算子的接口上,无需任何改动,即可让算子天然具有了支持量化操作的语义。从硬件实现的角度看,具体的某个算子实现只需根据输入的 Buffer 类型来 dispatch 到相应的量化计算 kernel 即可完成功能的支持,不需要关心和改动算子接口以外的部分。

在未来,如果需要继续扩展新的量化类型也很容易,只需要增加新的量化类型和算子实现即可,不需要对框架整体进行改动。同样的思想也可以轻松实现诸如 SparseBuffer 等其他可能需要的复杂对象,带来更强大的扩展性。

KV Cache 结构设计

KV Cache 管理是 LLM 推理框架设计中不可忽视的挑战之一,也是 llm 和一般的深度学习模型之间最大的差异。rtp-LLM 在最初版本就提供了类似于 Paged Attention 的 KV Cache 管理机制,在重构之后基于 paged attention 的思想,提供了更丰富的功能和更灵活的扩展性。未来的诸多工作,例如 pd 分离、KV Cache  offload 等都需要依赖基础的 KV Cache 管理机制进行展开。接下来我们介绍 rtp-LLM 中和 KV Cache 有关的机制与设计。

众所周知,类似于 paged attention 这种的基于 block 的 KV Cache 管理机制已经成了现代 LLM 推理引擎的标配,rtp-LLM 当然也不例外。

在 rtp-LLM 的框架设计中,KV Cache 管理的最小单位为一个 cache block。一个 cache block 中保存了指定 token 数的模型所有 layer 的KV Cache 内容。上文提到,框架启动时会根据 warm up 测算的结果决定KV Cache 可用的显存大小,cache manager 在启动时会将 KV Cache 部分的显存占用掉,并划分出多个物理的 cache block。

从计算 kernel 实现的角度看,即使是同一种硬件(例如 cuda),对 attention 也会有多种不同的实现,当前 rtp-LLM 就支持了多种 fused multi-head attention kernel。而不同的 kernel 实现接受的 KV Cache 输入方式不尽相同:有的 kernel 接受 k、v 两个起始地址加上每个 block 的偏移量;而有的 kernel 接受的直接就是 k 和 v 的两组指针数组。考虑到前一种传输方式可以转换到后一种,而后一种很难转换成前一种,我们将算子层面KV Cache  block 传输接口由最初的指针数组改成了 base 指针 +block id 数组。

框架为每个请求会分配对应的 KV Cache block,请求级别拿到的 KV Cache  block id。而在每次 GptModel 推理开始之前,框架会根据请求凑批的情况,把所有请求对应的 block id 用上文提到的方式拼接在一起,供 attention 算子读写。

全局显存管理策略

我们知道,老黄的显存卖得比金子还贵,显存空间可以称得上是物理意义上的寸土寸金。把有限的显存用到极致,对推理框架来说有着很大的价值,也带来了一定挑战。为了最大化显存的利用,我们在 rtp-LLM 框架中设计出了显存预分配 + 算子 Buffer 二次分配的方案。这一节我们介绍和显存相关的设计与优化。

显存管理的基础对象是 device 持有的BufferManager和它分配出的Buffer。对于 device 上分配出的 Buffer,指针对应的运存的生命周期也和 Buffer 严格绑定,在 Buffer 析构时即释放运存。在算子开发层面,开发者只需要从 device 中申请 Buffer 对象,即可获取一块生命周期随 Buffer 绑定的显存。

在申请 Buffer 时,BufferManager 会调用 device 对应的 Allocator 对象分配内存。通常,此时调用的应该是 device 的 malloc 函数,例如cudaMalloc。但是为了能更高效地管理和利用显存,我们在基础 Allocator 之上设计了TrackerAllocator。在 device 初始化时,框架会尝试一次性尽可能申请硬件的全部显存,并使用申请到的这部分显存创建TrackerAllocator对象,由它来替代默认的 Allocator,进行后续的显存分配。TrackerAllocator 的设计本身也非常简单,显存的分配模式并不像传统内存池一样有多次细小的分配需求,只需要做简单的线性内存分配即可满足需求。

另外值得一提的是,框架除了自身算子的 Buffer 分配外,还劫持了 torch 的 CUDAAllocator,因此 torch tensor 申请的显存同样可以全局管理与追踪。

预分配显存在框架层面带来了很多好处。硬件设备的 malloc 和 free 接口本身也是有一定时间开销的,这部分时间在 bert 这样的小模型下尤其明显,而显存池可以将显存分配、释放带来的 overhead 可以降到最低。更重要的是,用户可以 trace 出任何时刻显存的使用量、显存排布和分配堆栈。在有些场景下,例如算子开发时可能会遇到 buffer 没有及时销毁而导致本可以释放的显存仍被占用,或者在 oom 时用户希望分析当前是谁占用了显存,完整的 trace 可以为显存优化提供指导。

以在 A10 上运行 qwen2-7b 为例,我们来实战分析两种不同场景下显存用量最大时的分配情况。对于比较长的 query,框架打印出分配量最大时的显存排布如下:

--------------------------------------------------------------------------

| ADDR | size ( hex) | AVAIL| TRACE| TAG |

--------------------------------------------------------------------------

| 0x7f9b92000000 | 33554432 ( 2000000) | USED | | |

| 0x7f9b94000000 | 49152 ( c000) | USED | 0 | curandstate |

| 0x7f9b9400c000 | 9238347776 (226a60000) | USED | 1 | |

| 0x7f9dbaa6c000 | 4096 ( 1000) | USED | 3 | eos_id |

| 0x7f9dbaa6d000 | 14752 ( 39a0) | USED | 18 | combo_tokens |

| 0x7f9dbaa709a0 | 16 ( 10) | USED | 19 | |

| 0x7f9dbc3a54f0 | 16 ( 10) | USED | 23 | cu_seqlens |

| 0x7f9dbc3a5500 | 14752 ( 39a0) | USED | 24 | padding_offset |

| 0x7f9dbc3a8ea0 | 26428416 ( 1934400) | USED | 25 | attn_out_buf |

| 0x7f9dbdcdd2a0 | 26428416 ( 1934400) | USED | 35 | ffn_out_buf |

| 0x7f9dbf6116a0 | 26428416 ( 1934400) | USED | 34 | |

| 0x7f9dc0f45aa0 | 139693056 ( 8538c00) | USED | 36 | gemm_output |

| 0x7f9dc947e6a0 | 23271776 ( 1631960) | FREE | | |

--------------------------------------------------------------------------

不难看出,此处正在进行的是 ffn layer 中的矩阵乘。但是如果我们换种情况,运行一个长度较小的 query:

--------------------------------------------------------------------------

| ADDR | size ( hex) | AVAIL| TRACE| TAG |

--------------------------------------------------------------------------

| 0x7ee38e000000 | 33554432 ( 2000000) | USED | | |

| 0x7ee390000000 | 49152 ( c000) | USED | 0 | curandstate |

| 0x7ee39000c000 | 9238347776 (226a60000) | USED | 1 | kv_cache |

| 0x7ee5b6a6c000 | 4096 ( 1000) | USED | 3 | eos_id |

| 0x7ee5b6a6d000 | 150784 ( 24d00) | FREE | | |

| 0x7ee5b6a91d00 | 7168 ( 1c00) | USED | 39 | |

| 0x7ee5b6a93900 | 16 ( 10) | USED | 55 | |

| 0x7ee5b6a93910 | 96 ( 60) | USED | 56 | |

| 0x7ee5b6a93970 | 96 ( 60) | USED | 57 | |

| 0x7ee5b6a939d0 | 16 ( 10) | USED | 58 | |

| 0x7ee5b6a939e0 | 16 ( 10) | USED | 59 | |

| 0x7ee5b6a939f0 | 16 ( 10) | USED | 61 | |

| 0x7ee5b6a93a00 | 16 ( 10) | USED | 62 | |

| 0x7ee5b6a93a10 | 16 ( 10) | USED | 63 | |

| 0x7ee5b6a93a20 | 16 ( 10) | USED | 64 | |

| 0x7ee5b6a93a30 | 143040 ( 22ec0) | FREE | | |

| 0x7ee5b6ab68f0 | 150528 ( 24c00) | USED | 35 | ffn_out_buf |

| 0x7ee5b6adb4f0 | 608256 ( 94800) | USED | 40 | gemm_output |

| 0x7ee5b6b6fcf0 | 608256 ( 94800) | USED | 54 | |

| 0x7ee5b6c044f0 | 608256 ( 94800) | USED | 60 | |

| 0x7ee5b6c98cf0 | 2433280 ( 252100) | USED | 84 | |

| 0x7ee5b6eeadf0 | 264000016 ( fbc5210) | FREE | | |

--------------------------------------------------------------------------

注意这个打印出来的表中有一列TRACE,记录的是一个堆栈 id,这个 id 可以通过另一份打印出的堆栈的 log 查找到具体的调用堆栈。虽然最后一块显存没有打 tag,但是通过查看 84 号堆栈仍然可以找到其分配函数在fastertransformer::CudaDevice::sampleGreedy(fastertransformer::GreedyParams const&)中,由此可知此时显存占用最大的位置是 sampler,为 topk 或 topp 计算中需要的 workspace。

到目前为止,运行时需要的显存已经可以较好的分析,但是细心的读者可能已经发现了,在上面打印出的显存排布中,KV Cache 占了很大一块。在当前的框架设计中,KV Cache 需要提前按照 block 数量和每个 block 的 size 分配好独占显存,并不可在计算中使用,即使当前是空的。下一章我们会详细分析 KV Cache 管理的设计,在这里我们仅讨论如何平衡分配给 KV Cache block 的显存和 runtime 预留的显存。

虽然理论上要计算出模型所需的最大显存并非不可能,但是现实中可能因为诸多实现细节而导致预估不准确。那么最准确的方式莫过于直接跑一次服务启动时设定的最大输入长度,记录下这其中用到的最大显存量,将其他的显存分配给 KV Cache blocks。还是以 A10 上运行 qwen2.5-7b 为例,假如我们设定的最大序列长度为 32768,那么在服务启动后,通过 warm up 测量 runtime 需要的显存量并分配好 KV Cache block 之后,显存的分配情况如图:

实际上得承认,将可用显存不可变地划分给 KV Cache 和 Runtime 两部分并不是好的设计,而是在当前实现不够好的 Cache 管理机制下(必须使用提前划分好的显存,无法动态扩缩)不得不做出的一个妥协。未来结合 pd 分离等高级功能,需要重构 cache 管理机制去除掉 KV Cache Block 独占显存的限制,真正做到所有显存的最大化利用。

结    语

除了 cuda 之外,我们正在进行 AMD ROCm GPU(感谢 AMD 工程团队的大力支持)、倚天 ARM 架构 CPU(感谢平头哥开发团队和 ARM 公司的大力支持)、intel CPU(感谢 intel 团队的支持)的实现支持,并不断优化这些硬件平台上的性能。当前在 AMD GPU 和 ARM CPU 上支持了 qwen 系列等部分模型,代码也开源在 github https://github.com/alibaba/rtp-llm/,我们欢迎大家前来试用、拍砖。

放眼开源业界,到了 2024 年底的今天,LLM 推理框架已然是百花齐放的状态。不同的框架会从不同的角度去看待问题,而 RTP-LLM 放在当今的推理框架市场上,可以找到一些独特的地方:

  • 硬件层面的计算逻辑和 LLM 推理的业务逻辑做到了完全解耦,每种硬件都有一套独立的算子和接口,囊括了所有硬件层面的逻辑,业务逻辑的开发不需要直接调用任何硬件 API,更不需要关心硬件上如何实现。这使得框架新开发的业务逻辑可以无痛扩展到所有硬件上。

  • 从框架调度到模型编排全部使用原生 C++ 编写,因此框架和模型本身带来的 overhead 极低且易于优化。

  • 多层次且扩展性强的计算逻辑抽象使得新功能的适配、针对硬件优化变得简单。

  • 极致的显存管理机制,不但能追踪显存分配,还能清晰打印全局的显存排布,使得显存消耗一目了然,显存空洞的针对性优化十分容易。

有了这样的基础框架,在后续实现诸如 pd 分离、远程 KV Cache、tree 结构生成等框架层面的高级功能时,开发可以做到更加从容、更高效率。相信在未来,随着业务复杂度的进一步提高和硬件生态的丰富,这些特性会变得更有价值。

财经自媒体联盟更多自媒体作者

新浪首页 语音播报 相关新闻 返回顶部