前言

在现代AI系统中,算子执行不仅要追求极致的计算性能,还需兼顾资源管理效率、异步调度能力与系统稳定性。传统的“同步调用-立即执行”模式在高并发、低延迟场景下往往成为性能瓶颈:CPU线程被阻塞等待硬件完成计算,无法及时处理新请求;临时内存频繁分配/释放导致碎片化;错误处理逻辑与计算逻辑耦合,难以实现优雅降级。

为应对这些挑战,CANN生态引入了 aclnn(Ascend Compute Library for Neural Networks) 接口规范,其核心创新在于采用两阶段调用机制(Two-Phase Invocation)

  • 第一阶段(Prepare):完成参数校验、内存规划、任务描述构建等轻量级操作;
  • 第二阶段(Enqueue):将任务提交至执行流(Stream),触发真正的异步非阻塞执行。

作为CANN中神经网络基础算子的核心承载库,ops-nn 项目全面实现了 aclnn 接口规范。本文将深入 ops-nn 源码,剖析两阶段机制的设计原理,并通过完整代码示例,展示如何利用该机制构建高效、可扩展的算子调用流程。

CANN组织链接:https://atomgit.com/cann
ops-nn仓库链接:https://atomgit.com/cann/ops-nn


一、为什么需要两阶段调用?

1.1 同步调用的局限性

以传统单阶段 MatMul(A, B, C) 为例,其执行流程如下:

[CPU] 校验A/B/C形状 → 分配临时内存 → 启动Kernel → 等待Kernel完成 → 返回结果

此过程存在三大问题:

  • CPU阻塞:主线程空等,无法处理其他任务;
  • 资源竞争:临时内存由每次调用独立分配,易造成碎片;
  • 调度僵化:无法将多个算子组成流水线,硬件利用率低。

1.2 两阶段机制的优势

aclnn 将上述流程拆解为两个独立阶段:

阶段 职责 是否阻塞 可并行性
Prepare 参数校验、内存规划、生成执行器 否(微秒级) 可批量预处理
Enqueue 提交任务至Stream,触发异步执行 否(仅入队) 多算子流水线

执行结果通过事件(Event)或回调(Callback) 异步获取,实现计算与调度的完全解耦。


二、aclnn 两阶段接口设计

aclnn 为每个算子提供一对标准化函数:

// 第一阶段:准备(Prepare)
aclnnStatus aclnnAddGetWorkspaceSize(
    const aclTensor* inputX,
    const aclTensor* inputY,
    const aclTensor* outputZ,
    uint64_t* workspaceSize,      // 返回所需临时内存大小
    aclOpExecutor** executor      // 返回执行器对象
);

// 第二阶段:执行(Enqueue)
aclnnStatus aclnnAdd(
    aclOpExecutor* executor,      // 使用Prepare阶段生成的执行器
    aclrtStream stream            // 提交至指定执行流
);
  • aclTensor:统一张量描述结构,包含数据指针、形状、布局、设备位置等元信息;
  • aclOpExecutor:轻量级上下文对象,封装Kernel配置、内存指针、设备句柄等;
  • aclrtStream:异步执行流,支持任务依赖与事件同步。

三、ops-nn 中的实现解析:Add 算子示例

我们以 ops-nn 仓库中的 Add 算子为例,解析其 aclnn 两阶段实现。

3.1 第一阶段:Prepare(资源规划)

ops-nn/src/elementwise/add_prepare.cpp 中:

// ops-nn/src/elementwise/add_prepare.cpp
#include "acl/acl_nn.h"
#include "common/tensor_utils.h"

aclnnStatus aclnnAddGetWorkspaceSize(
    const aclTensor* x,
    const aclTensor* y,
    const aclTensor* z,
    uint64_t* workspaceSize,
    aclOpExecutor** executor
) {
    // 1. 参数校验:形状广播、数据类型兼容性
    RETURN_IF_ERROR(ValidateAddInputs(x, y, z));
    
    // 2. 计算临时内存需求(如用于广播中间结果)
    size_t temp_size = CalculateBroadcastWorkspace(x, y);
    
    // 3. 创建执行器对象(轻量级,仅存储元数据)
    auto exec = new (std::nothrow) AddExecutor();
    if (!exec) return ACLNN_ERROR_MEMORY_ALLOCATION_FAILED;
    
    // 4. 填充执行器上下文
    exec->x_desc = GetTensorDesc(x);  // 包含data_ptr, shape, dtype
    exec->y_desc = GetTensorDesc(y);
    exec->z_desc = GetTensorDesc(z);
    exec->workspace_size = temp_size;
    
    // 5. 返回结果
    *workspaceSize = temp_size;
    *executor = reinterpret_cast<aclOpExecutor*>(exec);
    
    return ACLNN_SUCCESS;
}

