背景概述

随着大语言模型(LLM)推理需求的增长,优化推理性能成为关键挑战。vLLM-Ascend作为针对昇腾硬件优化的推理框架,引入了ACLGraph图模式以降低算子下发开销,提升吞吐和时延性能。

本文基于Qwen3-8B模型,结合实际开发经验,系统介绍ACLGraph的应用方法、性能分析流程及优化实践,为开发者提供可复用的参考方案。

1. 图模式基础概念

1.1 eager模式

顾名思义,eager的中文意思叫做“即时”,这种模式下模型中的每个操作对应的算子会立即执行,CPU侧会逐个下发各个算子到NPU,这使得调试和开发更加直观和方便。每个算子都会立即返回结果。

1.2 图模式

在图模式中,各个算子会先被合成一个graph,graph相当于一个大算子,作为一个整体进行编译和执行,减少了大量的host侧到device侧下发的h2d时延;它的性能更好,因此在实际生产中被大量使用。

那我们昇腾都有哪些图模式呢?

当前vllm-ascend支持aclgraphtorchair两种图模式。

  • ACLGraph支持Piecewise分图策略,可将Attention模块单独成图,其余部分组成另一子图,灵活性较高。
  • Torchair目前主要支持整图下沉,适用于结构较为固定的场景。

从应用层面来讲,两者的主要区别是前者支持piecewise的分图方式,比如说把attention模块单独成一个子图,attention以外的运算组成另外一个图;而后者只支持​整图下沉​,没有前者灵活,且调试困难。从易用性来看,aclgraph和cudagraph较像,是昇腾软件未来的主要演进路径。

torchair中的reduce-overhead模式可以达到和aclgraph相似的效果,但是reduce-overhead这种方式不支持控制流的捕获,而vllm-Ascend中当前attention层存在大量的分支,依赖控制流判断;这也是为啥不用torch.compile的reduce-overhead去做图编译的原因。否则可能对每一个分支都做一次图编译,消耗太多内存。

1.3 中间表示(IR)

IR(Intermediate Representation,中间表示) 是介于 “高层代码” 和 “底层机器码” 之间的抽象表示形式,用于简化代码优化、跨平台适配和多语言支持。 作为 “翻译中间站”:将高层代码(如 Python、C++)转换为 IR,再将 IR 转换为底层机器码(如 x86、CUDA 指令),可以解决 “一种语言适配多种硬件” 的问题。

IR 结构规整、易于分析,编译器可通过 IR 进行全局优化(如消除冗余计算、算子融合、内存复用)。​简单说:你能在 Python 层面做好 “你看得见的冗余”,但编译器基于 IR 能做好 “你看不见或管不过来的冗余​。同一份 IR 可被不同硬件的后端编译器处理,实现 “一次编写,多端运行”,IR不仅能被cuda利用转换成cudagraph来组图,同样的方式也能被我们的晟腾利用起来,转换成aclgraph。

2. 模型权重下载

使用modelscope下载Qwen3-8B模型权重至本地目录:

--local_dir 设置成自己需要的路径

modelscope download --model Qwen/Qwen3-8B --local_dir /home/Qwen3-8B

3. 拉取镜像

根据硬件型号拉取对应版本的vLLM-Ascend镜像,例如A3设备可使用:

docker pull quay.io/ascend/vllm-ascend:v0.10.0rc1-a3-openeuler
export IMAGE=quay.io/ascend/vllm-ascend:v0.10.0rc1-a3-openeuler
​

4.启动容器

使用以下命令启动容器,挂载所需设备与目录:

docker run --rm \
--name vllm-ascend \
--device /dev/davinci0 \
--device /dev/davinci1 \
--device /dev/davinci2 \
--device /dev/davinci3 \
--device /dev/davinci_manager \
--device /dev/devmm_svm \
--device /dev/hisi_hdc \
-v /home:/home \
-v /usr/local/dcmi:/usr/local/dcmi \
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
-v /usr/local/Ascend/driver/lib64/:/usr/local/Ascend/driver/lib64/ \
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
-v /etc/ascend_install.info:/etc/ascend_install.info \
-v /root/.cache:/root/.cache \
-p 8000:8000 \
-it efd4a6c8fbc8 bash

