mhc_post 算子代码级正确性证明:从 mHC 论文公式到 PyTorch 参考实现与 AscendC NPU Kernel 的全链路推导
2026/9/18 23:16:37 网站建设 项目流程

mhc_post 算子代码级正确性证明:从 mHC 论文公式到 PyTorch 参考实现与 AscendC NPU Kernel 的全链路推导

【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer

mhc_post 是 CANN ops-transformer 仓库中 mHC(Manifold-Constrained Hyper-Connections)注意力模型在昇腾 NPU 上的关键后处理算子,负责将分支模块(Attention / FFN)的输出按H_post权重分发到多个流(streams)。本文以 v1 原型仓库 中的代码级正确性证明文档为核心,逐层还原「论文公式 → PyTorch 参考实现 → CPU 参考实现 → NPU Kernel 实现 → 数学等价性证明」的完整推导链路,并结合 Kernel 源码与测试用例,说明每一层实现如何相互印证,最终证明 NPU 实现与论文公式完全等价。

1. 背景:mhc_post 在 mHC 架构中的位置

mHC 论文(arXiv:2512.24880,DeepSeek 2024.12.31 发布)提出了流形约束超连接(Manifold-Constrained Hyper-Connections)架构。其核心迭代公式(Equation 3)为:

x_{l+1} = H_l^{res} · x_l + H_l^{post}^T · F(H_l^{pre} · x_l, W_l)

各符号含义如下:

符号含义说明
x_l第 l 层输入特征shape [batch, n×C],n 个 streams,每个 C 维
H_l^{pre}输入映射shape [1, n],用于将 n 个 streams 聚合为 1 个
H_l^{post}输出映射shape [1, n],用于将 1 个分发到 n 个 streams
F(...)分支模块如 Attention、FFN 的输出
H_l^{res}残差映射shape [n, n]

从仓库中的 验证脚本 可以看到,mhc_post 算子对应的正是公式中的H_l^{post}^T · F(...)部分:将分支模块的输出F(...)与经过 softmax 归一化的H_post向量做外积广播,再分发到多个 streams。

2. 论文公式的精确定义

mhc_post 的论文公式可写成如下一行:

mhc_post: output[b*s+i, seq, d] = branch_output[b, seq, d] × h_post[i]

其中输入输出形状为:

  • branch_output: [batch, seq_len, dim] —— 分支模块(如 Attention/FFN)的输出
  • h_post: [num_streams] —— 分发权重,即H_l^{post},经 softmax 归一化
  • output: [batch * num_streams, seq_len, dim] —— 每个 (batch, stream) 组合一份输出

公式要义是:对每一个 batch 样本,将其同一份分支输出复制到 num_streams 份,每份乘以对应 stream 的权重h_post[s],最终按batch * num_streams + stream的次序把 batch 与 stream 两个维度合并。这个算子不涉及任何累加或规约,只有「广播乘法 + 维度重排」两个动作,因此正确性证明可以做到逐元素级别。

3. PyTorch 参考实现(tokenbender/mHC)

仓库文档给出的 PyTorch 参考实现来自hyper_connections_mhc.pydepth_connection()函数,它用两步张量操作完成上述公式:

def depth_connection(self, branch_output, residuals, *, beta): # beta 就是 h_post,shape [num_streams] # Step 1: einsum 广播乘法 # "b ... d, s -> b ... s d" 的含义: # - 输入1 branch_output: [batch, seq, dim] # - 输入2 beta: [streams] # - 输出: [batch, seq, streams, dim] # - 计算: out[b,seq,s,d] = branch_output[b,seq,d] * beta[s] output = einsum(branch_output, beta, "b ... d, s -> b ... s d") # Step 2: reshape 合并 batch 和 stream 维度 # "b ... s d -> (b s) ... d" 的含义: # - 输入: [batch, seq, streams, dim] # - 输出: [batch*streams, seq, dim] # - 计算: out[b*s+i, seq, d] = in[b, seq, i, d] output = rearrange(output, "b ... s d -> (b s) ... d") return output

这两步与论文公式的对应关系非常清晰:

  • einsum"b ... d, s -> b ... s d"在 batch 与 stream 之间没有求和(einsum 输出中同时保留 b 与 s 下标),本质是外积广播output[b,seq,s,d] = branch_output[b,seq,d] × beta[s]
  • rearrange"b ... s d -> (b s) ... d"[batch, seq, streams, dim]按「先 batch 后 stream」的次序压平为[batch*streams, seq, dim],即output[b*streams + s, seq, d] = input[b, seq, s, d]

