昇腾CANN 8.0基于LLM P-D分离部署方案发布LLM-DataDist组件:高效低成本,简单易集成
1 摘要
大模型推理的Prefill和Decode阶段往往存在计算和内存受限问题,资源分配不均,导致成本居高不下,这些趋势和挑战,都驱动着推理系统的重构,以更高效的资源调度与并行解码,满足推理商业落地的经济性要求。在昇腾AI异构计算架构CANN最新推出的8.0版本中,基于LLM P-D分离部署方案设计并发布LLM-DataDist组件,API简单易用,可以低开发成本被MindIE-LLM、vLLM等大模型推理框架集成,并在近一年支撑了多个大模型业务的规模化商用。
2 P-D分离部署方案介绍
ChatGPT的推出意味着交互式人工智能逐渐成为商业化成熟产品,同时也进一步推动了底层技术大型语言模型LLM的研究和进步。现有主要使用的LLM有GPT系列、LLAMA系列、GLM系列,其模型基础架构都采用Transformer架构,并以Decoder-Only为主。
该类Transformer-Based-Decoder-Only的LLM在推理预测时,采用自回归生成(auto-aggressive generative)模式,即每个Token生成需要经过LLM模型的前向推理过程,即完成N次Transformer Layer层计算,意味着包含M个Token的语句完整生成需要经过M次完整LLM前向推理过程。
作为交互式AI技术的典型应用,ChatGPT对人机交互的响应速度有着极高的要求,从人类阅读速度出发,LLM的推理速度至少需要达到50ms/token。为了充分利用昇腾硬件强大的并行计算优势,推理系统在服务和任务调度层面实施了一系列工程优化措施,旨在通过提高数据密度来最大化并行计算的优势,例如增大Batch Size、扩展Sequence长度等。降低LLM推理时延、提升LLM推理集群的吞吐和算力利用率,成为相关AI应用大规模产品化变现的迫切要求。
在Transformer推理过程中利用KV Cache技术可降低Decoding阶段的计算量,目前已成为LLM推理系统的必选技术。
采用KV Cache的LLM推理过程通常分为预填充(Prefill)和解码(Decode)两个阶段。
- Prefill阶段:将用户请求的prompt传入大模型,进行计算,中间结果写入KV Cache并推出第1个token。随着Prompt Sequence Length长度线性增长,对首Token时延(TTFT)指标有要求的业务通常采用单batch方式执行LLM推理,该阶段属于计算密集型操作。
- Decode阶段:将请求的前1个Token传入大模型,从显存读取前文产生的KV Cache再进行计算。采用KV Cache技术后,单Token计算量低,而主要瓶颈在于搬运参数量,故通常需要采用多batch方式提升利用率,该阶段属于访存密集型操作。
图2-1 基于KV Cache的LLM推理过程
从实现上来看,整个LLM推理过程由Prefill和多轮迭代的Decode完成,如下图所示,想要在Decode阶段实现Continuous batching的前提是,每个被调度的Request需要空闲算力完成Prefill计算,按现有的部署模式,当Prefill和Decode部署在一起时,Prefill和Decode会交叉执行,从而导致增量Token时延(TBT)无法得到有效保障。
例如下图,当request5或request6到来时,系统可能会优先执行request5或request6的Prefill,此时request2/3/4的响应时延会受到一定影响,从而导致TBT不稳定。
图2-2 LLM响应时延分析
在实际的深度学习模型部署中,考虑到Prefill和Decode两阶段的计算/通信特征的差异特点,为了提升性能和资源利用效率,通过P-D分离部署方案将Prefill和Decode分别部署在不同规格和架构的集群中,并且配合服务层的任务调度,在满足TTFT和TBT指标范围内,结合Continuous batching机制尽可能提高Decode阶段的batch并发数,在提供更好用户体验的前提下,提升算力利用率。
图2-3 P-D分离部署方案
基于该方案,结合下图可以看到Prefill和Decode的执行互不影响,系统能够提供给用户一个稳定的TBT。
图2-4 基于P-D分离部署方案的LLM响应时延分析
基于以上收益,自2023年11月以来,业界对P-D分离的研究和应用持续火热,昇腾异构计算架构CANN针对LLM P-D分离部署方案设计LLM-DataDist组件,这一组件在支撑多个客户场景规模化落地方面发挥了重要作用,并经过多轮迭代优化后,作为CANN 8.0的关键特性之一正式发布。
3 LLM-DataDist整体架构
LLM-DataDist作为大模型分布式集群和数据管理组件,提供KV Cache传输及链路管理能力,并通过简易的API开放给用户,支持快速构建布式集群链路、动态扩缩等特性。同时利用昇腾集群多样化通信链路(HCCS/RoCE)可实现跨实例和集群的高效KV Cache传输,并支持与主流LLM推理框架如MindIE-LLM、vLLM等的集成。
图3-1 LLM-DataDist整体架构
从完整的P-D分离系统角度来看,LLM-DataDist通过link-mgr和cache-mgr支撑起了基于昇腾AI硬件的P-D分离系统的构建:
图3-2 基于CANN的P-D分离系统示意图
从一个简单的PD分离系统角度看来,基于LLM-DataDist提供的cache-mgr和link-mgr,上层可以构建如下能力:
- 集群管理层:基于link-mgr来构建起P-D集群的管理,并可以在业务的忙闲时以及故障场景下完成Prefill或Decode节点的上下线。
- 服务化框架层:基于cache-mgr的能力来构建起P-D之间的KV Cache传输,可以结合常见的KV Cache优化手段,例如PA(Paged Attention)、公共前缀缓存等。
4 LLM-DataDist支持动态建链,灵活调整集群配比
根据业务流量和Input/output负载不同,基于LLM-DataDist的动态链路管理能力,可以灵活设置P-D节点的集群配比数,在Serving服务运行过程中支持时延无感知的节点动态扩缩容,也可以在运行过程中实时灵活切换P-D角色。
开放API支持P-D角色划分
为了方便角色的管理,抽象出LLMDataDist类来表示Prefill还是Decode,通过实例化参数role进行区分,LLMDataDist类主要的几个方法如下:
- init:初始化LLM-DataDist,初始化过程中可以设置相关的超时时间,所使用的device等信息。
- finalize:与初始化相对应,用于释放资源。
- switch_role:切换当前LLM-DataDist的角色,支持在不中断业务的情况下,无缝互换P-D角色。
开放API支持链路管理
NN模型执行时的HCCL集合通信接口是双边通信,即需要两边同时发起建链,而在P-D分离方案中,简化建链操作,由Client单侧发起建链。由于动态扩缩的部分往往是Decode侧,因此将P定义为Server端,D定义为Client端,建链过程实现由D向P发起建链的流程,接口能力上主要提供了如下两个API:
- link_clusters,发起方为Decode侧,参数为Prefill侧的卡ip和端口号,示例如下:
from llm_datadist import LLMDataDist, LLMRole, LLMStatusCode, LLMClusterInfo
llm_datadist = LLMDataDist(LLMRole.DECODER, 0)
cluster = LLMClusterInfo()
cluster.remote_cluster_id = 1
cluster.append_local_ip_info("1.1.1.1", 26000)
cluster.append_remote_ip_info("1.1.1.1", 26000)
ret, rets = llm_datadist.link_clusters([cluster], 5000)
if ret != LLMStatusCode.LLM_SUCCESS:
raise Exception("link failed.")
for cluster_i in range(len(rets)):
link_ret = rets[cluster_i]
if link_ret != LLMStatusCode.LLM_SUCCESS:
print(f"{cluster_i} link failed.")
- unlink_clusters,发起方为Decode侧,参数为Prefill侧的卡ip和端口号,示例如下:
from llm_datadist import LLMDataDist, LLMRole, LLMStatusCode, LLMClusterInfo
llm_datadist = LLMDataDist(LLMRole.DECODER, 0)
cluster = LLMClusterInfo()
cluster.remote_cluster_id = 1
cluster.append_local_ip_info("1.1.1.1", 26000)
cluster.append_remote_ip_info("1.1.1.1", 26000)
ret, rets = llm_datadist.unlink_clusters([cluster], 5000)
if ret != LLMStatusCode.LLM_SUCCESS:
raise Exception("unlink failed.")
for cluster_i in range(len(rets)):
unlink_ret = rets[cluster_i]
if unlink_ret != LLMStatusCode.LLM_SUCCESS:
print(f"{cluster_i} unlink failed.")
link-mgr主要业务应用场景举例:
- 集群可靠性,当有P或D节点故障时,在不影响整个集群可用性的前提下,下线对应机器。
- 业务闲忙时应对,可以动态调整P-D的配比,以最大化LLM集群的吞吐。
5 LLM-DataDist支持P-D间高效KV Cache传输
对于分离集群间的KV Cache传输,通过昇腾AI处理器自带的8 * 200Gb RoCE网卡,独立的KV Cache传输通信链路与P/D模型并行使用的HCCS链路完全物理隔离,使大多数场景传输额外时延控制在一个Token的生成开销内,做到KV Cache传输被计算时延所掩盖。
开放API支持KV Cache管理
cache-mgr主要提供如下API:
- allocate_cache:申请KV Cache,为方便后续的KV Cache传输,可以为对应的KV Cache设置一个key。
- deallocate_cache:释放KV Cache。
- remove_cache_key:基于key删除在Prefill节点上的一块KV Cache,一旦删除,这块KV Cache就不可以被拉取。
- pull_cache:由Decode节点发起,从Prefill节点拉取对应的KV Cache。
- copy_cache:将KV Cache进行copy,在上一次nn没执行完成时,可以先将下一次的KV Cache进行暂存,待nn执行结束后,copy到指定的位置,从而最大程度打满Decode的batch。
同时,针对PA(Paged Attention)场景提供单独一套API,在pull blocks时进行传输上的性能优化,利用Roce网卡的Recv Scatter能力进行加速:
- allocate_blocks_cache:PA场景下,KV blocks的申请接口。
- pull_blocks:PA场景下,KV blocks的拉取接口。
- copy_blocks:PA场景下,KV blocks的copy接口。
- swap_blocks:KV blocks的换入换出,主要面向用户需要自行管理KV Cache的场景。
6 基于LLM-DataDist的实践
LLM-DataDist提供了简易的、可集成的API,不仅可以直接与PyTorch、MindSpore等AI框架结合使用,也可以简单的集成至vLLM等服务化框架中。
6.1 AI框架直调示例
以PyTorch框架为例,Prefill阶段代码样例为:
prompt_engine = LLMDataDist(LLMRole.PROMPT, cluster_id)
# 分离部署资源初始化
llm_config = LLMConfig()
prompt_engine.init(llm_config.generate_options())
# 申请KV Cache
kv_cache_manager = prompt_engine.kv_cache_manager
kv_cache_desc = CacheDesc(num_tensors=80, shape=[4, 2048, 8 // world_size, 128],
data_type=DataType.DT_FLOAT16)
kv_cache_keys = [CacheKey(prompt_cluster_id=0, req_id=0, model_id=0)]
cache = cache_manager.allocate_cache(kv_cache_desc, kv_cache_keys)
# 模型执行,将申请好的KV Cache传递给模型,替换原模型中的KV Cache tensor
kv_tensor_addrs = cache.per_device_tensor_addrs[0]
# 这里使用了PyTorch在NPU上的图模式--TorchAir
kv_tensors = torchair.llm_datadist.create_npu_tensors(cache.cache_desc.shape, torch.float16, kv_tensor_addrs)
mid = len(kv_tensors) // 2
k_tensors = kv_tensors[: mid]
v_tensors = kv_tensors[mid:]
kv_cache_tensors = list(zip(k_tensors, v_tensors))
# 此处传递的参数根据不同模型区分全量和增量的入参进行调整
kwargs["kv_tensors"] = kv_cache_tensors
outputs = runner.execute_model(input_tensors, **kwargs)
# 资源释放
kv_cache_manager.deallocate_cache(cache)
kv_cache_manager.remove_cache_key(kv_cache_keys[0])
prompt_engine.finalize()
Decode阶段代码样例为:
decoder_engine = LLMDataDist(LLMRole.DECODER, cluster_id)
# 分离部署资源初始化
llm_config = LLMConfig()
decoder_engine.init(llm_config.generate_options())
# 动态集群链路管理
clusters_info = LLMClusterInfo()
clusters_info.remote_cluster_id = 1
clusters_info.append_local_ip_info("1.1.1.1", 26000)
clusters_info.append_remote_ip_info("1.1.1.1", 26000)
decoder_engine.link_clusters([clusters_info])
# 申请KV Cache,pull_cache
kv_cache_desc = CacheDesc(num_tensors=80, shape=[4, 2048, 8 // world_size, 128],
data_type=DataType.DT_FLOAT16)
cache = cache_manager.allocate_cache(kv_cache_desc)
prompt_cache_key = CacheKey(prompt_cluster_id=0, req_id=0, model_id=0)
cache_manager.pull_cache(prompt_cache_key, cache, 0)
# 模型执行,将申请好的KV Cache传递给模型,替换原模型中的KV Cache tensor
kv_tensor_addrs = cache.per_device_tensor_addrs[0]
# 这里使用了PyTorch在NPU上的图模式--TorchAir
kv_tensors = torchair.llm_datadist.create_npu_tensors(cache.cache_desc.shape, torch.float16, kv_tensor_addrs)
mid = len(kv_tensors) // 2
k_tensors = kv_tensors[: mid]
v_tensors = kv_tensors[mid:]
kv_cache_tensors = list(zip(k_tensors, v_tensors))
# 此处传递的参数根据不同模型区分全量和增量的入参进行调整
kwargs["past_key_values"] = kv_cache_tensors
outputs = runner.execute_model(input_tensors, **kwargs)
# 资源释放
kv_cache_manager.deallocate_cache(cache)
decoder_engine.unlink_clusters([clusters_info])
decoder_engine.finalize()
6.2 服务化框架集成示例
目前,LLM-DataDist已支持与主流LLM推理框架如MindIE-LLM、vLLM等集成。在服务化场景中,主要适配方向是用LLM-DataDist提供的KV Cache的管理API替换服务化框架中KV Cache管理。
以vLLM为示例,主要适配点如下:
# 创建LLMDataDist,并初始化
llm_datadist = LLMDataDist(role, cluster_id)
llm_datadist.init(options)
# Decode阶段建链
cluster = LLMClusterInfo()
cluster.remote_cluster_id = prompt_cluster_id
cluster.append_local_ip_info("1.1.1.1", 26000)
cluster.append_remote_ip_info("1.1.1.1", 26000)
llm_datadist.link_clusters([cluster])
# 申请block cache内存
kv_cache_manager = llm_datadist.kv_cache_manager
blocks_cache_key = BlocksCacheKey(prompt_cluster_id, model_id)
cache = kv_cache_manager.allocate_blocks_cache(kv_cache_desc, blocks_cache_key)
# Decode阶段pull blocks
kv_cache_manager.pull_blocks(prompt_cache_key, cache, prompt_blocks_indices, decoder_blocks_indices)
# 通过cache创建torch的tensor
kv_tensor_addrs = cache.per_device_tensor_addrs[0]
kv_tensors = torchair.llm_datadist.create_npu_tensors(cache.cache_desc.shape, torch.float16, kv_tensor_addrs)
# 资源释放
kv_cache_manager.deallocate_cache(cache)
llm_datadist.unlink_clusters([cluster])
llm_datadist.finalize()
- 点赞
- 收藏
- 关注作者
评论(0)