5.离线推理

5.1 离线推理脚本

为了简化对比单算子模式和图模式的profiling,设置max_num_seqs=4,使得batch_size=4max_tokens=4,使得输出token为4。

import os
import time
from vllm import LLM, SamplingParams

# enable torch profiler, can also be set on cmd line
os.environ["VLLM_TORCH_PROFILER_DIR"] = "./vllm_profile"

prompts = [
    "Hello, my name is ",
    "The president of the United States is",
    "The capital of France is",
    "The future of AI is",
]

# Create a sampling params object.
sampling_params = SamplingParams(temperature=0.8, max_tokens=4, top_p=0.95)
# Create an LLM. set batch_size = 4
#eager
llm = LLM(model="/home/Qwen3-8B",max_num_seqs=1,enforce_eager=True)
#aclgraph
#llm = LLM(model="/home/Qwen3-8B",max_num_seqs=1)
# 开启性能采集
llm.start_profile()
# Generate texts from the prompts.
outputs = llm.generate(prompts, sampling_params)
# 结束性能采集
llm.stop_profile()

for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")

time.sleep(10)

5.2 离线推理profiling采集方法

  • 通过环境变量VLLM_TORCH_PROFILER_DIR来决定是否启用Profiling采集能力。
    vllm serve /home/Qwen3-8B --max_model_len 26240 --enforce-eager​
    
  1. 单算子模式profiling采集就按上述离线推理脚本来采集。
  2. 当前图模式caputure阶段在LLM实例初始化环节,而start_profile在初始化之后,所以采集不到capture的过程,无下发连线与算子shape等信息。临时解决方案:手动修改vllm,/vllm-workspace/vllm/vllm/v1/engine/core.py第78行,增加红框部分代码。同时无需再次调用llm.start_profile()接口,把脚本中的llm.start_profile()注释掉即可。

在这里插入图片描述

6. ACLGraph流程解析

首先我们借助上文中图模式下离线推理得到的profiling全貌,图中选中区域代表了aclgraph的成图过程,大致可以分为三个阶段:1)前端捕获 2)图拆分 3) 动态 Graph捕获与重放 。下面对三个阶段进行详细解读。

在这里插入图片描述

  • 前端捕获​:利用 torch.compile 及其集成的 Dynamo 技术,将模型的 Python 代码追踪并转换为 FX Graph 格式,它是原始的pytorch代码转化为中间表示(Intermediate Representation)后的一种数据结构。举个简单的例子:
    假设我们定义好的model如下:

    class MyModule(torch.nn.Module):
        def __init__(self):
            super().__init__()
            self.param = torch.nn.Parameter(torch.rand(3, 4))
            self.linear = torch.nn.Linear(4, 5)
    
        def forward(self, x):
            return torch.topk(torch.sum(
                self.linear(x + self.linear.weight).relu(), dim=-1), 3)

    转化为IR后,以表格的形势呈现出来的IR Graph数据结构如下所示。它由一系列Node(表格中每一行代表一个Node)组成,每个Node代表一个调用点(或其他语法结构)。Node列表共同构成了一个有效的python函数。

    | opcode | name | target | args | kwargs |
    | ---------------- | ---------------- | --------------------------- | --------------------- | --------------- |
    | placeholder | x | x | () | {} |
    | get_attr | linear_weight | linear.weight | () | {} |
    | call_function | add_1 | | (x, linear_weight) | {} |
    | call_module | linear_1 | linear | (add_1,) | {} |
    | call_method | relu_1 | relu | (linear_1,) | {} |
    | call_function | sum_1 | | (relu_1,) | {‘dim’: -1} |
    | call_function | topk_1 | | (sum_1, 3) | {} |
    | output | output | output | (topk_1,) | {} |

  • ​**图拆分 (****split_graph)**​: 在 自定义的后端VllmBackend 内部,调用 split_graph 函数。此函数依据预定义的切分点(如 unified_ascend_attention_with_output),将单一的、庞大的计算图(FX IR)分解为一个主图和多个独立的子图模块 (submodule)。
    由于当前attention模块选型分支较多,如果对每一个分支都进行成图将消耗大量的内存资源,严重影响性能,甚至出现OOM情况,所以这里利用piecewise的方式将attention前后进行拆分,将每一个attention后的操作到下一个attention前的操作组成一个图,attention模块单独成图。

  • 动态 Graph捕获与重放​: 对self.aclgraph_capture_sizes中的每个形状进行捕获,并将其逐一编译生成优化后的字节码。当真实进行推理时,就可以根据当前输入的张量形状,选择已经编译过的子图进行重放下发,实现高性能推理。要注意的是,self.aclgraph_capture_sizes会按照默认规则进行生成,如果追求极致性能,建议将self.aclgraph_capture_sizes手动修改为想要测试的batch_size列表,否则可能会按照默认值向上填充,造车不必要的资源浪费。

    7. aclgraph在vllm-ascend中的代码参考(可选)

  • 分图逻辑:vllm/vllm/compilation/backends.py 第548–551行
  • 分图点定义:vllm-ascend/vllm_ascend/platform.py 第166行
  • 动态捕获与重放:vllm/vllm/compilation/backends.py 第333行;vllm-ascend/vvl_ascend/compilation/piecewise_backend.py 第177–201行