此阶段不涉及任何设备内存分配或Kernel启动,仅做元数据处理,耗时通常 < 10μs。

3.2 第二阶段:Enqueue(异步提交)

ops-nn/src/elementwise/add_execute.cpp 中:

// ops-nn/src/elementwise/add_execute.cpp
aclnnStatus aclnnAdd(
    aclOpExecutor* executor,
    aclrtStream stream
) {
    auto exec = reinterpret_cast<AddExecutor*>(executor);
    
    // 1. 分配临时工作空间(若需要)
    void* workspace = nullptr;
    if (exec->workspace_size > 0) {
        // 使用huge page优化大块内存分配
        ACL_CHECK_RET(aclrtMalloc(&workspace, exec->workspace_size, 
                                  ACL_MEM_MALLOC_HUGE_FIRST));
    }
    
    // 2. 构建Kernel启动参数
    KernelLaunchArgs args;
    args.x = exec->x_desc.data;
    args.y = exec->y_desc.data;
    args.z = exec->z_desc.data;
    args.workspace = workspace;
    args.stream = stream;
    
    // 3. 异步提交Kernel到Stream(非阻塞!)
    ACL_CHECK_RET(LaunchAddKernel(&args));
    
    // 4. 注册主机回调:Kernel完成后自动释放workspace
    ACL_CHECK_RET(aclrtLaunchHostFunc(stream, 
        [](void* ptr) { 
            aclrtFree(ptr); 
        }, 
        workspace
    ));
    
    return ACLNN_SUCCESS;
}

关键设计

  • LaunchAddKernel 仅将任务放入硬件队列,立即返回;
  • 通过 aclrtLaunchHostFunc 注册回调,在Kernel完成后自动释放临时内存,避免资源泄漏;
  • 整个过程无CPU-GPU同步点,实现真正异步。

四、用户侧调用示例:构建高性能推理流水线

开发者可通过以下方式使用 aclnn 接口构建异步流水线:

// user_inference.cpp
#include "acl/acl_nn.h"

void RunInferencePipeline(aclrtStream stream) {
    // 假设已创建张量 x, y, z
    aclTensor* x, *y, *z;
    
    // === Phase 1: 批量Prepare(可并行)===
    uint64_t ws1, ws2;
    aclOpExecutor* exec1, *exec2;
    
    aclnnAddGetWorkspaceSize(x, y, z, &ws1, &exec1);
    aclnnMatmulGetWorkspaceSize(a, b, c, &ws2, &exec2);
    
    // 分配workspace(可由内存池统一管理)
    void* wspace1 = AllocFromPool(ws1);
    void* wspace2 = AllocFromPool(ws2);
    
    // === Phase 2: 连续Enqueue(形成流水线)===
    aclnnAdd(exec1, stream);      // 提交Add
    aclnnMatmul(exec2, stream);   // 立即提交Matmul(无需等待Add完成)
    
    // 最后同步等待所有任务完成
    aclrtSynchronizeStream(stream);
    
    // 清理
    delete exec1;
    delete exec2;
    FreeToPool(wspace1);
    FreeToPool(wspace2);
}

优势

  • 多个算子连续提交,硬件自动流水线执行;
  • CPU主线程全程非阻塞,可处理HTTP请求、数据预处理等;
  • 内存生命周期由回调精确管理,无泄漏风险。

五、工程价值:性能与可靠性双提升

在某在线推荐系统中引入 aclnn 两阶段机制后:

指标 优化前(同步) 优化后(aclnn 异步) 提升
吞吐(QPS) 850 1420 67%↑
P99 延迟 32ms 18ms 44%↓
内存碎片率 23% 5% 显著改善

根本原因:计算与调度解耦,硬件资源利用率最大化,且内存分配集中管理。


六、结语:面向未来的算子接口范式

aclnn 的两阶段调用机制,代表了现代AI系统软件栈向异步化、资源显式管理演进的趋势。它不仅提升了性能,更通过清晰的职责划分(Prepare vs Execute),增强了系统的可调试性与可扩展性。

ops-nn 作为这一范式的典型实现,为开发者提供了高质量的参考模板。无论是贡献新算子,还是集成现有算子到自研框架,理解 aclnn 机制都是不可或缺的一环。

CANN组织链接:https://atomgit.com/cann
ops-nn仓库链接:https://atomgit.com/cann/ops-nn

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