训练营简介
2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机,平板、开发板等大奖。

报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro

前言

在深度学习中,"Bandwidth is King"(带宽为王)。 对于 Add, Mul, Relu 这种计算量很小的算子,瓶颈完全在内存带宽上。将它们融合(Fusion)是提升性能的唯一出路。

但是,手写融合算子非常痛苦。你需要手动申请中间 Tensor,手动管理队列,手动调用指令。一旦公式变了,所有代码都要重写。

在 PyTorch 2.0 中,torch.compile 利用 Triton 实现了自动融合。而在 Ascend C 中,我们可以利用 C++ 强大的 模板元编程 能力,实现类似的效果: 写下一行公式,编译器自动生成最优的流水线代码。

本期文章,我们将挑战用 C++ 语言特性构建一个 Lazy Evaluation(惰性求值) 引擎。

一、 核心图解:像拼积木一样融合

传统执行是“做完一步走一步”,表达式模板是“先把积木搭好,最后一把推倒”。

二、 技术原理:表达式模板 (Expression Templates)

这是 C++ 高性能库(如 Eigen, XTensor)的核心技术。 核心思想是:运算符重载不进行计算,只构建语法树。

当我们写 C = A + B 时:

  1. 普通 C++:立即调用 Add 函数,计算结果存入临时变量,再赋给 C。

  2. 表达式模板:返回一个轻量级的对象 Sum<Vector, Vector>,它只记录了“我要对 A 和 B 做加法”这个信息,不进行任何计算

只有当我们把这个对象赋值给 C 时,计算才会真正发生(Lazy Evaluation),此时编译器会将多层嵌套的模板展开,生成一个高效的循环。

三、 实战:构建 Ascend C 融合 DSL

我们要实现的目标是,在 Kernel 的 Compute 函数中可以这样写:

// 像写数学公式一样,无需手动调用 Add/Mul/Relu 指令
// 编译器自动将其融合成一个流水线,没有中间内存读写
auto result = (inputA + inputB) * 0.5f;
Evaluate(output, result, tileLength);

3.1 定义基础包装类 (Wrapper)

首先,我们需要把 LocalTensor 包装一下,让它支持运算符重载。

template <typename T>
struct TensorWrapper {
    LocalTensor<T>& tensor;
    
    // 访问接口:返回第 i 个元素的值(用于标量模拟)
    // 或者返回 LocalTensor 本身(用于向量指令)
    __aicore__ inline LocalTensor<T>& Get() const { return tensor; }
};

3.2 定义操作节点 (Nodes)

定义一个代表“加法操作”的模板类。

template <typename LHS, typename RHS>
struct BinaryAdd {
    const LHS& lhs;
    const RHS& rhs;

    // 核心:在 Evaluate 时才调用指令
    template <typename DstT>
    __aicore__ inline void Eval(LocalTensor<DstT>& dst, uint32_t len) const {
        // 这里为了简化,假设 LHS 和 RHS 都是 TensorWrapper
        // 实际工程中需要处理递归展开,或者使用更底层的标量循环
        // Ascend C 推荐使用 Vector 指令,所以我们这里做一层"指令级"融合
        
        // 简单融合策略:
        // 如果是 (A + B) + C
        // 1. 先算 A+B -> Tmp
        // 2. 再算 Tmp+C -> Dst
        // 这需要动态申请临时 Tensor,比较复杂。
        
        // 更彻底的融合策略:
        // 利用 Ascend C 的三目指令 (Mad) 或双目指令
        // 但最通用的方式是:**退化为标量计算,利用编译器自动向量化**
        // 或者手动管理临时 Buffer 池
    }
};

 修正视点: 在 AI Core 上完全实现 Eigen 那样的标量级自动向量化比较困难。 更务实的做法是:利用 Pipe 里的临时 Buffer 作为寄存器

让我们实现一个简化的 "链式调用" 版本。

3.3 简易版 DSL 实现

我们可以重载 operator+,让它返回一个不仅包含数据,还包含操作链的对象。

// 定义一个表达式对象
template <typename OpType, typename L, typename R>
struct Expr {
    const L& l;
    const R& r;
    
    // 执行函数
    __aicore__ inline void Exec(LocalTensor<half>& dst, uint32_t len) {
        // 申请临时空间
        LocalTensor<half> tmp = ...; 
        l.Exec(tmp, len); // 递归计算左边
        r.Exec(dst, len); // 递归计算右边
        OpType::Run(dst, tmp, dst, len); // 执行当前操作
    }
};

// 运算符重载
template <typename L, typename R>
__aicore__ inline auto operator+(const L& l, const R& r) {
    return Expr<AddOp, L, R>{l, r};
}

3.4 终极形态:利用 Ascend C 的 Muls, Adds

对于形如 Y = X * a + b 的线性变换,Ascend C 提供了极高效的指令。 我们可以特化模板,当检测到 Scalar 参与运算时,自动切换到 MulsAdds 指令。

// 场景:Tensor * Scalar
template <typename T>
__aicore__ inline void Evaluate(LocalTensor<T>& dst, 
                                const MulExpr<TensorWrapper<T>, ScalarWrapper<T>>& expr, 
                                uint32_t len) {
    // 直接调用 Muls 指令,无需临时 Tensor
    Muls(dst, expr.lhs.Get(), expr.rhs.Value(), len);
}

四、 总结

虽然在 AI Core 上实现完整的 Expression Templates 比较复杂(受限于 C++ 标准库支持和硬件特性),但这种**“元编程思维”**非常有价值。

  1. 抽象:将数学逻辑与底层指令解耦。

  2. 融合:通过模板展开,让编译器看到完整的计算图,从而有机会消除中间内存写回。

  3. 效率:开发效率提升,写算子像写 Python 一样简单。

这不仅是代码技巧,更是编译器设计的雏形。

Logo

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

更多推荐