8. ACLGraph的Profiling特征

开启aclgraph后,device侧会占据大量的stream,总体看上去像是猫爪一样。每一条流上的小竖线代表一张子图。batch_size(num_token)越大,占据的流数字越小,num_token越小,占据的流数字越大。
在这里插入图片描述

上下两条竖线之间的横轴空隙在主流上由attention的计算填充。

在这里插入图片描述

ACLGraph使用与分析指南

9.1 如何判断当前eager模式下有没有算子下发瓶颈?或者说如何判断开启aclgraph后能否带来性能收益?

如图所示,host侧Thread 73456中每个step的第一个AscendCL@PagedAttentionOperation::Execute算子开始到Tread73366中AscendCL@aclrtSynchronizeStream结束的下发时间明显小于device侧的执行时间;并且Free占比较小,device侧空泡较少。没有明显的host bound,这种情况下即使开启aclgraph也不会带来收益。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这里插入图片描述

9.2 图模式相对于单算子模式的优点:

1.减少host下发带来的空泡

eager模式下的host侧下发间隔
在这里插入图片描述

图模式下的算子下发间隔

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

9.3 aclgraph中piecewise体现在哪里?

如下图所示,主流(stream2)上还保留着attention的单独计算。attention计算完毕后,stream43开始执行当前attention到下一个attention之前的计算。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

10. 附录

10.1 单算子服务化拉起

vllm serve /home/Qwen3-8B --max_model_len 26240 --enforce-eager

10.2 对话测试

注意 model字段要和server的模型路径保持一致!端口号和容器服务化端口一致都是8000

curl http://localhost:8000/v1/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "/home/Qwen3-8B",
        "prompt": "The future of AI is",
        "max_tokens": 7,
        "temperature": 0
    }'

10.3 开启profiling

通过环境变量VLLM_TORCH_PROFILER_DIR来决定是否启用Profiling采集能力。

export VLLM_TORCH_PROFILER_DIR="./vllm_profile"

10.4 benchmark测试

通过benchamark向服务发送固定长度请求,并采集profiling。

python benchmarks/benchmark_serving.py \
     --backend vllm \
     --model "/home/Qwen3-8B" \
     --port 8000 \
     --dataset-name "random" --random-input-len 128 --random-output-len 40 \
     --random-range-ratio 0.8 \
     --ignore-eos \
     --profile \
     --max-concurrency "4" \
     --request-rate "inf" \
     --num-prompts 5120 \
     --percentile-metrics "ttft,tpot,itl,e2el"

10.5 ACLGraph图模式服务化拉起

vllm serve /home/Qwen3-8B --max_model_len 26240


通过上述实践,开发者可结合具体场景灵活选用图模式配置,有效提升推理性能。建议在批量请求或高并发场景下优先启用ACLGraph以优化端到端延迟与吞吐。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