0x00 前言
注意是“部署”调优,不是“性能”调优!因此本文与底层Kernel如果优化等无关,主要关注应用层面。本文记录一些使用TensorRT-LLM过程中,对性能有影响的参数的理解以及一些工具的用法。如果理解有误,欢迎指正。本文内容包括:
- 0x01 入门学习路线推荐(进行中)
- 0x02 Batch size相关的设置
- 0x03 影响首Token时延的配置
- 0x04 是否使用custom_all_reduce
- 0x05 影响Decode时延的配置
- 0x06 fp8/int8 KV Cache相关的设置
- 0x07 In-Flight Batching相关的设置
- 0x08 bls模式相关的设置
- 0x09 如何开启debug模式
- 0x0a tensorrtllm_backend使用问题
- 0x0b tensorrt_llm离线推理
- 0x0c 源码编译及benchmark工具使用
- 0x0d FP8/SQ/AWQ量化校准使用
- 0x0e 自定义FP8量化校准数据(进行中)
- 0x0f triton server镜像编译和使用
- 0x10 总结
好记性,不如烂笔头,本文长期更新,内容随缘(就看最近踩到了什么坑~)。内容比较流水账,建议直接跳到想看的章节。
0x01 入门学习路线推荐
- 离线推理推荐先看llama示例,目前是最全面:https://https://github.com/NV...
- 服务化也推荐看llama示例:https://github.com/triton-inf...
- 推荐看完本文,估计能少踩不少坑~
0x02 Batch size相关的设置
- max_batch_size
指允许进入engine的并行跑的最大请求数。对于显存足够的情况下,比如72B模型部署在总显存640G的机器,如果设置地太小,比如8,则会影响服务的吞吐。因为,最多只允许8个请求在engine中并行处理,其他的请求都在排队。另外,启动triton服务时,也需要指定max_batch_size,这个指的是config.pbtxt中的配置;triton server中的max_batch_size和我们在编译engine的时候指定的max_batch_size含义其实是不一样的。
Triton server配置中的max_batch_size: 这个是Triton server本身的dynamic_batching的遗留产物,比如我们在做CV模型的部署时,通常就需要结合triton中的max_batch_size这个参数和dynamic_batching来使用,从而实现动态组batch的功能。这里指的是,triton server的dynamic_batching功能,会把服务请求按照max_batch_size为最大颗粒度组成一个batch,然后再发给TensorRT-LLM处理。也就是triton server的max_batch_size,强调的组batch行为是triton server这个框架自带的特性,和TensorRT-LLM无关。
name: "tensorrt_llm"backend: "${triton_backend}"max_batch_size: ${triton_max_batch_size}
trtllm-build中的max_batch_size: 这个是指trtllm在编译engine的时候,engine支持的最大batch_size。使用过TensorRT的同学们应该对这个参数非常熟悉了。如果太大,可能会导致在编译engine阶段就OOM。
trtllm-build --checkpoint_dir ./tmp --output_dir ./engine --max_batch_size 8 ...
Anyway,为了避免出现各种奇怪的问题,tensorrt_llm/config.pbtxt、tensorrt_llm_bls/config.pbtxt以及trtllm-build中使用的max_batch_size最好保持一致。补充一下,由于tensorrtllm_backend中,还有ensemble(https://github.com/triton-inf...)、preprocessing和postprocessing,因此需要把里边config.pbtxt的max_batch_size都配置成和tensorrt_llm/config.pbtxt中max_batch_size相同的值,否则无法启动服务(太多配置要改了...)
- max_num_tokens
根据官方文档:Best Practices for Tuning the Performance of TensorRT-LLM(https://nvidia.github.io/Tens...) 中的介绍,max_num_tokens表示engine支持并行处理的最大tokens数,TensorRT-LLM需要为此预留部分的显存,此参数与max_batch_size存在相互制约的关系。由于TensorRT-LLM需要根据max_num_tokens预留显存,因此该值越大,留给KV Cache用的显存就越少。(这个理解需要验证~实际上是我自己没有很理解官方文档中的这个解释,但是又不敢承认...)
- 如何合理地设置max_batch_size和max_num_tokens?
max_num_tokens可以按照以下这个公式进行估计。其中alpha是0.0和1.0之间的浮点值,它代表在推理过程中每次调用前向函数时对context阶段请求数量的粗略估计。建议使用0.05到0.20之间(5%-20%之间)的值,但可能取决于实际场景。
max_batch_size * max_input_len * alpha + max_batch_size * max_beam_width * (1 - alpha)
当max_beam_width=1时,我们知道max_batch_size一般不会很大,比如128,而(1-alpha)小于1,因此,项max_batch_size max_beam_width (1 - alpha)可以忽略不计,max_num_tokens的计算公式简化为:
max_batch_size * max_input_len * alpha # alpha建议使用0.05到0.20之间
举个列子,当max_batch_size=64, max_input_len=1024, alpha为0.2,此时max_num_tokens大约为13107。对于没有指定remove_input_padding
的情况,max_num_tokens不生效。
Anyway,就个人实践而言,不同的max_batch_size和max_num_tokens搭配时,确实对TTFT和TPOT有影响,但是怎么才能设置成最优值,对我来说依然是个巨大的“迷”,纯纯手工autotune。所以个人的建议就是,如果你不是为了压榨极致的性能,就使用TensorRT-LLM的默认值吧。
- 为什么build engine正常到启动服务却报OOM?
实践过程有遇到过一些max_batch_size不合理导致服务崩溃的情况。比如max_batch_size设置太大,在build engine阶段不报错,但是在启动triton server阶段报OOM(由于启动服务时发现,系统剩余显存,已经不足够你存放至少一条请求的KV Cache)。这是为啥呢?build engine不报错,反而跑服务崩了。因为max_batch_size是给TensorRT编译engine用的,编译engine的阶段,无法知道你后续启动服务时需要使用多少KV Cache(其实是可以算的吧,不然启动服务的时候咱会抛出这个信息,感觉是TensorRT-LLM没有在build engine阶段做这个功能),因此就有可能导致,对于某个max_batch_size,虽然编译engine没问题,但是启动服务的时候会报OOM。这个体验不是很好,会导致用户可能需要重复build engine,直到能试探到那个可正常用的max_batch_size(_在max_batch_size的边缘疯狂试探......)
0x03 影响首Token时延的配置
- max_queue_delay_microseconds
单位是微秒,1000微秒为1毫秒。表示请求在triton服务队列里为了dynamic_batching最大的等待时间。等到超过这个时间后,才会将请求发给TensorRT-LLM In-Flight Batching的等待队列,接着是IFB进行调度管理,将请求调度给引擎进行推理。如果max_queue_delay_microseconds设置太大,比如100000,则会导致请求被强制等待100ms,表现在LLM时延指标上,大概率就是TTFT慢了100ms。因此,这个值建议根据实际情况设置一个合理的值,避免首Token时延增加。比如:
dynamic_batching { preferred_batch_size: [ 24 ] max_queue_delay_microseconds: 100}
并且,从调度的角度来说,dynamic_batching组完batch之后,扔给IFB,实际上只是从一个等待队列扔给IFB的另一个等待队列,并不是直接给推理引擎直接推理了。因此,在开了IFB的情况下,这个max_queue_delay_microseconds的设置,可以设置成0即可,将对请求调度的管理完全交给TensorRT-LLM的IFB即可。不太理解为啥tensorrtllm_backend中的参考config.pbtxt还要保留这个配置。这个配置在使用IFB的情况下对用户具有比较大的迷惑性。首Token时延相关的设置还包括是否使用custom_all_reduce,具体看下一小节。其他的,想到再补充。
- enable_kv_cache_reuse
TensorRT-LLM里边的KV Cache Reuse功能,指的就是Automatic Prefix Caching功能,具体的实现方式未知,因为这部分代码闭源。enable_kv_cache_reuse开启后,主要是影响首Token时延,即TTFT。对于具有较长system prompt或者多轮对话等场景,可以使用所有请求复用system prompt中的KV Cache,而不需要重新计算,从而降低TTFT的耗时。vLLM中有相同的功能,具体的实现原理可以阅读我写的另一篇文章:
DefTruth:Prefill优化 原理&图解vLLM Automatic Prefix Cache(RadixAttention): 首Token时延优化https://zhuanlan.zhihu.com/p/...
在TensorRT-LLM中使用Automatic Prefix Caching功能需要打开enable_kv_cache_reuse开关,比如:
gptManagerBenchmark --enable_kv_cache_reuse enable
并且在build engine阶段,需要开启use_paged_context_fmha,即在context阶段使用fused multihead attention kernel。具体示例如下:
trtllm-build --use_paged_context_fmha enable
对应到triton server,则需要修改all_models/inflight_batcher_llm/tensorrt_llm/config.pbtxt中的配置:
parameters: { key: "enable_kv_cache_reuse" value: { string_value: "true" }}
另外,特别需要注意的是,在使用enable_kv_cache_reuse功能时,有两个比较重要的参数是可以调优的,分别是tokens_per_block以及kv_host_cache_bytes。由于KV Cache是按照block的颗粒度进行reuse的,因此,tokens_per_block的值将决定了KV Cache reuse的边界情况是如果表现的。比如,tokens_per_block=1024,这将意味着,任何<1024 tokens的prompt以及任一prompt的最后不足1024 tokens的KV Cache Block都无法被不同的request复用。也就是说,tokens_per_block越小,能够被prefix caches命中的tokens可能就越多(这和prefix caching的特性有关,推荐阅读:DefTruth:Prefill优化 原理&图解vLLM Automatic Prefix Cache(RadixAttention): 首Token时延优化)(https://zhuanlan.zhihu.com/p/...)。默认情况下这个值是128(最新的已经修改成64了),但是在使用了prefix caching功能情况下,可以考虑调整成更小的值,比如32/16等。
trtllm-build --tokens_per_block 32 ...
kv_host_cache_bytes则是指定使用多少bytes的CPU主机内存,来保存swap出来的KV Cache。当开启enable_kv_cache_reuse时,如果服务系统达到可支持的QPS上限,则可能会导致大量的KV Cache被逐出,如果此时设置了kv_host_cache_bytes,比如45G,则逐出的KV Cache会被暂存在CPU内存中,以备后续被换入,从而增加KV Cache Reuse的可能性。但是,recompute和swap到底哪个更高效,也是不好说的,得根据实际的机器进行调试。
parameters: { key: "kv_cache_host_memory_bytes" value: { string_value: "45000000000" }}
更多关于enable_kv_cache_reuse的使用,可以参考TensorRT-LLM的文档:kv_cache_reuse.md(https://github.com/NVIDIA/Ten...)
- use_fp8_context_fmha
对于Hopper架构,并且使用FP8量化的模型,可以考虑开启use_fp8_context_fmha,以使用FP8 Context FMHA Kernel,对Prefill阶段进行加速。这个功能目前只支持Hopper架构,期待TensorRT-LLM能够在更多有FP8的架构上支持FP8 Context FMHA。该功能主要影响首Token时延。
0x04 是否使用custom_all_reduce
- use_custom_all_reduce
custom_all_reduce目前是自动的开启的(不确定之后会不会改成默认不开启)。custom_all_reduce会受到NVLink、P2P通信、是否跨NUMA通信等情况的影响。以下,是关于NVLink、P2P、NUMA的简单介绍:
[1] NVLink是英伟达(NVIDIA)开发并推出的一种总线及其通信协议。NVLink采用点对点结构、串列传输,用于中央处理器(CPU)与图形处理器(GPU)之间的连接,也可用于多个图形处理器之间的相互连接。与PCI Express不同,一个设备可以包含多个NVLink,并且设备之间采用网格网络而非中心集线器方式进行通信。该协议于2014年3月首次发布,采用专有的高速信号互连技术(NVHS)。该技术支持同一节点上GPU之间的全互联,并经过多代演进,提高了高性能计算应用中的双向带宽性能。参考:知北游:GPU通信技术
[2] P2P是指NVIDIA GPUDirect P2P通信,NVIDIA GPUDirect是Magnum IO的一部分,作为一种技术,可以增强GPU中心的数据移动与访问。通过GPUDirect技术,网络适配器和存储驱动可以直接读取GPU内存,避免不必要的内存复制。其优点是:降低CPU负载,减小延迟,带来显著的性能提升。支持功能包括GPUDirect Peer to Peer (P2P)、GPUDirect Remote Direct Memory Access (RDMA)等;参考:一棵红杉树:一文读懂GPU通信互联技术(https://zhuanlan.zhihu.com/p/...)
[3] NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU( 如 4 个 ) 组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个人都有 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) 。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。参考:Linux编程用C:一文掌握CPU的SMP与NUMA架构!(https://zhuanlan.zhihu.com/p/...)
[4] NIC是指NIC网卡,一般安装在计算机或服务器上,通过网络与另一台计算机、服务器或其他网络设备进行通信。如今市场上网卡类型众多,但主要以有线网卡和无线网卡为主,其中无线网卡利用无线技术访问网络,而有线网卡需要使用DAC或AOC或光模块和光纤跳线进行连接。目前局域网基本上都是采用的以太网技术,根据网卡应用领域的不同可分为计算机网卡和服务器网卡。对于客户端计算机而言,一般情况下使用一个网卡即可,但对于服务器而言,则需要使用多个网卡来满足处理更多网络流量的需求。通常,计算机网卡都只有一个网络接口,而服务器网卡拥有多个网络接口,如双端口、四端口。参考:飞速FS:100G网卡NIC详细介绍及其发展趋势分析(https://zhuanlan.zhihu.com/p/...)
就个人的实践经验而言,在支持NVLink的机器上,开启custom_all_reduce对TTFT和TPOT均有性能收益。在仅仅支持P2P通信,但是不跨NUMA通信的情况下,也推荐开启custom_all_reduce,或许会有性能收益;在仅支持P2P通信,但是需要跨NUMA通信(比如8卡机器,0-3卡属于一个NUMA,4-7卡属于另一个NUMA),也不推荐使用custom_all_reduce;当连P2P都不支持时,就只能关掉custom_all_reduce了。具体到TTFT和TPOT,TTFT的通信量更大,TPOT的通信量较小,因此是否使用custom_all_reduce对TTFT耗时的影响更为明显。最后,推荐始终使用最新的驱动。
通过nvidia-smi topo --matrix可以查看当前的机器的GPU-CPU通信拓扑。以下是一个从vllm issue (https://github.com/vllm-proje...)里边捞的日志:
nvidia-smi topo --matrix
GPU Topology:
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 NIC0 CPU Affinity NUMA Affinity GPU NUMA ID
GPU0 X PIX PHB PHB SYS SYS SYS SYS PHB 0-13,28-41 0 N/A
GPU1 PIX X PHB PHB SYS SYS SYS SYS PHB 0-13,28-41 0 N/A
GPU2 PHB PHB X PIX SYS SYS SYS SYS PHB 0-13,28-41 0 N/A
GPU3 PHB PHB PIX X SYS SYS SYS SYS PHB 0-13,28-41 0 N/A
GPU4 SYS SYS SYS SYS X PIX PHB PHB SYS 14-27,42-55 1 N/A
GPU5 SYS SYS SYS SYS PIX X PHB PHB SYS 14-27,42-55 1 N/A
GPU6 SYS SYS SYS SYS PHB PHB X PIX SYS 14-27,42-55 1 N/A
GPU7 SYS SYS SYS SYS PHB PHB PIX X SYS 14-27,42-55 1 N/A
NIC0 PHB PHB PHB PHB SYS SYS SYS SYS X
Legend: X = Self SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI)
NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU)
PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge)
PIX = Connection traversing at most a single PCIe bridge
NV# = Connection traversing a bonded set of # NVLinksNIC Legend:
NIC0: rocep1s0
X: 表示当前卡自己和自己的连接;SYS: 表示通过PCIe进行跨NUMA的通信,比如这个示例中的GPU4和GPU0的topo,被标记为SYS,说明这两个卡的通信是需要跨NUMA的;PHB、PXB和PIX都是表示通过PCIe进行连接,但连接方式不同,被标记为这些值的GPU之间一般是不跨NUMA的;NIC0表示这个机器具有一个NIC网卡,可以和所有的卡进行连接;NV#表示卡间是通过NVLink进行连接,这个示例没有NVLink;再捞一个带NVLink的示例(H20):
GPU Topology:
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 NIC0 NIC1 NIC2 NIC3 NIC4 CPU Affinity NUMA Affinity GPU NUMA ID
GPU0 X NV18 NV18 NV18 NV18 NV18 NV18 NV18 NODE NODE NODE SYS SYS 0-47,96-143 0 N/A
GPU1 NV18 X NV18 NV18 NV18 NV18 NV18 NV18 NODE PIX NODE SYS SYS 0-47,96-143 0 N/A
GPU2 NV18 NV18 X NV18 NV18 NV18 NV18 NV18 NODE NODE NODE SYS SYS 0-47,96-143 0 N/A
GPU3 NV18 NV18 NV18 X NV18 NV18 NV18 NV18 NODE NODE PIX SYS SYS 0-47,96-143 0 N/A
GPU4 NV18 NV18 NV18 NV18 X NV18 NV18 NV18 SYS SYS SYS PIX NODE 48-95,144-191 1 N/A
GPU5 NV18 NV18 NV18 NV18 NV18 X NV18 NV18 SYS SYS SYS NODE NODE 48-95,144-191 1 N/A
GPU6 NV18 NV18 NV18 NV18 NV18 NV18 X NV18 SYS SYS SYS NODE PIX 48-95,144-191 1 N/A
GPU7 NV18 NV18 NV18 NV18 NV18 NV18 NV18 X SYS SYS SYS NODE NODE 48-95,144-191 1 N/A
NIC0 NODE NODE NODE NODE SYS SYS SYS SYS X NODE NODE SYS SYS
NIC1 NODE PIX NODE NODE SYS SYS SYS SYS NODE X NODE SYS SYS
NIC2 NODE NODE NODE PIX SYS SYS SYS SYS NODE NODE X SYS SYS
NIC3 SYS SYS SYS SYS PIX NODE NODE NODE SYS SYS SYS X NODE
NIC4 SYS SYS SYS SYS NODE NODE PIX NODE SYS SYS SYS NODE X
更多GPU/CPU通信相关,目前有很多文章介绍,推荐阅读以下文章:
- [1] 知北游:GPU通信技术(介绍了常见的“数据通信路径”,NVLink/P2P/GDR/GDS等)(https://zhuanlan.zhihu.com/p/...)
- [2] 一棵红杉树:一文读懂GPU通信互联技术 (https://zhuanlan.zhihu.com/p/...)
- [3] Linux编程用C:一文掌握CPU的SMP与NUMA架构!(https://zhuanlan.zhihu.com/p/...)
- [4] 飞速FS:100G网卡NIC详细介绍及其发展趋势分析(https://zhuanlan.zhihu.com/p/...)
- [5] 函谷叨客:【研究综述】浅谈GPU通信和PCIe P2P DMA(https://zhuanlan.zhihu.com/p/...)
- [6] 神经蛙没头脑:高性能GPU服务器AI网络架构(上篇)(https://zhuanlan.zhihu.com/p/...)
- [7] 神经蛙没头脑:高性能GPU服务器AI网络架构(下篇)(https://zhuanlan.zhihu.com/p/...)
- [8] 极致Linux内核:什么是NIC(网络接口卡)?(https://zhuanlan.zhihu.com/p/...)
0x05 影响Decode时延的配置
- disable_xqa
XQA
TensorRT-LLM针对Decoding阶段以及MQA/GQA,提出了XQA优化技术。按照官方文档的说明,目前依然是一个实验性的feature。XQA主要影响Decode阶段的时延。XQA目前支持以下优化:
- FP16 / BF16 compute data type.
- FP16 / BF16 / FP8 / INT8 KV cache data type.
- Paged KV cache (64 / 128 tokens per block).
XQA是默认开启的,但是可以通过在build engine阶段指定--disable_xqa来关闭。另外,需要注意的是,虽然XQA是默认开启的,但是不代表最后一定会使用XQA,因为TensorRT-LLM内部会通过启发式算法来决定实际使用XQA还是使用Masked MHA Kernel。不过,我们可以通过指定环境变量来强制使用XQA:
export TRTLLM_FORCE_XQA=1 # 在trtllm-build之前设置
- enable_chunked_context
chunk context功能和vLLM的chunk-prefills功能类似,在prompt比较长的情况下可以考虑开启,比如>=2048。chunk context主要是对Decode时延进行优化;将prompt进行chunk,并和decode阶段的request组batch进行推理。Chunk Prefills相关论文为《SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills》;但是在TensorRT-LLM的IFB模式下,已经是每个request单独使用一个decode stream进行推理,不同的request是交替运行的,IFB实际上Decode优先的调度策略;而vLLM中的continuos batching是首Token优先的调度策略;因此在IFB模式下,开启enable_chunked_context,应该不会有特别明显的性能提升(TODO: 后续有更详细的实验对比,再更新一下)。
Chunk Prefills
Chunk Prefills也比较有意思,论文看了几遍,之后抽空来补一篇解说。
- use_fused_mlp
该选项会把MLP部分融合成一个kernel,开启后对decode阶段的TPOT的性能会有一定提升,个人经验大概是1%~2.5%左右。如果发现对模型效果没有影响,建议开启。
trtllm-build --use_fused_mlp
- multi_block_mode
当需要应用的场景是小batch场景时(比如注重时延的Chat场景,服务的吞吐不会很高),并且input_seq_len大于1024时,可以考虑开启multi_block_mode。但是multi_block_mode这个flag只是一个runtime运行时的建议,就算指定了,如果TRT-LLM发现运行时没有性能收益,则不会使用multi_block_mode。不太确定这个multi_block_mode的原理是否和FlashDecoding相似。参考:https://https://nvidia.github...
multi_block_mode
0x06 fp8/int8 KV Cache相关的设置
- kv_cache_scaling_factor
一般情况下,模型的以INT8 或 FP8 运行时,GPT Attention也可以使用不同的数值精度,比如 FP32、FP16 和 BFloat16 。不过,TensorRT-LLM 支持 INT8 和 FP8 的 KV Cache,即QuantMode.INT8_KV_CACHE 和 QuantMode.FP8_KV_CACHE。GPT Attention运算过程中会保存 KV Cache。当启用 INT8 或 FP8 KV 缓存时,必须使用缩放因子将输入值量化为 8 位。对于量化,缩放因子存储在 kv_cache_scaling_factor 张量中。它的形状是[1],当前版本仅支持每张量量化。量化使用倒数比例,因为它在plugin中乘以 fp_value (1.0 / kv_cache_scaling_factor) 。在Decode/Generate期间,从KV Cache中读取的值在 MHA/MQA Kernel中在线反量化,反量化可以描述为 quantized_value kv_cache_scaling_factor。在covert_checkpoint阶段我们可以指定--int8_kv_cache来使用Int8 KV Cache缓存。
int8_kv_cache
目前,TensorRT-LLM中LLaMA的示例是最为完整的,建议入门先看LLaMA。这个贴一下LLaMA KV Cache INT8 + Weight Only Int8混合使用的例子。原理不展开讲了,不是本文的重点。
# Build model with both INT8 weight-only and INT8 KV cache enabledpython convert_checkpoint.py --model_dir ./llama-models/llama-7b-hf \ --output_dir ./tllm_checkpoint_1gpu_int8_kv_wq \ --dtype float16 \ --int8_kv_cache \ --use_weight_only \ --weight_only_precision int8trtllm-build --checkpoint_dir ./tllm_checkpoint_1gpu_int8_kv_wq \ --output_dir ./tmp/llama/7B/trt_engines/int8_kv_cache_weight_only/1-gpu \ --gemm_plugin auto \ --multi_block_mode enable \
0x07 In-Flight Batching相关的设置
不同的调度策略,会影响对请求的处理方式。一般来说,IFB里边直接使用默认的调度策略即可,但是多了解一些总没坏处。有些情况下,我们可以考虑根据实际的业务场景来选择合适的调度策略。比如,我们经常用吞吐量来衡量服务的好坏,比如 tokens/s,或者 reqs/s;但是站在用户的角度来说,并不是 tokens/s 或 reqs/s越大,用户需要等待的时延就越小。相反,如果你想尽可能提高tokens/s 或 reqs/s,那必然意味着每个请求本身完成的时延会变大。比如,你服务的吞吐很大,1w个请求,在第100秒的那一瞬间一起完成并返回了。这时,reqs/s = 100,意味着平均每秒能处理100个请求。指标上看,服务性能不错。但是呢,这种情况,对于用户来说,其实很糟糕,因为每个用户,实际都等待了100s。那么,这种时候,默认的调度策略不一定是最好的。
- batch_scheduler_policy
batch_scheduler_policy指的是IFB的调度策略。目前,包括两种调度策略,即MAX_UTILIZATION
和GUARANTEED_NO_EVICT
;当启用IFB时,MAX_UTILIZATION 在表示在每次forward时打包尽可能多的请求。它通过尽可能多地调度服务请求来最大化 GPU 的利用率,如果达到 KV 缓存大小限制时,正在迭代中的一些请求则会被Evict(逐出)。而相对的,GUARANTEED_NO_EVICT策略,则会确保请求不会被逐出。MAX_UTILIZATION 适用在追求高吞吐的场景,GUARANTEED_NO_EVICT则更适合在关注用户体验的场景,因为它会确保请求不会被强制逐出,从而导致额外的重计算耗时。
TensorRT-LLM IFB
这里补充一下自己对TensorRT-LLM In-Flight Batching的理解:
0x08 bls模式相关的设置
tensorrtllm_backend支持一种访问服务的模式,即BLS模式(Business Logic Scripting( https://github.com/triton-inf...))。我们知道,通常情况下,triton sever在服务启动后,输入输出是固定的,根据config.pbtxt来决定。但是在LLM的后处理中,我们经常需要采取各种各样的sampling策略,充斥着大量的条件判断以及采样策略。这些都是动态变化的,采样策略需要根据具体传入的条件值进行不同的处理。因此,tensorrtllm_backend就整了一个BLS模式,来解决这个问题。(BLS这个名称不太好理解,说简单点,就是sampling后处理大杂烩的缝合......)。BLS模式介绍文档:https://github.com/triton-inf...
BLS Inputs
- bls_instance_count
按照tensorrtllm_backend文档的建议,这个值需要设置成和trtllm-build engine时使用max_batch_size相同的值,以确保服务能够对请求进行并发处理。如果设置为1,那可能会导致服务变成串行处理的(当然,如果你不使用BLS模式,应该不会有这个问题)。具体实践中,我尝试过instance_count=1以及instance_count>=max_batch_size,对于前者,会发现每个请求处理的时间变长了,具体表现在TTFT耗时明显变长。当instance_count>=max_batch_size时,能够确保足够的并行性。
- accumulate_tokens
BLS 模式中有个叫做accumulate_tokens的参数可以在流式请求中使用。当该参数为True时,BLS在调用postprocessing models时,会使用累计的token ids,而不仅仅是当前step的单个token id;这会影响tokenizer解码的结果,特别是,某些special token实际上是由几个token ids组成的时候,需要设置accumulate_tokens为True。
parameters: { key: "accumulate_tokens" value: { string_value: "True" }}
0x09 如何开启debug模式
- log-verbose
通过指定log-verbose=3, 可以在启动triton server后,看到详细的运行日志。包括每个step的状态和运行信息等。launch_triton_server.py中如果开启的log模式,则使用的log-verbose就是3。
launch_triton_server.py
输出日志:
[TensorRT-LLM][INFO] {"Active Request Count":249,"Context Requests":8,"Free KV cache blocks":0,"Generation Requests":231,"Iteration Counter":90,"Max KV cache blocks":2448,"Max Request Count":256,"MicroBatch ID":0,"Runtime CPU Memory Usage":28784,"Runtime GPU Memory Usage":540173600,"Runtime Pinned Memory Usage":0,"Scheduled Requests":239,"Timestamp":"12-13-2023 14:55:14","Tokens per KV cache block":128,"Total Context Tokens":6904,"Used KV cache blocks":2448}
Debug日志
- Performance Analysis
TensorRT-LLM性能分析可以参考官方文档:
Performance Analysisnvidia.github.io/TensorRT-LLM/performance/perf-analysis.html
0x0a tensorrtllm_backend使用问题
本小节记录一些tensorrtllm_backend的使用问题,流水账。由于tensorrtllm_backend和TensorRT-LLM是属于两个分离的模块,因此使用上感觉不是很丝滑。也容易引入一些额外的使用问题。目前看到社区所有的LLM推理框架,server和推理引擎都是一个整体的,比如vllm,你不需要额外去理解FastAPI的概念,只需要关注vllm.entrypoint.api_server即可,使用起来也很简单。但是目前,tensorrtllm_backend和TensorRT-LLM是分开的,当用户想要跑个服务时,还必须熟悉Triton Server这一套,不然TensorRT-LLM也无法用起来。这里也记录一下使用tensorrtllm_backend时需要注意的问题。
- 版本一致性问题
tensorrtllm_backend和TensorRT-LLM的版本目前是严格对应的。或者说,tensorrtllm_backend里边的triton models应该暂时还没考虑向后兼容。你用commit 480的tensorrtllm_backend编译的server镜像来跑某个<480的commit里边的triton models,有可能是跑不通的。因此,当你升级TensorRT-LLM和tensorrtllm_backend时,需要注意把models也都换了。不然,大概率会出现一些奇怪的bugs。
- 使用体验问题
因为server代码和TensorRT-LLM框架是分离的,导致每次TensorRT-LLM和tensorrtllm_backend升级,用户都得手动拷贝server代码,并且检查到底更新了啥,哪些地方是和现在业务里边用的产生了冲突。从用户体验上来说,这是一个非常割裂和Ugly的过程。举个例子,试想一下,假设你现在用的是vllm,然后vllm的server代码在另外一个repo,现在vllm框架升级了,但是server代码不在框架内,你得手动拷贝server代码来用,并检查有什么需要修改和注意的。这是一件非常痛苦的事情。比较希望TensorRT-LLM能够将server整合到一起,提供一个简单的server使用方式,比如:
![image.png](/img/bVb82h)
PS: 这不禁让我想起了TensorFlow和PyTorch的静态图vs动态图的故事。目前看来,由于triton server和TensorRT-LLM本来就是两个框架,虽然不可能进行整合,但是在TensorRT-LLM中增加一个server模块,基于triton server封装统一的API,从而将server相关的代码也整合进TensorRT-LLM,这应该是完全可能的。似乎lmdeploy目前就是这个做法,上手用户体验感觉不错。所以,这个用户体验的问题完全是可以解决的。
lmdeploy 对triton server的封装
目前使用下来,最深刻的体验就是,花在非性能问题的排查上的时间是最多的,特别是triton server。
- OpenAI协议问题
tensorrtllm_backend目前似乎没有支持OpenAI协议,因此,如果需要使用OpenAI协议,还得用户自己包一层FastAPI,这个过程,有难免会引入新的问题,或者新的bug,排查起来,也不是很高效。大家都支持,没搞懂为啥TensorRT-LLM不支持?为啥要提这个点,还是回到实际的应用,如果已经有应用使用了OpenAI协议,那为了使用TRT-LLM还得做对应的修改。
- 其他
嗯,是的。本文部分内容存在吐槽的倾向。当然,吐槽归吐槽,该用还是老老实实用着(你就说他快不快吧)。总之,还是非常感谢TensorRT-LLM团队将这么优秀的工作开源到社区(虽然是部分开源)。Anyway,祝TensorRT-LLM越做越好。最后,再补画一个自己会比较喜欢的使用模式。
假设存在这样的trtllm-launcher
trtllm-launcher --model Qwen/Qwen1.5-72B-Chat --tensor-parallel-size 8 --enable-kv-cache-reuse --use-custom-all-reduce --enforce-xqa ...
0x0b tensorrt_llm离线推理
- ModelRunner和ModelRunnerCpp的不统一
最近想在多模态场景下将examples中的ModelRunner切换成ModelRunnerCpp,以便可以使用prefix caching的功能。似乎是多模态的模型还不支持使用ModelRunnerCpp。不过为啥会存在两套API和功能都不完全对齐的ModelRunner呢?还不止这个。ModelRunnerCpp支持更多的功能,现在源码编译tensorrt_llm就会默认编译python_binding。ModelRunnerCpp支持Prefix Caching和Chunk Context,ModelRunner不支持。并且对应的SamplingConfig虽然是同名,但是是两个不同的类。期待API完全统一。
- ModelRunnerCpp使用
可以参考示例:https://github.com/NVIDIA/Ten...
# 初始化ModelRunnerCpp
if test_trt_llm:
if not PYTHON_BINDINGS and not args.use_py_session:
logger.warning(
"Python bindings of C++ session is unavailable, fallback to Python session."
)
args.use_py_session = True
runner_cls = ModelRunner if args.use_py_session else ModelRunnerCpp
runner_kwargs = dict(engine_dir=args.engine_dir,
rank=runtime_rank,
debug_mode=args.debug_mode,
gpu_weights_percent=args.gpu_weights_percent)
if args.medusa_choices is not None:
args.medusa_choices = ast.literal_eval(args.medusa_choices)
assert args.temperature == 1.0, "Medusa should use temperature == 1.0"
assert args.num_beams == 1, "Medusa should use num_beams == 1"
runner_kwargs.update(medusa_choices=args.medusa_choices)
if not args.use_py_session:
runner_kwargs.update(
max_batch_size=max_batch_size,
max_input_len=test_token_num,
max_output_len=output_len,
max_beam_width=num_beams,
max_attention_window_size=max_attention_window_size,
sink_token_length=sink_token_length,
max_tokens_in_paged_kv_cache=args.max_tokens_in_paged_kv_cache,
kv_cache_enable_block_reuse=args.kv_cache_enable_block_reuse, # 是否用prefix caching
kv_cache_free_gpu_memory_fraction=args.
kv_cache_free_gpu_memory_fraction,
enable_chunked_context=args.enable_chunked_context,
)
runner = runner_cls.from_dir(**runner_kwargs)
# 调用generate接口
with torch.no_grad():
outputs = runner.generate(
batch_input_ids,
max_new_tokens=output_len,
max_attention_window_size=max_attention_window_size,
sink_token_length=sink_token_length,
end_id=end_id,
pad_id=pad_id,
temperature=temperature,
top_k=top_k,
top_p=top_p,
stop_words_list=stop_words_list,
bad_words_list=bad_words_list,
num_beams=num_beams,
length_penalty=length_penalty,
early_stopping=early_stopping,
repetition_penalty=repetition_penalty,
presence_penalty=presence_penalty,
frequency_penalty=frequency_penalty,
lora_uids=args.lora_task_uids,
output_sequence_lengths=True,
return_dict=True,
medusa_choices=args.medusa_choices)
torch.cuda.synchronize()
提示:不推荐使用SamplingConfig来指定采样参数,目前SamplingConfig很乱,在python实现里边有两个同名的类(最新的更新似乎已经删除了sampling_config这个参数?up to 202406011),一个是binding C++的SamplingConfig,一个纯python实现给ModelRunner(不是ModelRunnerCpp)用的。
PS: 个人觉得这个离线推理的示例还是过于复杂了,我还是喜欢vLLM这种简单明了的风格:
- ModelRunner
不推荐使用,目测只是个临时的产物,极大概率是会抛弃的。它引入了很多使用上的困惑和模糊,功能支持也没有ModelRunnerCpp完善,比如不支持prefix caching、chunk context等。
- High-Level API
TensorRT-LLM目前正在开发High-Level API,看着使用方式比较自然,不过目前只支持LLaMA系列的模型。先关注着,看下后续的发展(静待花开)。示例参考:https://https://github.com/NV...
High-Level API
High-Level API把LLM离线推理拆解成3个步骤:config -> init -> generate
0x0c 源码编译及benchmark工具使用
- 源码编译
这里简单贴一下我源码编译的命令,仅做参考,不保证在其他环境能跑通。我的基础镜像和CUDA版本:nvcr.io/nvidia/pytorch:24.02-py3(https://nvcr.io/nvidia/pytorc...)、CUDA 12.3以及TensorRT 10.
apt-get update
apt-get install sudo
# prepare for mpi4py buildapt install openmpi-bin libopenmpi-dev
unset CPLUS_INCLUDE_PATH && unset C_INCLUDE_PATHexport CPLUS_INCLUDE_PATH=//usr/local/mpi/include/:$CPLUS_INCLUDE_PATHexport C_INCLUDE_PATH=/usr/local/mpi/include/:$C_INCLUDE_PATHexport LD_LIBRARY_PATH=/opt/hpcx/ucx/lib:/opt/hpcx/ompi/lib:/usr/lib/x86_64-linux-gnu/:/usr/local/lib/python3.10/dist-packages/torch/lib:/usr/local/cuda/lib64:/usr/local/cuda/compat/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64# 安装TensorRT 10.0.1.6wget https://developer.nvidia.com/downloads/compute/machine-learning/tensorrt/10.0.1/tars/TensorRT-10.0.1.6.linux.x86_64-gnu.cuda-12.4.tar.gz --no-check-certificate
tar -xvf TensorRT-10.0.1.6.linux.x86_64-gnu.cuda-12.4.tar.gz
rm -f $(find /usr/lib/x86_64-linux-gnu -name "libnvinfer*.so*")rm -f $(find /usr/lib/x86_64-linux-gnu -name "libnv*parser*.so*")cp TensorRT-10.0.1.6/lib/*.so* /usr/lib/x86_64-linux-gnu
cp TensorRT-10.0.1.6/bin/trtexec /opt/tensorrt/trtexec
cp TensorRT-10.0.1.6/bin/trtexec /opt/tensorrt/bin/
python3 -m pip install TensorRT-10.0.1.6/python/tensorrt-10.0.1-cp310-none-linux_x86_64.whl
rm -rf /usr/local/tensorrt
cp -r TensorRT-10.0.1.6 /usr/local/tensorrt# 下载TensorRT-LLM源码git clone https://github.com/NVIDIA/TensorRT-LLM.git
git submodule update --init --recursive --force# 手动安装一些依赖(直接install requirement.txt容易被mpi4py卡主)pip config set global.index-url https://mirrors.cloud.tencent.com/pypi/simple
python3 -m pip uninstall cugraph torch torch-tensorrt tensorrt transformer-engine flash-attn torchvision torchtext torchdata torchaudio dask-cuda cugraph-service-server cuml -y
python3 -m pip install cmake
python3 -m pip install --no-cache-dir --extra-index-url https://pypi.nvidia.com mpi4py
python3 -m pip install -r requirements.txt
python3 -m pip install -r requirements-dev.txt
python3 -m pip install flash-attn --no-build-isolation# 源码编译 全架构 比较耗时python3 ./scripts/build_wheel.py --clean --trt_root /usr/local/tensorrt -j 48# 编译指定架构Ada: sm89, Ampere sm80: A30,A800,A100, sm86: 4090,4080,...python3 ./scripts/build_wheel.py --clean --cuda_architectures "89-real" --trt_root /usr/local/tensorrt
默认编译是不带benchmark功能。需要在源码编译TensorRT-LLM的时候,指定benchmark选项:
python3 ./scripts/build_wheel.py --clean --benchmarks --trt_root /usr/local/tensorrt -j 48# 如果是在conda环境,可能还需要指定对应的python3进行编译python3 ./scripts/build_wheel.py --clean --benchmarks --trt_root /usr/local/tensorrt -j 48 \ --extra-cmake-vars PYTHON_EXECUTABLE=/usr/bin/python3
跑benchmark之前,还要准备好模型engine文件,这里以Internlm2-chat-20b为例(需要main分支最新的代码)。
# InternLM2 TRT-LLM性能测试 FP16 TP2cd TensorRT-LLM/examples/internlm2
python convert_checkpoint.py \ --model_dir ./internlm2-chat-20b/ \ --dtype float16 \ --output_dir ./tmp_fp16_tp2 \ --tp_size 2trtllm-build \ --checkpoint_dir ./tmp_fp16_tp2 \ --output_dir ./engine/internlm2-chat-20b/fp16/2-gpu/ \ --max_batch_size 8 \ --max_input_len 3072 \ --max_output_len 1024 \ --max_num_tokens 16384 \ --max_beam_width 1 \ --workers 2 \ --gemm_plugin float16 \ --gpt_attention_plugin float16 \ --remove_input_padding enable \ --paged_kv_cache enable \ --context_fmha enable \ --logits_dtype float32 \ --context_fmha_fp32_acc enable \ --use_paged_context_fmha enable
- gptSessionBenchmark
gptSessionBenchmark跑的是static batching,可以用来快速看一下模型的性能,做一下性能收益评估之类的。比如:
export TRTLLM_BIN_DIR=/workspace/TensorRT-LLM/cpp/buildmpirun --allow-run-as-root -n 2 $TRTLLM_BIN_DIR/benchmarks/gptSessionBenchmark \ --engine_dir ./engine/internlm2-chat-20b/fp16/2-gpu/ \ --batch_size "1" --warm_up 2 --num_runs 20 \ --input_output_len "3072,128"
又比如,你想快速预估一下prompt为2048 token时的首Token的耗时:
export TRTLLM_BIN_DIR=/workspace/TensorRT-LLM/cpp/buildmpirun --allow-run-as-root -n 2 $TRTLLM_BIN_DIR/benchmarks/gptSessionBenchmark \ --engine_dir ./engine/internlm2-chat-20b/fp16/2-gpu/ \ --batch_size "1" --warm_up 2 --num_runs 20 \ --input_output_len "2048,1"
- gptManagerBenchmark
gptSessionBenchmark跑的是static batching,没有In-Flight Batching,如果需要跑IFB,则可以使用gptManagerBenchmark。gptManagerBenchmark是模拟请求发送和调度的,因此在使用前,需要先准备好数据集。TensorRT-LLM提供了一个prepare_dataset.py脚本可以帮助用户快速准备测试数据集(只关注性能)。
在性能评估中,我们经常需要固定输入和输出的长度,那么可以这样:
# 准备定长2048 token输入,定长128输出cd TensorRT-LLM/benchmarks/cpppython3 prepare_dataset.py \ --output ./tokens-fixed-lengths.json \ --tokenizer PATH-TO/internlm2-chat-20b/ \ token-norm-dist \ --num-requests 512 \ --input-mean 2048 --input-stdev 0 \ --output-mean 128 --output-stdev 0
产出的数据会保存在json文件中,大概长这样:
{"metadata": {"workload_type": "token-norm-dist", "input_mean": 2048, "input_stdev": 0, "output_mean": 128, "output_stdev": 0, "num_requests": 512, "tokenize_vocabsize": 92544, "max_input_len": 2048, "max_output_len": 128, "workload_name": "workload_type:token-norm-dist__input_mean:2048__input_stdev:0__output_mean:128__output_stdev:0__num_requests:512__tokenize_vocabsize:92544__max_input_len:2048__max_output_len:128"}, "samples": [{"input_len": 2048, "input_ids": [3452, 88226, 47775, 35731, 52781, 12529, 86990, 88852, 86033, 14974, 3960, 68403, 42664, 63581, 85238, 2417, 81796, 16710, 71676, 43421, 56550, 35152, 16223, 44050, 35639, 19196, 89462, 13952, 34254, 64423, 24180, 63111, 87473, 13318, 32424, 65016, 1218, 51691, 79986, 10
运行gptManagerBenchmark,并指定调度方式为IFB,request_rate表示需要模拟的QPS:
export CUDA_VISIBLE_DEVICES=0,1export TRTLLM_BIN_DIR=/workspace/TensorRT-LLM/cpp/buildmpirun --allow-run-as-root -n 2 $TRTLLM_BIN_DIR/benchmarks/gptManagerBenchmark \ --engine_dir PATH-TO/engine/internlm2-chat-20b/fp16/2-gpu/ \ --type IFB --request_rate 10 --max_num_samples 128 --warm_up 2 \ --enable_kv_cache_reuse false --dataset ./tokens-fixed-lengths.json
需要注意的是max_batch_size这个参数,可能会对gptManagerBenchmark的吞吐有影响,太小的max_batch_size会限制TensorRT-LLM能够并行处理的请求数,太大的又容易导致OOM(哭,能不能搞成自动算好的,伸手党拒绝手工autotune...)。比如对于Intermlm2-chat-20b这个模型,在max_batch_size=8时你会发现高并发情况,trtllm不如lmdeploy,但是将max_batch_size改成>=16,结论可能就反过来了。这也是目前使用TensorRT-LLM比较难受的地方,总是要反复尝试才能得到一个最优配置。
- 模拟static batching一次发出所有请求
gptManagerBenchmark可以模拟static batching一次发出所有请求,但是需要注意的是,这只是模拟static batching一次发出所有请求的方式,但是内部应该还是走的IFB对到达的请求进行调度。
# 模拟static batching一次性发出所有请求(实际内部会按照IFB调度到达的请求)export CUDA_VISIBLE_DEVICES=0,1export TRTLLM_BIN_DIR=/workspace/TensorRT-LLM/cpp/buildmpirun --allow-run-as-root -n 2 $TRTLLM_BIN_DIR/benchmarks/gptManagerBenchmark \ --engine_dir PATH-TO/engine/internlm2-chat-20b/fp16/2-gpu/ \ --type IFB --request_rate -1 static_emulated_batch_size 16 \ --static_emulated_timeout 100 --max_num_samples 16 \ --enable_kv_cache_reuse false \ --dataset ./tokens-fixed-lengths.json
static_emulated_batch_size表示一次发送要组batch的大小,示例这里是16;static_emulated_timeout表示等待组batch的时长,单位是ms,如果超过这个时长没有组满batch,也会直接送给推理引擎。
- W8A18/W4A16性能测试
如果要测试weight only int8/int4量化,目前还是比较方便的,这个要点赞一下。通过转换脚本直接转换,并重新编译engine文件即可。TensorRT-LLM中weight only的实现原理和FasterTransformers中的应该是一样的,推荐看我之前写的几篇文章:
DefTruth:[LLM推理优化] WINT8/4-(00): 通俗易懂讲解-快速反量化算法https://zhuanlan.zhihu.com/p/...
DefTruth:[LLM推理优化] WINT8/4-(01): PRMT指令详解及FasterTransformer源码解析https://zhuanlan.zhihu.com/p/...
DefTruth:[LLM推理优化] WINT8/4-(02): 快速反量化之INT8转BF16https://zhuanlan.zhihu.com/p/...
DefTruth:[LLM推理优化] WINT8/4-(03): LOP3指令详解及INT4转FP16/BF16分析https://zhuanlan.zhihu.com/p/...
这里提供两个TensorRT-LLM使用W4A16和W8A16的参考脚本:
# W8A16 TP2python3 convert_checkpoint.py \ --model_dir $HF_MODELS/internlm2-chat-20b/ \ --dtype float16 \ --output_dir ./tmp_int8_tp2 \ --use_weight_only \ --weight_only_precision int8 \ --tp_size 2trtllm-build \ --checkpoint_dir ./tmp_int8_tp2 \ --output_dir $HF_MODELS/engine/internlm2-chat-20b/int8/2-gpu/ \ --max_batch_size 16 \ --max_input_len 3072 \ --max_output_len 1024 \ --max_num_tokens 16384 \ --max_beam_width 1 \ --workers 2 \ --gemm_plugin float16 \ --gpt_attention_plugin float16 \ --remove_input_padding enable \ --paged_kv_cache enable \ --context_fmha enable \ --logits_dtype float32 \ --context_fmha_fp32_acc enable \ --use_paged_context_fmha enable \ --weight_only_precision int8# W4A16 TP2python3 convert_checkpoint.py \ --model_dir $HF_MODELS/internlm2-chat-20b/ \ --dtype float16 \ --output_dir ./tmp_int4_tp2 \ --use_weight_only \ --weight_only_precision int4 \ --tp_size 2trtllm-build \ --checkpoint_dir ./tmp_int4_tp2 \ --output_dir $HF_MODELS/engine/internlm2-chat-20b/int4/2-gpu/ \ --max_batch_size 16 \ --max_input_len 3072 \ --max_output_len 1024 \ --max_num_tokens 16384 \ --max_beam_width 1 \ --workers 2 \ --gemm_plugin float16 \ --gpt_attention_plugin float16 \ --remove_input_padding enable \ --paged_kv_cache enable \ --context_fmha enable \ --logits_dtype float32 \ --context_fmha_fp32_acc enable \ --use_paged_context_fmha enable \ --weight_only_precision int4
跑gptManagerBenchmark替换为新编译的int8/int4的engine即可。这对于快速预估int8/int4的性能收益还是很方便的。点赞。
0x0d FP8/SQ/AWQ量化校准使用
TensorRT-LLM的量化工具在examples/quantization目录下,需要安装额外的依赖:
cd TensorRT/examples/quantizationpython3 -m pip install -r requirements.txt
- FP8量化-W8A8
如果只是为了快速评估FP8量化后的性能,可以不用额外准备数据。quantize.py脚本会使用默认的数据集cnn_dailymail进行FP8校准。比如,量化Qwen1.5-7B-Chat模型:
# FP8量化-W8A8python3 quantize.py \ --model_dir $HF_MODELS/Qwen1.5-7B-Chat/ \ --dtype float16 \ --qformat fp8 \ --output_dir ./tmp_fp8_tp2 \ --tp_size 2 \ --calib_size 256 \ --calib_max_seq_length 4096# build enginetrtllm-build \ --checkpoint_dir ./tmp_fp8_tp2 \ --output_dir $HF_MODELS/engine/Qwen1.5-7B-Chat/fp8/2-gpu/ \ --max_batch_size 16 \ --max_input_len 3072 \ --max_output_len 1024 \ --max_num_tokens 16384 \ --max_beam_width 1 \ --workers 2 \ --gemm_plugin float16 \ --gpt_attention_plugin float16 \ --remove_input_padding enable \ --paged_kv_cache enable \ --context_fmha enable \ --logits_dtype float32 \ --context_fmha_fp32_acc enable \ --use_paged_context_fmha enable
- SQ量化-INT8 W8A8
以LLaMA为示例,如果需要进行SmoothQuant量化,则需要在convert_checkpoint阶段进行。注意,这和FP8量化不一样,FP8量化走的是单独的量化脚本逻辑。参考:TensorRT-LLM/examples/llama at main · NVIDIA/TensorRT-LLM(https://github.com/NVIDIA/Ten...)
cd TensorRT-LLM/examples/llama# Build model for SmoothQuant in the _per_token_ + _per_channel_ modepython3 convert_checkpoint.py --model_dir /llama-models/llama-7b-hf \ --output_dir /tmp/tllm_checkpoint_1gpu_sq \ --dtype float16 \ --smoothquant 0.5 \ # alpha https://arxiv.org/pdf/2211.10438.pdf --per_token \ --per_channeltrtllm-build --checkpoint_dir /tmp/tllm_checkpoint_1gpu_sq \ --output_dir ./engine_outputs \ --gemm_plugin auto
TensorRT-LLM SmoothQuant默认使用per-tensor量化,但是我们可以指定per_token和per_channel来执行token和channel维度的混合量化。per_token针对激活进行量化,per_channel则是针对权重进行量化。
SmoothQuant
- AWQ-INT4 W4A16
TensorRT-LLM内置支持的INT4 Weight Only不用走量化校准的过程,但是实际应用时量化后的模型精度不一定完全符合要求。此时,可以考虑使用quantize.py脚本提供的AWQ 4bits量化功能。AWQ和SQ出自同一个作者,AWQ会根据激活的数值分布来判断哪些权重是属于重要的权重,AWQ论文为,《AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration》。作者指出,模型的权重并不同等重要,仅有0.1%~1%的小部分显著权重对模型输出精度影响较大。因此如果能有办法只对0.1%~1%这一小部分权重保持原来的精度(FP16),对其他权重进行低比特量化,就可以在保持精度几乎不变的情况下,大幅降低模型内存占用,并提升推理速度。推荐阅读:认输你就真了:W4A16模型量化大法 AWQ(https://zhuanlan.zhihu.com/p/...)
AWQ
以LLaMA为例,在TensorRT-LLM中使用AWQ,示例如下:
# Quantize HF LLaMA 7B checkpoint into INT4 AWQ formatpython ../quantization/quantize.py --model_dir ./tmp/llama-7b-hf \ --dtype float16 \ --qformat int4_awq \ --awq_block_size 128 \ --output_dir ./quantized_int4-awq \ --calib_size 32trtllm-build --checkpoint_dir ./quantized_int4-awq \ --output_dir ./tmp/llama/7B/trt_engines/int4_AWQ/1-gpu/ \ --gemm_plugin auto
参考示例:TensorRT-LLM/examples/llama at main · NVIDIA/TensorRT-LLM(https://github.com/NVIDIA/Ten...)
0x0e 自定义FP8量化校准数据(进行中)
TensorRT-LLM中的FP8校准使用默认的数据集,但是,为了满足更高的精度要求。我们通常需要使用SFT训练数据进行FP8校准。本小节记录一下FP8自定义校准数据的方式。(进行中,有空再更新)
0x0f triton server镜像编译和使用
参考官方文档:https://https://github.com/tr...
- 编译镜像
这个过程需要保证网络的畅通,编译过程需要下载和安装比较多依赖。官方编译命令:
# Update the submodulescd tensorrtllm_backendgit lfs installgit submodule update --init --recursive# Use the Dockerfile to build the backend in a container# For x86_64DOCKER_BUILDKIT=1 docker build -t triton_trt_llm -f dockerfile/Dockerfile.trt_llm_backend .# For aarch64DOCKER_BUILDKIT=1 docker build -t triton_trt_llm --build-arg TORCH_INSTALL_TYPE="src_non_cxx11_abi" -f dockerfile/Dockerfile.trt_llm_backend .
如果编译环境需要设置网络代理,可以稍微改一下dockerfile,并在build镜像传入代理:
ARG BASE_IMAGE=nvcr.io/nvidia/tritonserverARG BASE_TAG=24.04-py3ARG http_proxyARG https_proxy...
如果需要编译带benchmarks工具的trtllm + triton server镜像,则需要修改dockerfile中编译trtllm命令为:
# 如果需要编译带benchmarks工具的trtllm + triton server镜像,则需要修改编译trtllm命令为:RUN cd /app/tensorrt_llm && python3 scripts/build_wheel.py --benchmarks --trt_root="${TRT_ROOT}" -i -c && cd ..
编译后,tensorrt_llm源码及编译产物在镜像内的/app/tensorrt_llm目录内,benchmark工具则在/app/tensorrt_llm/cpp/build/benchmarks/目录中。编译镜像命令如下:
DOCKER_BUILDKIT=1 docker build \ --build-arg "http_proxy=http://ip:port" \ --build-arg "https_proxy=http://ip:port" \ -t triton_trt_llm -f dockerfile/Dockerfile.trt_llm_backend .
- 准备模型和triton models
参考官方文档:htthttps://github.com/triton-inf...,主要步骤是把tensorrtllm_backend事先写好的all_models拷贝到自己的目录下使用,并且把编译好的engine文件拷贝到tensorrt_llm/1这个目录位置。以下案例对应的目录位置是triton_model_repo/tensorrt_llm/1。注意,这里triton models指的是triton server中的model概念,一个服务就是一个model。
# Create the model repository that will be used by the Triton servercd /tensorrtllm_backend # 假设tensorrtllm_backend源码下载到这个位置mkdir triton_model_repo# Copy the example models to the model repositorycp -r all_models/inflight_batcher_llm/* triton_model_repo/# Copy the TRT engine to triton_model_repo/tensorrt_llm/1/cp tensorrt_llm/examples/internlm2/engines/fp16/2-gpu/* triton_model_repo/tensorrt_llm/1
- 修改config.pbtxt配置
需要修改配置的config.pbtxt主要包括:
triton_model_repo/preprocessing/config.pbtxt # 修改 tokenizer_dirtriton_model_repo/postprocessing/config.pbtxt # 修改 tokenizer_dirtriton_model_repo/tensorrt_llm/config.pbtxt # 可以考虑手动修改一些配置比如batch_scheduler_policy等triton_model_repo/tensorrt_llm_bls/config.pbtxt # 修改bls_instance_count等
举个例子,将preprocessing和postprocessing中的tokenizer路径修改为你自定义的路径:
parameters { key: "tokenizer_dir" value: { string_value: "/workspace/hf_models/internlm2-chat-20b" }}
又比如将tensort_llm/config.pbtxt中的调度策略修改为guaranteed_no_evict:
parameters: { key: "batch_scheduler_policy" value: { string_value: "guaranteed_no_evict" }}
- 填充config模板
这个是需要注意的,tensorrtllm_backend的README并没有提到这点。而是要在他的docs/llama.md示例中才能找到这个步骤。没有这个步骤,服务是跑不起来的。(提醒,如果采用这种方式生成config配置,就不要采用手动修改的方式了,避免报错)。以下是一个例子,-i 表示原地修改。另外注意需要修改bls_instance_count为build engine时的max_batch_size,否则会影响服务的性能。如果还需要支持流式返回,需要设置decoupled_mode:True。
export HF_INTERNLM2=/workspace/hf_models/internlm2-chat-20bexport ENGINE_PATH=/tensorrtllm_backend/triton_model_repo/tensorrt_llm/1python3 tools/fill_template.py -i triton_model_repo/preprocessing/config.pbtxt tokenizer_dir:${HF_INTERNLM2},triton_max_batch_size:16,preprocessing_instance_count:1python3 tools/fill_template.py -i triton_model_repo/postprocessing/config.pbtxt tokenizer_dir:${HF_INTERNLM2},triton_max_batch_size:16,postprocessing_instance_count:1python3 tools/fill_template.py -i triton_model_repo/tensorrt_llm_bls/config.pbtxt triton_max_batch_size:16,decoupled_mode:False,bls_instance_count:16,accumulate_tokens:Falsepython3 tools/fill_template.py -i triton_model_repo/ensemble/config.pbtxt triton_max_batch_size:16python3 tools/fill_template.py -i triton_model_repo/tensorrt_llm/config.pbtxt triton_backend:tensorrtllm,triton_max_batch_size:16,decoupled_mode:False,max_beam_width:1,engine_dir:${ENGINE_PATH},exclude_input_in_output:True,enable_kv_cache_reuse:False,batching_strategy:inflight_fused_batching,max_queue_delay_microseconds:0
- 启动服务
launch_triton_server.py封装了基本的triton server启动命令。注意,model_repo路径需要使用绝对路径。
cd /tensorrtllm_backend# --world_size is the number of GPUs you want to use for servingpython3 scripts/launch_triton_server.py --world_size=2 --model_repo=/tensorrtllm_backend/triton_model_repo
服务启动成功的话,日志大概长这样:
I0615 12:37:38.990772 225 tritonserver.cc:2538]
+----------------------------------+------------------------------------------------------------------------------------------------------------------------+
| Option | Value |
+----------------------------------+------------------------------------------------------------------------------------------------------------------------+
| server_id | triton |
| server_version | 2.45.0 |
| server_extensions | classification sequence model_repository model_repository(unload_dependents) schedule_policy model_configuration syste |
| | m_shared_memory cuda_shared_memory binary_tensor_data parameters statistics trace logging |
| model_repository_path[0] | /tensorrtllm_backend/triton_model_repo |
| model_control_mode | MODE_NONE |
| strict_model_config | 1 |
| rate_limit | OFF |
| pinned_memory_pool_byte_size | 268435456 |
| cuda_memory_pool_byte_size{0} | 67108864 |
| cuda_memory_pool_byte_size{1} | 67108864 |
| cuda_memory_pool_byte_size{2} | 67108864 |
| cuda_memory_pool_byte_size{3} | 67108864 |
| cuda_memory_pool_byte_size{4} | 67108864 |
| cuda_memory_pool_byte_size{5} | 67108864 |
| cuda_memory_pool_byte_size{6} | 67108864 |
| cuda_memory_pool_byte_size{7} | 67108864 |
| min_supported_compute_capability | 6.0 |
| strict_readiness | 1 |
| exit_timeout | 30 |
| cache_enabled | 0 |
+----------------------------------+------------------------------------------------------------------------------------------------------------------------+
I0615 12:37:39.005804 225 grpc_server.cc:2463] Started GRPCInferenceService at 0.0.0.0:8001
I0615 12:37:39.006061 225 http_server.cc:4692] Started HTTPService at 0.0.0.0:8000
I0615 12:37:39.047801 225 http_server.cc:362] Started Metrics Service at 0.0.0.0:8002
- 测试服务
更多示例,请参考:https://https://github.com/tr...
curl -X POST localhost:8000/v2/models/ensemble/generate -d '{"text_input": "What is machine learning?", "max_tokens": 20, "bad_words": "", "stop_words": ""}'
输出日志:
{"context_logits":0.0,"cum_log_probs":0.0,"generation_logits":0.0,"model_name":"ensemble","model_version":"1","output_log_probs":[0.0,0.0,0.0,0.0,0.0,0.0],"sequence_end":false,"sequence_id":0,"sequence_start":false,"text_output":" Machine learning is a type of artificial intelligence that allows software applications to become more accurate in predicting outcomes without"}
0x10 总结
本文主要关注TensorRT-LLM的应用,记录了一些使用TensorRT-LLM过程中,对性能有影响的参数的理解以及一些工具的用法。好记性,不如烂笔头,本文长期更新,内容随缘(就看最近踩到了什么坑~)。号外,LLM推理部署各方向新进展,推荐我整理的Awesome-LLM-Inference,传送门:https://github.com/DefTruth/A...
Awesome-LLM-Inference
参考文献
- [0]https://nvidia.github.io/Tens...(https://nvidia.github.io/TensorRT-LLM/performance/perf-best-practices.html)
- [1] 知北游:GPU通信技术(介绍了常见的“数据通信路径”,NVLink/P2P/GDR/GDS等)(https://zhuanlan.zhihu.com/p/...)
- [2] 一棵红杉树:一文读懂GPU通信互联技术(https://zhuanlan.zhihu.com/p/...)
- [3] Linux编程用C:一文掌握CPU的SMP与NUMA架构!(https://zhuanlan.zhihu.com/p/...)
- [4] 飞速FS:100G网卡NIC详细介绍及其发展趋势分析(https://zhuanlan.zhihu.com/p/...)
- [5] 函谷叨客:【研究综述】浅谈GPU通信和PCIe P2P DMA(https://zhuanlan.zhihu.com/p/...)
- [6] 神经蛙没头脑:高性能GPU服务器AI网络架构(上篇)(https://zhuanlan.zhihu.com/p/...)
- [7] 神经蛙没头脑:高性能GPU服务器AI网络架构(下篇)(https://zhuanlan.zhihu.com/p/...)
- [8] 极致Linux内核:什么是NIC(网络接口卡)?(https://zhuanlan.zhihu.com/p/...)
- [9] https://nvidia.github.io/Tens...
The End
作者:DefTruth
来源:大模型生态圈
推荐阅读
- 北理工提出 LTrack 双摄像头系统 | 专注于暗场景多目标跟踪,自动驾驶和夜间监控的福音!
- 窥探Triton的lower(三)
- 窥探Trition的lower(一)
- MLIR_对自定义IR Dialect编写bufferization pass
欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区嵌入式AI专栏欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。