两步组合的结果正是论文公式。仓库中的 Python 验证脚本 用np.einsum('bsd,n->bsnd', ...)加 transpose/reshape 复现了该参考实现,并断言与参考实现逐元素最大差异小于 1e-6。

4. CPU 参考实现(测试代码中的mhc_post_cpu

在编写 NPU Kernel 之前,原型先提供了一个可直接对照的 CPU 参考实现mhc_post_cpu(),完整代码位于 test_mhc_post.cpp:

void mhc_post_cpu( const float* branch_output, // [batch, seq_len, dim] const float* h_post, // [num_streams] float* output, // [batch * num_streams, seq_len, dim] int64_t batch, int64_t seq_len, int64_t dim, int64_t num_streams ) { // 遍历每个batch for (int64_t b = 0; b < batch; ++b) { // 遍历每个stream for (int64_t s = 0; s < num_streams; ++s) { // 获取当前stream的权重 float weight = h_post[s]; // 计算输出的batch索引: b*num_streams + s // 这对应 rearrange "b s -> (b s)" 的语义 int64_t out_batch_idx = b * num_streams + s; // 遍历每个序列位置和维度 for (int64_t seq = 0; seq < seq_len; ++seq) { for (int64_t d = 0; d < dim; ++d) { // 输入索引: branch_output[b, seq, d] int64_t in_idx = b * seq_len * dim + seq * dim + d; // 输出索引: output[b*s+i, seq, d] int64_t out_idx = out_batch_idx * seq_len * dim + seq * dim + d; // 核心计算: 乘法 // 对应论文公式: output[b*s+i] = branch_output[b] * h_post[i] output[out_idx] = branch_output[in_idx] * weight; } } } } }

这份实现的价值在于:它把论文公式中张量层面的「广播乘法 + 维度合并」显式翻译成了标量循环,每一行代码都可以与公式逐项对应:

论文公式CPU 代码说明
output[b*s+i, ...]out_batch_idx = b * num_streams + sbatch 和 stream 索引合并方式一致
branch_output[b, ...]in_idx = b * seq_len * dim + ...输入只依赖 batch 索引
h_post[i]weight = h_post[s]权重只依赖 stream 索引
*output[...] = branch_output[...] * weight简单乘法

CPU 实现完全按照行主序(row-major)线性索引展开,seq * dim + d对应[seq, d]平面的连续排布,b * seq_len * dim对应 batch 维的步长,out_batch_idx * seq_len * dim对应合并后 batch 维的步长,与 PyTorch 的[batch*streams, seq, dim]布局一致。

5. NPU Kernel 实现(AscendCMhcPostKernel

NPU 端使用 AscendC 编程模型实现,完整代码位于 kernel/mhc_post_kernel.cpp。核心思路是把每个 (batch, stream) 组合映射为一个 block,从而天然并行地覆盖batch * num_streams个输出切片。

5.1 Init:block 到 (batch, stream) 的索引解析

__aicore__ inline void Init( GM_ADDR branch_output, GM_ADDR h_post, GM_ADDR output, int64_t batch, int64_t seq_len, int64_t dim, int64_t num_streams ) { this->batch = batch; this->seq_len = seq_len; this->dim = dim; this->num_streams = num_streams; int64_t block_idx = GetBlockIdx(); this->batch_idx = block_idx / num_streams; // 解析 batch 索引 this->stream_idx = block_idx % num_streams; // 解析 stream 索引 this->batch_elements = seq_len * dim; // 每个 batch 的元素数 // 输入偏移: 只依赖 batch_idx,对应 branch_output[b, ...] int64_t input_offset = this->batch_idx * this->batch_elements; this->gm_branch_output.SetGlobalBuffer( reinterpret_cast<__gm__ float*>(branch_output) + input_offset, this->batch_elements ); this->gm_h_post.SetGlobalBuffer( reinterpret_cast<__gm__ float*>(h_post), num_streams ); // 输出偏移: 依赖 batch_idx 和 stream_idx,对应 output[b*s+i, ...] int64_t output_offset = (this->batch_idx * num_streams + this->stream_idx) * this->batch_elements; this->gm_output.SetGlobalBuffer( reinterpret_cast<__gm__ float*>(output) + output_offset, this->batch_elements ); this->tile_length = 256; // 固定 tile 大小 this->tile_num = (this->batch_elements + this->tile_length - 1) / this->tile_length; pipe.InitBuffer(inQueue, BUFFER_NUM, this->tile_length * sizeof(float)); pipe.InitBuffer(outQueue, BUFFER_NUM, this->tile_length * sizeof(float)); }

其中GetBlockIdx()返回当前 block 编号。由于启动的 block 总数等于batch * num_streamsblock_idx / num_streamsblock_idx % num_streams恰好完成论文公式中(b s) -> b, s的反向解析,对应output[b*s+i]中索引的拆解。

5.2 LoadWeight:加载当前 stream 权重

__aicore__ inline void LoadWeight() { // 加载当前stream的权重,对应论文公式中 h_post[i] this->weight_value = this->gm_h_post.GetValue(this->stream_idx); }

h_post只有num_streams个标量,每个 block 只需按自己的stream_idx读取一个权重即可。

5.3 Compute:向量乘标量

__aicore__ inline void Compute(int64_t length) { LocalTensor<float> inLocal = inQueue.DeQue<float>(); LocalTensor<float> outLocal = outQueue.AllocTensor<float>(); // 核心计算: 向量乘标量,对应论文公式: output = branch_output * h_post[i] // Muls 是 AscendC 的向量乘标量指令 Muls(outLocal, inLocal, this->weight_value, length); outQueue.EnQue(outLocal); inQueue.FreeTensor(inLocal); }

NPU 端用一条Muls(向量乘标量)指令完成整个[seq_len, dim]平面(或其中一段 tile)的逐元素乘法,与 CPU 参考实现中的标量乘法循环语义一致。由于 mhc_post 只需要乘法、不涉及累加/除法等复杂运算,FP32 下理论上可以达到与 CPU 参考实现 bit-exact 的精度(见第 7 节验证)。

5.4 Process:tiling 流水

__aicore__ inline void Process() { this->weight_value = this->gm_h_post.GetValue(this->stream_idx); for (int64_t tile_idx = 0; tile_idx < this->tile_num; ++tile_idx) { int64_t offset = tile_idx * this->tile_length; int64_t length = this->tile_length; if (offset + length > this->batch_elements) { length = this->batch_elements - offset; // 尾块裁剪 } CopyIn(offset, length); Compute(length); CopyOut(offset, length); } }

batch_elements = seq_len * dim可能很大,Kernel 用固定的tile_length = 256对每个 (batch, stream) 的[seq_len, dim]平面做分块,配合BUFFER_NUM = 2的双缓冲队列(inQueue/outQueue)实现「搬入-计算-搬出」的流水。尾块(不整除 256)通过length = batch_elements - offset精确裁剪。

5.5 CopyIn / CopyOut:对齐与非对齐路径

constexpr int32_t ALIGN_ELEM = 8; // fp32 对齐元素数 __aicore__ inline void CopyIn(int64_t offset, int64_t length) { LocalTensor<float> inLocal = inQueue.AllocTensor<float>(); if (length % ALIGN_ELEM == 0) { DataCopy(inLocal, gm_branch_output[offset], length); } else { DataCopyExtParams params{1, static_cast<uint32_t>(length * sizeof(float)), 0, 0, 0}; DataCopyPadExtParams<float> pad{false, 0, 0, 0.0f}; DataCopyPad(inLocal, gm_branch_output[offset], params, pad); } inQueue.EnQue(inLocal); } __aicore__ inline void CopyOut(int64_t offset, int64_t length) { LocalTensor<float> outLocal = outQueue.DeQue<float>(); if (length % ALIGN_ELEM == 0) { DataCopy(gm_output[offset], outLocal, length); } else { DataCopyExtParams params{1, static_cast<uint32_t>(length * sizeof(float)), 0, 0, 0}; DataCopyPad(gm_output[offset], outLocal, params); } outQueue.FreeTensor(outLocal); }

length是 8 的倍数(fp32 下的 32 字节对齐)时走常规DataCopy;否则走DataCopyPad/DataCopyExtParams的带填充路径,保证任意seq_len * dim(包括非对齐的 dim,如dim=7)都能正确处理。这与仓库中 test_unaligned.cpp 针对dim=7非对齐场景的测试一一对应。

5.6 Kernel 入口与 do 接口

extern "C" __global__ __aicore__ void mhc_post_kernel( GM_ADDR branch_output, GM_ADDR h_post, GM_ADDR output, int64_t batch, int64_t seq_len, int64_t dim, int64_t num_streams ) { MhcPostKernel op; op.Init(branch_output, h_post, output, batch, seq_len, dim, num_streams); op.Process(); } extern "C" void mhc_post_do( uint32_t blockDim, void* stream, uint8_t* branch_output, uint8_t* h_post, uint8_t* output, int64_t batch, int64_t seq_len, int64_t dim, int64_t num_streams ) { mhc_post_kernel<<<blockDim, nullptr, stream>>>( branch_output, h_post, output, batch, seq_len, dim, num_streams ); }

mhc_post_kernel是 AscendC 的__global__ __aicore__入口,mhc_post_do是对外的可调用封装:调用方传入blockDim(即batch * num_streams)与 ACL stream,通过<<<blockDim, nullptr, stream>>>启动核函数。

5.7 NPU 实现与论文公式的逐项对照

论文公式NPU 代码说明
遍历所有 (b,s) 组合block_idx ∈ [0, batch*num_streams)并行处理每个组合
b = idx / sbatch_idx = block_idx / num_streams解析 batch 索引
i = idx % sstream_idx = block_idx % num_streams解析 stream 索引
branch_output[b]input_offset = batch_idx * elements输入偏移计算
output[b*s+i]output_offset = (b*s+i) * elements输出偏移计算
h_post[i]weight = gm_h_post.GetValue(stream_idx)加载权重
output = input * weightMuls(out, in, weight, len)向量乘标量

6. 数学等价性证明

论文公式、CPU 实现与 NPU 实现三者之间的等价性可以形式化如下。

给定:

  • branch_output: shape [B, S, D]
  • h_post: shape [N]
  • output: shape [B*N, S, D]

论文公式:

output[b*N + n, s, d] = branch_output[b, s, d] × h_post[n] 其中 b ∈ [0,B), n ∈ [0,N), s ∈ [0,S), d ∈ [0,D)

CPU 实现等价性:

for b in [0, B): for n in [0, N): out_idx = b * N + n // ← 对应 output[b*N + n, ...] weight = h_post[n] // ← 对应 h_post[n] for s in [0, S): for d in [0, D): output[out_idx, s, d] = branch_output[b, s, d] * weight // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // 对应 branch_output[b, s, d] × h_post[n]

NPU 实现等价性:

// 并行化: 每个 block 处理一个 (b, n) 组合 block_idx ∈ [0, B*N): b = block_idx / N // ← 解析 batch 索引 n = block_idx % N // ← 解析 stream 索引 weight = h_post[n] // ← 对应 h_post[n] // 向量化: 处理整个 [S, D] 平面 output[block_idx, :, :] = branch_output[b, :, :] * weight // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // 对应 branch_output[b, s, d] × h_post[n]

由于block_idx = b * N + n,所以output[block_idx]等价于output[b*N + n];因此NPU 实现与论文公式完全等价

7. 测试用例与精度验证

正确性证明文档之外,原型目录下的测试代码从多个维度对该等价性做了工程验证:

7.1 基础正确性测试

test_mhc_post.cpp 是核心对照测试:以batch=2, seq_len=4, dim=64, num_streams=4为例,h_post先按[1,2,3,4]初始化再经softmax归一化,branch_output(i % 100) / 100.0f填充;先跑 CPU 参考实现得到期望输出,再通过 ACL 接口分配设备内存、以blockDim = batch * num_streams启动mhc_post_do,最后以1e-4容差逐元素比对 CPU 与 NPU 结果。该文件同时给出了softmax的 C++ 实现(先减最大值再指数归一化),与h_post需要满足 softmax 约束的论文设定一致。

7.2 多 shape 综合测试

test_multi_shape.cpp 覆盖 10 组形状:基础测试、单 batch、大 batch、长序列、大 dim、多 streams、典型配置、dim=32边界、streams=2边界以及大规模(4×64×256×4),全部使用随机正态分布初始化并经 softmax,判定条件为最大绝对误差< 1e-4且最大相对误差< 1e-3

7.3 非对齐场景测试

test_unaligned.cpp 专门针对dim=7(元素数与 8 不对齐)验证 Kernel 的DataCopyPad分支,容差1e-5

7.4 精度详细测试

test_precision.cpp 用 5 组不同输入尺度(小数值 0.01、标准正态、大数值 100、混合正负、大规模)统计最大绝对/相对误差与完全一致点数,判定阈值max_abs < 1e-5

7.5 FP32 精度极限分析

test_fp32_limit.cpp 对比了 FP32 理论极限(machine epsilon ≈ 1.19e-7,约 7 位十进制有效数字)与 6 个数量级范围(从 [1e-30, 1e-20] 到 [1e20, 1e30] 及混合正负)的实际测试结果,结论是mhc_post 的 Muls 乘法在所有测试范围内与 CPU 参考实现达到 bit-exact——因为昇腾 NPU 的 FP32 乘法遵循 IEEE 754 标准,而单次乘法不引入累加误差。

7.6 Python 侧公式验证

verify_mhc_post.py 从公式层面对三类结论做交叉验证:

  • verify_formula():小规模(batch=2, seq_len=2, dim=4, num_streams=3)手工逐点验证output[out_idx,0,0] = branch_output[b,0,0] * h_post[s]
  • compare_with_einsum():将参考实现与np.einsum('bsd,n->bsnd')+ transpose/reshape 对比,断言最大差异< 1e-6,等价于 PyTorch 的 einsum + rearrange 参考路径;
  • generate_test_vectors():按与 C++ 测试完全一致的初始化方式((i % 100)/100h_post=[1,2,3,4]softmax 归一化)生成二进制测试向量,供 NPU 端对照。

其余两个脚本 test_comprehensive.py 与 test_formula_proof.py 进一步补充了综合场景与公式级证明。test/下还有 test_mhc_post_detail.cpp 提供更细粒度的逐点输出检查。

8. 构建与运行环境说明

原型目录提供 CMakeLists.txt 作为构建入口,其关键配置如下:

  • CMAKE_CXX_STANDARD 17,项目名mhc_post_v1
  • 通过include(${ASCEND_PATH}/compiler/tikcpp/ascendc_kernel_cmake/ascendc.cmake)引入 AscendC 编译框架(ASCEND_PATH默认指向/usr/local/Ascend/ascend-toolkit/latest);
  • SOC_VERSION默认为ascend910b2,可通过 CMake 缓存变量覆盖;
  • ascendc_library(mhc_post_kernel STATIC kernel/mhc_post_kernel.cpp)将 Kernel 编译为静态库;
  • 测试程序通过宏add_test_exe生成,链接mhc_post_kernel ascendcl runtime并引入${ASCEND_PATH}/include

测试程序运行依赖 ACL(AscendCL)运行时:需要先aclInitaclrtSetDevice(0)aclrtCreateStream,通过aclrtMalloc/aclrtMemcpy完成主机与设备间的数据搬运,最后aclrtSynchronizeStream同步后再把设备端结果拷回主机比对。v1 原型为仅 fp32、固定 tile=256 的初始原型,如 README 所注,已归档仅供后续版本参考。

9. 小结:四层实现如何相互印证

mhc_post 的代码级正确性证明,本质是一条「从数学到机器指令」的等值链:

  1. 论文公式定义语义:output[b*N + n, s, d] = branch_output[b, s, d] × h_post[n]
  2. PyTorch 参考实现(einsum + rearrange)把公式翻译为张量操作,两步分别对应「外积广播」与「维度合并」;
  3. CPU 参考实现mhc_post_cpu)把张量操作再翻译为逐元素标量循环,成为可逐项对照的黄金基准;
  4. NPU Kernel 实现MhcPostKernel)通过block_idx / num_streamsblock_idx % num_streams复现索引解析,用Muls完成乘法,用 tiling + 双缓冲 + 对齐/填充两条 DMA 路径覆盖任意形状,最终由block_idx = b * N + n的构造保证与论文公式逐元素等价。

这一「公式 → 框架参考实现 → CPU 基准 → 硬件 Kernel → 数学证明 + 多维测试」的方法论,也为后续在昇腾 NPU 上迁移其他 mHC 算子(如mhc_premhc_sinkhorn等,见 mhc 目录)提供了可复制的验证范式。

【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询