状态(Status): Reviewing 作者(Authors): @samulenzz 创建日期(Created): 2026-07-26 更新日期(Updated): 2026-07-26 相关 Issue/PR: #227
本提案针对 DrivingSDK 仓中 GraphSoftmax 自定义算子进行 Ascend C 内核级性能优化。GraphSoftmax 是自动驾驶场景中图神经网络(GNN)的核心算子,用于对图结构中按节点分组的边特征执行 softmax 归一化。优化后在保持 FP32 精度的前提下,通过消除冗余 Global Memory 访问、引入 run 级聚合和唯一索引快速路径等手段,实现算子端到端性能提升 20% 以上。
使用场景:GraphSoftmax 广泛应用于 QCNet 等自动驾驶感知模型中,对图中每条边的 8 维特征按目标节点分组做 softmax。在 QCNet 推理和训练流程中,该算子被高频调用,其性能直接影响模型整体推理延迟。
当前痛点:
singleLoopTaskCount
不做此提案的影响:在大规模图(边数 > 10000)场景下,GraphSoftmax 将成为 QCNet 推理的性能瓶颈;随着自动驾驶模型对图结构利用的加深,该瓶颈会愈发突出。
目标:
非目标:
mx_driving.graph_softmax
src
[E, 8]
index
[0, 1500000)
GraphSoftmax 对图中的边特征 src[E, 8] 按 index[E] 分组执行数值稳定的 softmax:
src[E, 8]
index[E]
对每条边 e,其所属组 g = index[e]: max_g = max_{e' ∈ group(g)} src[e'] // 按组取最大值 exp_e = exp(src[e] - max_g) // 减最大值后取指数 sum_g = Σ_{e' ∈ group(g)} exp[e'] // 按组求和 output_e = exp_e / (sum_g + ε) // 归一化
原始实现和优化后实现都遵循三阶段流程:ScatterMax → ComputeExp + ScatterSum → ComputeOutput。核心区别在于如何执行 scatter 聚合以及如何减少 GM 访问。
原始 kernel 的主要性能瓶颈如下图所示:
flowchart TD subgraph INIT["初始化阶段"] A1[InitGlobalMemory<br/>初始化 scatterMax = -INF<br/>初始化 scatterSum = 0] end subgraph SCATTER_MAX["Phase 1: ScatterMax(分块循环)"] B1[CopyIn: src + index<br/>每次加载 ≤ 2360 条边] B2[逐 edge Atomic ScatterMax<br/>每条边 1 次 MTE3 原子写] B1 --> B2 B2 -->|下一块| B1 end subgraph SCATTER_SUM["Phase 2: ComputeExp + ScatterSum(分块循环)"] C1[CopyIn: src + index] C2["Gather scatterMax[index[e]]<br/>逐 edge 读 GM"] C3["Sub + Exp(向量计算)"] C4[逐 edge Atomic ScatterAdd<br/>每条边 1 次 MTE3 原子写] C5[CopyOut: exp 中间结果] C1 --> C2 --> C3 --> C4 --> C5 C5 -->|下一块| C1 end subgraph SOFTMAX["Phase 3: ComputeSoftmax(分块循环)"] D1[CopyIn: exp 中间结果 + index] D2["Gather scatterSum[index[e]]<br/>逐 edge 读 GM"] D3["Adds(ε) + Div(向量计算)"] D4[CopyOut: 最终结果] D1 --> D2 --> D3 --> D4 D4 -->|下一块| D1 end INIT --> SCATTER_MAX --> SCATTER_SUM --> SOFTMAX style B2 fill:#ff6b6b,color:#fff style C2 fill:#ff6b6b,color:#fff style C4 fill:#ff6b6b,color:#fff style D2 fill:#ff6b6b,color:#fff
红色节点标注了主要性能瓶颈:逐 edge 的 GM 原子操作和逐 edge 的 GM gather。
瓶颈量化:对于 E = 30000 的典型用例:
优化后的实现引入三条互斥的执行路径,根据运行时数据特征自适应选择最优路径:
flowchart TD START([Process 入口]) CHECK_LARGE{"totalTask ≥ 5120?"} COPY_IDX[CopyIn: index 数组] PUBLISH[向量化检测 index 有序性<br/>PublishIndexOrder] SYNC1[SyncAll:全核同步] LOAD_FLAGS[CopyIndexOrderFlagsIn<br/>加载所有核的有序标记] ALL_UNIQUE{"AllIndexes<br/>StrictlyIncreasing?"} FAST_PATH["<b>快速路径</b><br/>Duplicate(1.0) → CopyOut<br/>直接输出全 1.0"] INIT_WS["InitWorkspace<br/>(延迟初始化 scatter 空间)"] LOAD_SRC[CopySrcIn: 一次加载全部 src] SHOULD_RUN{"ShouldBuildRuns?<br/>(采样前 64 行估算)"} BUILD_RUNS["BuildRuns<br/>连续相同 index 合并为 run"] RUN_CHECK{"runCount × 3 ≤ coreTask?"} subgraph RUNWISE["Runwise 路径"] R_MAX["ReduceMaxRun (UB 内规约)"] R_SMAX[ScatterMax per-run<br/>仅 runCount 次 MTE3] R_EXP["GatherByRun + RunwiseSub + Exp"] R_SUM_CHECK{"RunGroups<br/>StrictlyIncreasing?"} R_OSUM["ReduceSumRun + Ordered ScatterAdd<br/>(无原子冲突)"] R_ASUM["ScatterAdd per-edge<br/>(原子聚合兜底)"] R_DIV["GatherByRun + RunwiseDiv"] end subgraph EDGEWISE["Edgewise 路径(兜底)"] E_MAX[ScatterMax per-edge<br/>逐 edge MTE3 原子写] E_EXP["GatherByEdge + Sub + Exp"] E_SUM[ScatterAdd per-edge] E_DIV["GatherByEdge + Adds(ε) + Div"] end COPYOUT[CopyOut: 写回最终结果] DONE([返回]) START --> CHECK_LARGE CHECK_LARGE -->|是| COPY_IDX CHECK_LARGE -->|否| INIT_WS COPY_IDX --> PUBLISH --> SYNC1 --> LOAD_FLAGS --> ALL_UNIQUE ALL_UNIQUE -->|是| FAST_PATH --> DONE ALL_UNIQUE -->|否| INIT_WS INIT_WS --> LOAD_SRC --> SHOULD_RUN SHOULD_RUN -->|是| BUILD_RUNS --> RUN_CHECK SHOULD_RUN -->|否| E_MAX RUN_CHECK -->|是:进入 Runwise 路径| R_MAX RUN_CHECK -->|否:回退 Edgewise| E_MAX R_MAX --> R_SMAX --> R_EXP --> R_SUM_CHECK R_SUM_CHECK -->|是| R_OSUM --> R_DIV R_SUM_CHECK -->|否| R_ASUM --> R_DIV R_DIV --> COPYOUT E_MAX --> E_EXP --> E_SUM --> E_DIV --> COPYOUT COPYOUT --> DONE style FAST_PATH fill:#4caf50,color:#fff style R_MAX fill:#2196f3,color:#fff style R_SMAX fill:#2196f3,color:#fff style R_EXP fill:#2196f3,color:#fff style R_OSUM fill:#2196f3,color:#fff style R_DIV fill:#2196f3,color:#fff
绿色 = 唯一索引快速路径,蓝色 = Run 级聚合路径。
优化 1:唯一索引快速路径(Unique-Index Fast Path)
当 index 数组严格递增(每条边指向不同节点)时,每组仅含 1 条边,softmax 结果恒为 1.0。优化后直接跳过全部 scatter 操作,用 Duplicate(1.0) 填充输出。
Duplicate(1.0)
检测过程使用向量化 Compare 指令批量比较相邻 index 值,并通过全核协同(每核发布有序标记到 GM,再批量加载判定)实现全局判定:
Compare
sequenceDiagram participant Core0 as AIV Core 0 participant Core1 as AIV Core 1 participant CoreN as AIV Core N participant GM as Global Memory Note over Core0,CoreN: Phase 1: 各核独立检测本地 index 有序性 Core0->>Core0: CopyIndexIn + CopyShiftedIndexIn Core1->>Core1: CopyIndexIn + CopyShiftedIndexIn CoreN->>CoreN: CopyIndexIn + CopyShiftedIndexIn Core0->>Core0: Compare(shifted, original, LE)<br/>向量化比较 64 元素/批 Core1->>Core1: Compare(shifted, original, LE) CoreN->>CoreN: Compare(shifted, original, LE) Note over Core0,CoreN: Phase 2: 发布有序标记 Core0->>GM: indexOrderFlagGm[0] = 0 (有序) Core1->>GM: indexOrderFlagGm[1] = 1 (无序) CoreN->>GM: indexOrderFlagGm[N] = 0 (有序) Note over Core0,CoreN: SyncAll 全核同步 Note over Core0,CoreN: Phase 3: 加载并汇总 GM->>Core0: CopyIndexOrderFlagsIn (所有核的标记) Core0->>Core0: AllIndexesStrictlyIncreasing? alt 全部有序 Core0->>Core0: Duplicate(1.0) + CopyOut Note over Core0: 提前返回,跳过所有 scatter else 存在无序 Note over Core0: 继续标准流程 end
优化 2:Run 级聚合(Run-Based Aggregation)
对于连续相同 index 的边序列("run"),先在 UB 内做 ReduceMax / ReduceSum 将每个 run 归约为一行,再做 scatter 操作。这将 scatter 次数从 E(边数)降低到 runCount(run 数),显著减少 GM 原子操作:
E
runCount
graph LR subgraph 原始["原始方案: 逐 edge scatter"] E0["edge 0 → group 2"] -->|"Atomic MTE3"| G2a["GM[2]"] E1["edge 1 → group 2"] -->|"Atomic MTE3"| G2b["GM[2]"] E2["edge 2 → group 2"] -->|"Atomic MTE3"| G2c["GM[2]"] E3["edge 3 → group 5"] -->|"Atomic MTE3"| G5a["GM[5]"] E4["edge 4 → group 5"] -->|"Atomic MTE3"| G5b["GM[5]"] E5["edge 5 → group 7"] -->|"Atomic MTE3"| G7a["GM[7]"] end subgraph 优化["优化方案: Run 级 scatter"] R0["Run 0: edge 0-2<br/>UB ReduceMax → 1 行"] -->|"1 次 MTE3"| G2["GM[2]"] R1["Run 1: edge 3-4<br/>UB ReduceMax → 1 行"] -->|"1 次 MTE3"| G5["GM[5]"] R2["Run 2: edge 5<br/>直接使用 src"] -->|"1 次 MTE3"| G7["GM[7]"] end style G2a fill:#ff6b6b,color:#fff style G2b fill:#ff6b6b,color:#fff style G2c fill:#ff6b6b,color:#fff style G5a fill:#ff6b6b,color:#fff style G5b fill:#ff6b6b,color:#fff style G7a fill:#ff6b6b,color:#fff style G2 fill:#4caf50,color:#fff style G5 fill:#4caf50,color:#fff style G7 fill:#4caf50,color:#fff
Run 构建采用启发式决策:先对前 64 行采样估算 run 密度(ShouldBuildRuns),仅当 sampleRunCount × 3 ≤ sampleRows 时才构建 run 表,避免在极端稀疏(几乎每条边一个 run)场景下引入额外开销。
ShouldBuildRuns
sampleRunCount × 3 ≤ sampleRows
优化 3:Runwise 广播计算
在 ComputeExp 和 ComputeOutput 阶段,原始实现对每条边都需从 GM gather 对应的 max/sum 值。优化后改为 per-run gather(仅 runCount 次 MTE2),再用 BinaryRepeatParams 对 run 内所有边做广播 Sub/Div:
BinaryRepeatParams
graph TB subgraph 原始["原始: 逐 edge Gather + 计算"] direction LR G0["GM → statLocal[0]"] --> S0["Sub(src[0], stat[0])"] G1["GM → statLocal[1]"] --> S1["Sub(src[1], stat[1])"] G2["GM → statLocal[2]"] --> S2["Sub(src[2], stat[2])"] G3["GM → statLocal[3]"] --> S3["Sub(src[3], stat[3])"] end subgraph 优化["优化: per-run Gather + 广播计算"] direction LR GR["GM → statLocal[runStart]<br/>仅 1 次 MTE2 per run"] GR --> BR["Sub(src[0..3], stat[runStart])<br/>repeat=4, 广播同一行"] end style G0 fill:#ff6b6b,color:#fff style G1 fill:#ff6b6b,color:#fff style G2 fill:#ff6b6b,color:#fff style G3 fill:#ff6b6b,color:#fff style GR fill:#4caf50,color:#fff style BR fill:#4caf50,color:#fff
优化 4:有序 Run ScatterSum
当所有 run 的 group index 严格递增(RunGroupsStrictlyIncreasing),ScatterSum 阶段各 run 不会写到同一个 GM 位置,原子操作不存在实际竞争。此时先在 UB 内 ReduceSum,再直接原子写结果,避免了逐 edge 原子加的开销。
RunGroupsStrictlyIncreasing
优化 5:延迟 Workspace 初始化
原始实现在 InitGM 中无条件初始化 scatter workspace(InitGlobalMemory 将 scatterMax 填充为 -INF、scatterSum 填充为 0)。优化后将初始化延迟到唯一索引快速路径判定之后——若快速路径命中则完全跳过初始化,节省 2 × coreWorkspace × 8 × sizeof(float) 字节的 GM 写操作。
InitGM
InitGlobalMemory
scatterMax
-INF
scatterSum
0
2 × coreWorkspace × 8 × sizeof(float)
优化 6:单遍数据加载
移除原始实现中的 singleLoopTaskCount 分块循环。优化后 Tiling 阶段将 singleLoopTaskCount 设为 coreTask(即每核全部任务量),所有 src 和 index 数据一次性加载到 UB,消除多轮 CopyIn/CopyOut 的同步开销。UB 容量经过精确计算(见 3.3 节),确保在 80% UB 可用空间内放下全部 buffer。
coreTask
选择理由:在 QCNet 的实际数据分布中,graph 结构常具有较高的 run 密度(连续边指向相同节点),Run 级聚合能显著减少 GM 操作。同时,稀疏图的唯一索引快速路径可以实现数量级的加速。启发式 run 构建决策确保在不利数据分布下不引入性能退化。
优化后的 Tiling 策略做了以下关键改进:
UB Buffer 布局(所有地址对齐到 32 字节):
srcBuf_
coreTask × 8 × 4
statBuf_
indexBuf_
coreTask × 4
runStartBuf_
runLengthBuf_
runGroupBuf_
reduceTmpBuf_
reduceTmpBytes
reducePartialBuf_
32
indexOrderFlagBuf_
aivNum × 32
UB 容量校验:Tiling 阶段按以下公式计算所需 UB 空间,若超出 80% 可用 UB 则拒绝任务:
requiredUB = 2 × Align32(coreTask × 8 × 4) // srcBuf + statBuf + Align32(coreTask × 4) // indexBuf + 3 × Align32(coreTask × 4) // runStart/Length/Group + Align32(reduceTmpBytes) // reduce 临时 + 2 × 32 // reducePartial + padding + aivNum × 32 // indexOrderFlags
Reduce 临时空间计算:通过 AscendC::GetReduceMaxMaxMinTmpSize 和 AscendC::GetReduceSumMaxMinTmpSize 在 host 端动态获取,取二者最大值作为 reduceTmpBytes 传入 kernel。
AscendC::GetReduceMaxMaxMinTmpSize
AscendC::GetReduceSumMaxMinTmpSize
Workspace 布局:
UserWorkspace: ┌─────────────────────────────────────────────┐ │ scatterMaxGm [totalWorkspace × 8] float │ ← 各核分区,按 coreWorkspace 偏移 ├─────────────────────────────────────────────┤ │ scatterSumGm [totalWorkspace × 8] float │ ├─────────────────────────────────────────────┤ │ indexOrderFlagGm [aivNum × 8] uint32 │ ← 各核有序标记(新增) └─────────────────────────────────────────────┘
优化后的 Process() 主流程:
Process()
stateDiagram-v2 [*] --> CheckLargeTask CheckLargeTask --> UniqueDetection: totalTask ≥ 5120 CheckLargeTask --> InitWorkspace: totalTask < 5120 state UniqueDetection { [*] --> CopyIndex CopyIndex --> VectorCompare: 向量化比较相邻 index VectorCompare --> PublishFlag: 写有序标记到 GM PublishFlag --> GlobalSync: SyncAll GlobalSync --> LoadAllFlags: 加载全核标记 LoadAllFlags --> CheckResult CheckResult --> FastExit: 全部有序 CheckResult --> [*]: 存在无序 } FastExit --> OutputOnes: Duplicate(1.0) + CopyOut OutputOnes --> [*] InitWorkspace --> CopyInAll: InitGlobalMemory + CopyIn src/index UniqueDetection --> InitWorkspace: 存在无序 CopyInAll --> BuildRunDecision state BuildRunDecision { [*] --> Sample64: 采样前 64 行 Sample64 --> EstimateDensity EstimateDensity --> BuildRuns: sampleRuns × 3 ≤ sampleRows EstimateDensity --> SkipRuns: 否则 BuildRuns --> RunwisePath SkipRuns --> EdgewisePath } state RunwisePath { [*] --> ReduceMaxPerRun ReduceMaxPerRun --> ScatterMaxPerRun ScatterMaxPerRun --> Sync1 Sync1 --> GatherMaxByRun GatherMaxByRun --> RunwiseSub RunwiseSub --> Exp Exp --> CheckOrdered CheckOrdered --> OrderedScatterSum: runs 有序 CheckOrdered --> EdgewiseScatterSum: runs 无序 OrderedScatterSum --> Sync2 EdgewiseScatterSum --> Sync2 Sync2 --> GatherSumByRun GatherSumByRun --> RunwiseDiv RunwiseDiv --> [*] } state EdgewisePath { [*] --> ScatterMaxPerEdge ScatterMaxPerEdge --> Sync3 Sync3 --> GatherMaxPerEdge GatherMaxPerEdge --> SubExp SubExp --> ScatterSumPerEdge ScatterSumPerEdge --> Sync4 Sync4 --> GatherSumPerEdge GatherSumPerEdge --> AddsDiv AddsDiv --> [*] } RunwisePath --> CopyOut EdgewisePath --> CopyOut CopyOut --> [*]
以 E = 30000(典型 QCNet 用例)为例,假设平均 run 密度为 3 边/run:
对于唯一索引场景(稀疏图),性能提升接近 100%(跳过全部 scatter)。
本次优化新增了完善的输入校验和空指针守护:
context == nullptr
platformInfoPtr == nullptr
aivNum == 0 || ubSize == 0
storageShape.GetDimNum() != 2
*NPtr <= 0 || *NPtr > MAX_GROUP_COUNT
numEdgeValue <= 0 || numEdgeValue > MAX_EDGE_COUNT
numFeatureValue != NUM_FEATURE
requiredUbBytes > availableUbBytes
reduceTmpBytes == 0
userWorkspace == nullptr
Input("src")
Input("index")
Output("softmax_result")
Attr("N")
AddConfig("ascend910b")
AddConfig("ascend910_93")
AddConfig("ascend950")
test_graph_softmax.py
precision_compare
ops_performance
SyncAll()
SetAtomicMax
SetAtomicAdd
DisableDmaAtomic
开发环境:
kernel_operator.h
kernel_tiling/kernel_tiling.h
tiling/tiling_api.h
开发约束:
可验收设计:
mx_driving.tools.precision_compare
mx_driving.tools.ops_perf
extern "C" __global__ __aicore__ void graph_softmax( GM_ADDR src, GM_ADDR index, GM_ADDR softmaxResult, GM_ADDR workspace, GM_ADDR tilingData);
softmaxResult
workspace
tilingData
返回参数:无(void kernel)。
异常处理:userWorkspace == nullptr 时直接返回,不执行计算。
约束说明:
[0, N)
变更说明:
indexOrderFlagGm
调用参考代码:
import torch import mx_driving src = torch.randn(10000, 8, dtype=torch.float32).npu() index = torch.randint(0, 5000, (10000,), dtype=torch.int32).npu() output = mx_driving.graph_softmax(src, index)
接口描述:Host 端到 Device 端的 Tiling 参数传递结构。
结构定义:
BEGIN_TILING_DATA_DEF(GraphSoftmaxTilingData) TILING_DATA_FIELD_DEF(uint32_t, coreTask); TILING_DATA_FIELD_DEF(uint32_t, coreWorkspace); TILING_DATA_FIELD_DEF(uint32_t, totalTask); TILING_DATA_FIELD_DEF(uint32_t, totalWorkspace); TILING_DATA_FIELD_DEF(uint32_t, bigCoreCount); TILING_DATA_FIELD_DEF(uint32_t, singleLoopTaskCount); TILING_DATA_FIELD_DEF(uint32_t, reduceTmpBytes); // 新增 END_TILING_DATA_DEF;
coreWorkspace
totalTask
totalWorkspace
bigCoreCount
本次优化不改变外部 API,无需更新用户侧编程手册。内部开发者参考:
kernels/graph_softmax/op_kernel/graph_softmax.cpp
kernels/graph_softmax/op_host/graph_softmax.cpp
docs/zh/tools/precision_compare.md
docs/zh/tools/ops_perf.md
复用现有 tests/torch/test_graph_softmax.py,覆盖以下测试维度:
tests/torch/test_graph_softmax.py
精度验证方法:
from mx_driving.tools.precision_compare import precision_compare result = precision_compare(npu_output, golden_output) assert result.passed # FP32: err_threshold=1e-4, pass_rate≥99.99%
性能验证方法:
from mx_driving.tools.ops_perf import ops_performance # Step 1: 采集 baseline(使用 master 分支编译的算子) output = ops_performance( mx_driving.graph_softmax, src_npu, index_npu, _perf={"mode": "baseline", "case_id": f"graph_softmax_E{E}"} ) # Step 2: 采集 candidate(使用优化分支编译的算子) output = ops_performance( mx_driving.graph_softmax, src_npu, index_npu, _perf={ "mode": "performance", "case_id": f"graph_softmax_E{E}", "baseline_files": [baseline_csv] } )
GraphSoftmaxGrad
GraphSoftmax
算子测试数据约束.md
/label add triaged
状态(Status): Reviewing
作者(Authors): @samulenzz
创建日期(Created): 2026-07-26
更新日期(Updated): 2026-07-26
相关 Issue/PR: #227
1. 概述
1.1 简介
本提案针对 DrivingSDK 仓中 GraphSoftmax 自定义算子进行 Ascend C 内核级性能优化。GraphSoftmax 是自动驾驶场景中图神经网络(GNN)的核心算子,用于对图结构中按节点分组的边特征执行 softmax 归一化。优化后在保持 FP32 精度的前提下,通过消除冗余 Global Memory 访问、引入 run 级聚合和唯一索引快速路径等手段,实现算子端到端性能提升 20% 以上。
1.2 动机
使用场景:GraphSoftmax 广泛应用于 QCNet 等自动驾驶感知模型中,对图中每条边的 8 维特征按目标节点分组做 softmax。在 QCNet 推理和训练流程中,该算子被高频调用,其性能直接影响模型整体推理延迟。
当前痛点:
singleLoopTaskCount硬编码为 2360),多次 CopyIn/CopyOut 带来重复的 MTE2/MTE3 同步开销。不做此提案的影响:在大规模图(边数 > 10000)场景下,GraphSoftmax 将成为 QCNet 推理的性能瓶颈;随着自动驾驶模型对图结构利用的加深,该瓶颈会愈发突出。
1.3 目标
目标:
非目标:
mx_driving.graph_softmax封装逻辑。2. 用例分析
2.1 功能用例
2.2 性能指标
2.3 DFX 要求
2.4 使用限制
src的 shape 必须为二维[E, 8],feature 维度固定为 8。index元素取值范围[0, 1500000)。3. 方案设计
3.1 总体方案
3.1.1 GraphSoftmax 算法概述
GraphSoftmax 对图中的边特征
src[E, 8]按index[E]分组执行数值稳定的 softmax:原始实现和优化后实现都遵循三阶段流程:ScatterMax → ComputeExp + ScatterSum → ComputeOutput。核心区别在于如何执行 scatter 聚合以及如何减少 GM 访问。
3.1.2 原始实现分析
原始 kernel 的主要性能瓶颈如下图所示:
flowchart TD subgraph INIT["初始化阶段"] A1[InitGlobalMemory<br/>初始化 scatterMax = -INF<br/>初始化 scatterSum = 0] end subgraph SCATTER_MAX["Phase 1: ScatterMax(分块循环)"] B1[CopyIn: src + index<br/>每次加载 ≤ 2360 条边] B2[逐 edge Atomic ScatterMax<br/>每条边 1 次 MTE3 原子写] B1 --> B2 B2 -->|下一块| B1 end subgraph SCATTER_SUM["Phase 2: ComputeExp + ScatterSum(分块循环)"] C1[CopyIn: src + index] C2["Gather scatterMax[index[e]]<br/>逐 edge 读 GM"] C3["Sub + Exp(向量计算)"] C4[逐 edge Atomic ScatterAdd<br/>每条边 1 次 MTE3 原子写] C5[CopyOut: exp 中间结果] C1 --> C2 --> C3 --> C4 --> C5 C5 -->|下一块| C1 end subgraph SOFTMAX["Phase 3: ComputeSoftmax(分块循环)"] D1[CopyIn: exp 中间结果 + index] D2["Gather scatterSum[index[e]]<br/>逐 edge 读 GM"] D3["Adds(ε) + Div(向量计算)"] D4[CopyOut: 最终结果] D1 --> D2 --> D3 --> D4 D4 -->|下一块| D1 end INIT --> SCATTER_MAX --> SCATTER_SUM --> SOFTMAX style B2 fill:#ff6b6b,color:#fff style C2 fill:#ff6b6b,color:#fff style C4 fill:#ff6b6b,color:#fff style D2 fill:#ff6b6b,color:#fff瓶颈量化:对于 E = 30000 的典型用例:
3.1.3 优化方案总览
优化后的实现引入三条互斥的执行路径,根据运行时数据特征自适应选择最优路径:
flowchart TD START([Process 入口]) CHECK_LARGE{"totalTask ≥ 5120?"} COPY_IDX[CopyIn: index 数组] PUBLISH[向量化检测 index 有序性<br/>PublishIndexOrder] SYNC1[SyncAll:全核同步] LOAD_FLAGS[CopyIndexOrderFlagsIn<br/>加载所有核的有序标记] ALL_UNIQUE{"AllIndexes<br/>StrictlyIncreasing?"} FAST_PATH["<b>快速路径</b><br/>Duplicate(1.0) → CopyOut<br/>直接输出全 1.0"] INIT_WS["InitWorkspace<br/>(延迟初始化 scatter 空间)"] LOAD_SRC[CopySrcIn: 一次加载全部 src] SHOULD_RUN{"ShouldBuildRuns?<br/>(采样前 64 行估算)"} BUILD_RUNS["BuildRuns<br/>连续相同 index 合并为 run"] RUN_CHECK{"runCount × 3 ≤ coreTask?"} subgraph RUNWISE["Runwise 路径"] R_MAX["ReduceMaxRun (UB 内规约)"] R_SMAX[ScatterMax per-run<br/>仅 runCount 次 MTE3] R_EXP["GatherByRun + RunwiseSub + Exp"] R_SUM_CHECK{"RunGroups<br/>StrictlyIncreasing?"} R_OSUM["ReduceSumRun + Ordered ScatterAdd<br/>(无原子冲突)"] R_ASUM["ScatterAdd per-edge<br/>(原子聚合兜底)"] R_DIV["GatherByRun + RunwiseDiv"] end subgraph EDGEWISE["Edgewise 路径(兜底)"] E_MAX[ScatterMax per-edge<br/>逐 edge MTE3 原子写] E_EXP["GatherByEdge + Sub + Exp"] E_SUM[ScatterAdd per-edge] E_DIV["GatherByEdge + Adds(ε) + Div"] end COPYOUT[CopyOut: 写回最终结果] DONE([返回]) START --> CHECK_LARGE CHECK_LARGE -->|是| COPY_IDX CHECK_LARGE -->|否| INIT_WS COPY_IDX --> PUBLISH --> SYNC1 --> LOAD_FLAGS --> ALL_UNIQUE ALL_UNIQUE -->|是| FAST_PATH --> DONE ALL_UNIQUE -->|否| INIT_WS INIT_WS --> LOAD_SRC --> SHOULD_RUN SHOULD_RUN -->|是| BUILD_RUNS --> RUN_CHECK SHOULD_RUN -->|否| E_MAX RUN_CHECK -->|是:进入 Runwise 路径| R_MAX RUN_CHECK -->|否:回退 Edgewise| E_MAX R_MAX --> R_SMAX --> R_EXP --> R_SUM_CHECK R_SUM_CHECK -->|是| R_OSUM --> R_DIV R_SUM_CHECK -->|否| R_ASUM --> R_DIV R_DIV --> COPYOUT E_MAX --> E_EXP --> E_SUM --> E_DIV --> COPYOUT COPYOUT --> DONE style FAST_PATH fill:#4caf50,color:#fff style R_MAX fill:#2196f3,color:#fff style R_SMAX fill:#2196f3,color:#fff style R_EXP fill:#2196f3,color:#fff style R_OSUM fill:#2196f3,color:#fff style R_DIV fill:#2196f3,color:#fff3.1.4 优化策略详解
优化 1:唯一索引快速路径(Unique-Index Fast Path)
当
index数组严格递增(每条边指向不同节点)时,每组仅含 1 条边,softmax 结果恒为 1.0。优化后直接跳过全部 scatter 操作,用Duplicate(1.0)填充输出。检测过程使用向量化
Compare指令批量比较相邻 index 值,并通过全核协同(每核发布有序标记到 GM,再批量加载判定)实现全局判定:sequenceDiagram participant Core0 as AIV Core 0 participant Core1 as AIV Core 1 participant CoreN as AIV Core N participant GM as Global Memory Note over Core0,CoreN: Phase 1: 各核独立检测本地 index 有序性 Core0->>Core0: CopyIndexIn + CopyShiftedIndexIn Core1->>Core1: CopyIndexIn + CopyShiftedIndexIn CoreN->>CoreN: CopyIndexIn + CopyShiftedIndexIn Core0->>Core0: Compare(shifted, original, LE)<br/>向量化比较 64 元素/批 Core1->>Core1: Compare(shifted, original, LE) CoreN->>CoreN: Compare(shifted, original, LE) Note over Core0,CoreN: Phase 2: 发布有序标记 Core0->>GM: indexOrderFlagGm[0] = 0 (有序) Core1->>GM: indexOrderFlagGm[1] = 1 (无序) CoreN->>GM: indexOrderFlagGm[N] = 0 (有序) Note over Core0,CoreN: SyncAll 全核同步 Note over Core0,CoreN: Phase 3: 加载并汇总 GM->>Core0: CopyIndexOrderFlagsIn (所有核的标记) Core0->>Core0: AllIndexesStrictlyIncreasing? alt 全部有序 Core0->>Core0: Duplicate(1.0) + CopyOut Note over Core0: 提前返回,跳过所有 scatter else 存在无序 Note over Core0: 继续标准流程 end优化 2:Run 级聚合(Run-Based Aggregation)
对于连续相同 index 的边序列("run"),先在 UB 内做 ReduceMax / ReduceSum 将每个 run 归约为一行,再做 scatter 操作。这将 scatter 次数从
E(边数)降低到runCount(run 数),显著减少 GM 原子操作:graph LR subgraph 原始["原始方案: 逐 edge scatter"] E0["edge 0 → group 2"] -->|"Atomic MTE3"| G2a["GM[2]"] E1["edge 1 → group 2"] -->|"Atomic MTE3"| G2b["GM[2]"] E2["edge 2 → group 2"] -->|"Atomic MTE3"| G2c["GM[2]"] E3["edge 3 → group 5"] -->|"Atomic MTE3"| G5a["GM[5]"] E4["edge 4 → group 5"] -->|"Atomic MTE3"| G5b["GM[5]"] E5["edge 5 → group 7"] -->|"Atomic MTE3"| G7a["GM[7]"] end subgraph 优化["优化方案: Run 级 scatter"] R0["Run 0: edge 0-2<br/>UB ReduceMax → 1 行"] -->|"1 次 MTE3"| G2["GM[2]"] R1["Run 1: edge 3-4<br/>UB ReduceMax → 1 行"] -->|"1 次 MTE3"| G5["GM[5]"] R2["Run 2: edge 5<br/>直接使用 src"] -->|"1 次 MTE3"| G7["GM[7]"] end style G2a fill:#ff6b6b,color:#fff style G2b fill:#ff6b6b,color:#fff style G2c fill:#ff6b6b,color:#fff style G5a fill:#ff6b6b,color:#fff style G5b fill:#ff6b6b,color:#fff style G7a fill:#ff6b6b,color:#fff style G2 fill:#4caf50,color:#fff style G5 fill:#4caf50,color:#fff style G7 fill:#4caf50,color:#fffRun 构建采用启发式决策:先对前 64 行采样估算 run 密度(
ShouldBuildRuns),仅当sampleRunCount × 3 ≤ sampleRows时才构建 run 表,避免在极端稀疏(几乎每条边一个 run)场景下引入额外开销。优化 3:Runwise 广播计算
在 ComputeExp 和 ComputeOutput 阶段,原始实现对每条边都需从 GM gather 对应的 max/sum 值。优化后改为 per-run gather(仅
runCount次 MTE2),再用BinaryRepeatParams对 run 内所有边做广播 Sub/Div:graph TB subgraph 原始["原始: 逐 edge Gather + 计算"] direction LR G0["GM → statLocal[0]"] --> S0["Sub(src[0], stat[0])"] G1["GM → statLocal[1]"] --> S1["Sub(src[1], stat[1])"] G2["GM → statLocal[2]"] --> S2["Sub(src[2], stat[2])"] G3["GM → statLocal[3]"] --> S3["Sub(src[3], stat[3])"] end subgraph 优化["优化: per-run Gather + 广播计算"] direction LR GR["GM → statLocal[runStart]<br/>仅 1 次 MTE2 per run"] GR --> BR["Sub(src[0..3], stat[runStart])<br/>repeat=4, 广播同一行"] end style G0 fill:#ff6b6b,color:#fff style G1 fill:#ff6b6b,color:#fff style G2 fill:#ff6b6b,color:#fff style G3 fill:#ff6b6b,color:#fff style GR fill:#4caf50,color:#fff style BR fill:#4caf50,color:#fff优化 4:有序 Run ScatterSum
当所有 run 的 group index 严格递增(
RunGroupsStrictlyIncreasing),ScatterSum 阶段各 run 不会写到同一个 GM 位置,原子操作不存在实际竞争。此时先在 UB 内 ReduceSum,再直接原子写结果,避免了逐 edge 原子加的开销。优化 5:延迟 Workspace 初始化
原始实现在
InitGM中无条件初始化 scatter workspace(InitGlobalMemory将scatterMax填充为-INF、scatterSum填充为0)。优化后将初始化延迟到唯一索引快速路径判定之后——若快速路径命中则完全跳过初始化,节省2 × coreWorkspace × 8 × sizeof(float)字节的 GM 写操作。优化 6:单遍数据加载
移除原始实现中的
singleLoopTaskCount分块循环。优化后 Tiling 阶段将singleLoopTaskCount设为coreTask(即每核全部任务量),所有 src 和 index 数据一次性加载到 UB,消除多轮 CopyIn/CopyOut 的同步开销。UB 容量经过精确计算(见 3.3 节),确保在 80% UB 可用空间内放下全部 buffer。3.1.5 优化前后对比
3.2 技术选型
选择理由:在 QCNet 的实际数据分布中,graph 结构常具有较高的 run 密度(连续边指向相同节点),Run 级聚合能显著减少 GM 操作。同时,稀疏图的唯一索引快速路径可以实现数量级的加速。启发式 run 构建决策确保在不利数据分布下不引入性能退化。
3.3 功能与性能设计
3.3.1 Tiling 设计
优化后的 Tiling 策略做了以下关键改进:
UB Buffer 布局(所有地址对齐到 32 字节):
srcBuf_coreTask × 8 × 4字节statBuf_coreTask × 8 × 4字节indexBuf_coreTask × 4字节runStartBuf_coreTask × 4字节runLengthBuf_coreTask × 4字节runGroupBuf_coreTask × 4字节reduceTmpBuf_reduceTmpBytes字节reducePartialBuf_32字节indexOrderFlagBuf_aivNum × 32字节UB 容量校验:Tiling 阶段按以下公式计算所需 UB 空间,若超出 80% 可用 UB 则拒绝任务:
Reduce 临时空间计算:通过
AscendC::GetReduceMaxMaxMinTmpSize和AscendC::GetReduceSumMaxMinTmpSize在 host 端动态获取,取二者最大值作为reduceTmpBytes传入 kernel。Workspace 布局:
3.3.2 核心流程
优化后的
Process()主流程:stateDiagram-v2 [*] --> CheckLargeTask CheckLargeTask --> UniqueDetection: totalTask ≥ 5120 CheckLargeTask --> InitWorkspace: totalTask < 5120 state UniqueDetection { [*] --> CopyIndex CopyIndex --> VectorCompare: 向量化比较相邻 index VectorCompare --> PublishFlag: 写有序标记到 GM PublishFlag --> GlobalSync: SyncAll GlobalSync --> LoadAllFlags: 加载全核标记 LoadAllFlags --> CheckResult CheckResult --> FastExit: 全部有序 CheckResult --> [*]: 存在无序 } FastExit --> OutputOnes: Duplicate(1.0) + CopyOut OutputOnes --> [*] InitWorkspace --> CopyInAll: InitGlobalMemory + CopyIn src/index UniqueDetection --> InitWorkspace: 存在无序 CopyInAll --> BuildRunDecision state BuildRunDecision { [*] --> Sample64: 采样前 64 行 Sample64 --> EstimateDensity EstimateDensity --> BuildRuns: sampleRuns × 3 ≤ sampleRows EstimateDensity --> SkipRuns: 否则 BuildRuns --> RunwisePath SkipRuns --> EdgewisePath } state RunwisePath { [*] --> ReduceMaxPerRun ReduceMaxPerRun --> ScatterMaxPerRun ScatterMaxPerRun --> Sync1 Sync1 --> GatherMaxByRun GatherMaxByRun --> RunwiseSub RunwiseSub --> Exp Exp --> CheckOrdered CheckOrdered --> OrderedScatterSum: runs 有序 CheckOrdered --> EdgewiseScatterSum: runs 无序 OrderedScatterSum --> Sync2 EdgewiseScatterSum --> Sync2 Sync2 --> GatherSumByRun GatherSumByRun --> RunwiseDiv RunwiseDiv --> [*] } state EdgewisePath { [*] --> ScatterMaxPerEdge ScatterMaxPerEdge --> Sync3 Sync3 --> GatherMaxPerEdge GatherMaxPerEdge --> SubExp SubExp --> ScatterSumPerEdge ScatterSumPerEdge --> Sync4 Sync4 --> GatherSumPerEdge GatherSumPerEdge --> AddsDiv AddsDiv --> [*] } RunwisePath --> CopyOut EdgewisePath --> CopyOut CopyOut --> [*]3.3.3 性能提升分析
以 E = 30000(典型 QCNet 用例)为例,假设平均 run 密度为 3 边/run:
对于唯一索引场景(稀疏图),性能提升接近 100%(跳过全部 scatter)。
3.4 安全隐私与 DFX 设计
3.4.1 安全性增强
本次优化新增了完善的输入校验和空指针守护:
context == nullptrplatformInfoPtr == nullptraivNum == 0 || ubSize == 0storageShape.GetDimNum() != 2*NPtr <= 0 || *NPtr > MAX_GROUP_COUNTnumEdgeValue <= 0 || numEdgeValue > MAX_EDGE_COUNTnumFeatureValue != NUM_FEATURErequiredUbBytes > availableUbBytesreduceTmpBytes == 0userWorkspace == nullptrcontext == nullptr3.4.2 兼容性设计
Input("src")、Input("index")、Output("softmax_result")、Attr("N")保持一致。AddConfig("ascend910b")和AddConfig("ascend910_93"),新增AddConfig("ascend950")条件编译。reduceTmpBytes字段,为前向扩展保留空间。3.4.3 可测试性设计
test_graph_softmax.pyUT,覆盖 E ∈ {1, 1112, 2223, ..., 50000} 共 46 个 case。precision_compare工具。ops_performance工具。3.4.4 可靠性设计
SyncAll()确保 scatter 结果全局可见后再进入下一阶段。SetAtomicMax/SetAtomicAdd/DisableDmaAtomic严格配对。Compare+ 位掩码检查,不依赖浮点比较。3.5 编程与调用设计
3.5.1 编程模型基本设计
开发环境:
kernel_operator.h、kernel_tiling/kernel_tiling.h、tiling/tiling_api.h开发约束:
可验收设计:
mx_driving.tools.precision_comparemx_driving.tools.ops_perftest_graph_softmax.py3.5.2 接口定义与设计
3.5.2.1 GraphSoftmax(AscendC Kernel API)
extern "C" __global__ __aicore__ void graph_softmax( GM_ADDR src, GM_ADDR index, GM_ADDR softmaxResult, GM_ADDR workspace, GM_ADDR tilingData);srcindexsoftmaxResultworkspacetilingData返回参数:无(void kernel)。
异常处理:
userWorkspace == nullptr时直接返回,不执行计算。约束说明:
src的第二维必须为 8。index的元素值必须在[0, N)范围内,N 由 Attr("N") 指定。变更说明:
reduceTmpBytesTiling 字段。indexOrderFlagGm区域。调用参考代码:
import torch import mx_driving src = torch.randn(10000, 8, dtype=torch.float32).npu() index = torch.randint(0, 5000, (10000,), dtype=torch.int32).npu() output = mx_driving.graph_softmax(src, index)3.5.2.2 GraphSoftmaxTilingData(Tiling 数据结构)
接口描述:Host 端到 Device 端的 Tiling 参数传递结构。
结构定义:
BEGIN_TILING_DATA_DEF(GraphSoftmaxTilingData) TILING_DATA_FIELD_DEF(uint32_t, coreTask); TILING_DATA_FIELD_DEF(uint32_t, coreWorkspace); TILING_DATA_FIELD_DEF(uint32_t, totalTask); TILING_DATA_FIELD_DEF(uint32_t, totalWorkspace); TILING_DATA_FIELD_DEF(uint32_t, bigCoreCount); TILING_DATA_FIELD_DEF(uint32_t, singleLoopTaskCount); TILING_DATA_FIELD_DEF(uint32_t, reduceTmpBytes); // 新增 END_TILING_DATA_DEF;coreTaskcoreWorkspacetotalTasktotalWorkspacebigCoreCountsingleLoopTaskCountreduceTmpBytesreduceTmpBytes字段,用于传递 ReduceMax/ReduceSum 的临时 buffer 大小。singleLoopTaskCount语义变更:从固定值 2360 变为等于coreTask(消除分块循环)。3.5.3 编程手册设计
本次优化不改变外部 API,无需更新用户侧编程手册。内部开发者参考:
kernels/graph_softmax/op_kernel/graph_softmax.cpp中的模块化设计,各优化策略通过独立方法实现。kernels/graph_softmax/op_host/graph_softmax.cpp中的 UB 容量计算和 Reduce 临时空间动态获取方式。mx_driving.tools.precision_compare工具,参考docs/zh/tools/precision_compare.md。mx_driving.tools.ops_perf工具,参考docs/zh/tools/ops_perf.md。4. 测试设计
4.1 单元测试 (UT)
复用现有
tests/torch/test_graph_softmax.py,覆盖以下测试维度:精度验证方法:
from mx_driving.tools.precision_compare import precision_compare result = precision_compare(npu_output, golden_output) assert result.passed # FP32: err_threshold=1e-4, pass_rate≥99.99%性能验证方法:
from mx_driving.tools.ops_perf import ops_performance # Step 1: 采集 baseline(使用 master 分支编译的算子) output = ops_performance( mx_driving.graph_softmax, src_npu, index_npu, _perf={"mode": "baseline", "case_id": f"graph_softmax_E{E}"} ) # Step 2: 采集 candidate(使用优化分支编译的算子) output = ops_performance( mx_driving.graph_softmax, src_npu, index_npu, _perf={ "mode": "performance", "case_id": f"graph_softmax_E{E}", "baseline_files": [baseline_csv] } )4.2 集成测试
mx_driving.graph_softmaxPyTorch 接口调用,验证 forward + backward 联合正确性。GraphSoftmaxGrad(未修改)与优化后GraphSoftmax的配合正确性。4.3 端到端测试
ops_performance工具在模型流水线中采集算子级性能数据。附录
参考资料链接
docs/zh/tools/ops_perf.mddocs/zh/tools/precision_compare.md算子测试数据约束.md