作者​:昇腾实战派
知识地图​:https://blog.csdn.net/Lumos_Lovegood/article/details/161601003

1. 背景概述

在 AI 计算场景中,算子的执行效率直接影响模型训练与推理的整体性能。Ascend C 作为面向昇腾 AI 处理器的算子开发语言,提供了丰富的编程接口与硬件抽象能力。然而,要充分发挥硬件潜力,开发者需要系统性地识别并消除性能瓶颈。本文从搬运、内存、API 使用、流水、Tiling 及头开销等六大方向出发,结合实际案例,总结了一套可复用的性能优化方法论,帮助开发者快速定位问题并实施有效优化。

2. 优化总览与方法论

Ascend C 算子性能优化是一个持续迭代的过程,遵循四步循环:性能分析 → 识别瓶颈 → 实施优化 → 验证效果,不断重复直至达成性能目标。

优化手段分为六大方向:搬运优化、内存优化、API 使用优化、流水优化、Tiling 优化、头开销与指令优化。各建议按优先级分类——为大多数算子带来收益的建议具有最高优先级,仅影响特定场景的手段给予较低优先级。开发者应先使用 msProf(获取 Profiling 数据)和 CAModel(获取仿真流水图)定位瓶颈,再选择对应方向开展优化。


3. 搬运优化

3.1 尽量一次搬运较大的数据块

原理说明

搬运不同大小的数据块时,对带宽的利用率(有效带宽 / 理论带宽)不同。硬件的 DMA 引擎启动一次搬运存在固定开销,小数据块搬运时该开销占比高,带宽利用率低。根据实测经验,单次搬运数据长度达到 16KB 以上时,通常能较好地发挥出带宽的最佳性能。

优化路径

  1. 在 Tiling 阶段合理设计切分大小,确保每次搬运的数据量足够大(≥16KB)。
  2. 对于 Matmul 场景,使能大包搬运(设置 depthA1/depthB1),一次将多个基本块从 GM 搬入 L1。
  3. 合并多个小张量的搬运操作,减少搬运次数。

简单案例

Matmul 算子中,默认每次从 GM 搬运一个基本块(baseM×baseK)到 L1。使能大包搬运后,一次搬入 depthA1 个基本块,搬运数据量从几 KB 提升到几十 KB,MTE2 搬运效率提升约 20%+。


3.2 GM 地址尽量 512B 对齐

原理说明

AI 处理器的 DMA 引擎在从 GM 向 Local Memory 搬运数据时,针对 512B 对齐的地址有硬件加速路径。若地址仅满足 32B 最低对齐要求,DMA 引擎需要额外的地址转换和拼接操作,导致有效带宽下降。实测显示,32B 对齐场景下带宽最差仅为 512B 对齐场景的 70%。

优化路径

  1. 在算子设计阶段,规划输入输出 Tensor 的内存分配策略,使起始地址 512B 对齐。
  2. Tiling 切分时,确保每个数据块的起始偏移量为 512B 的整数倍。
  3. 必要时对数据进行 padding,以满足对齐要求。

简单案例

某 Vector 算子输入 Tensor 起始地址为 32B 对齐,每次从 GM 搬运 64KB 数据。调整内存分配使起始地址 512B 对齐后,同样搬运 64KB 数据,搬运耗时降低约 30%,整体算子性能提升约 15%。


3.3 高效使用搬运 API

原理说明

DataCopy 等搬运 API 提供了 srcStridedstStrideblockLenblockCount 等参数,可在一次 API 调用中完成多块数据的批量搬运。DMA 引擎内部对这种批量搬运做了流水优化。若使用 for 循环逐块调用 DataCopy,每次调用都有独立的指令发射和启动开销,且无法利用 DMA 内部流水,效率远低于批量搬运。

优化路径

  1. 分析数据在 GM 上的排布,识别是否存在固定间隔的搬运模式。
  2. 将 for 循环搬运改写为带 stride 参数的单次 DataCopy 调用。
  3. 验证搬运结果的正确性(stride 计算容易出错)。

简单案例

从 GM 上搬运图像数据,每行 16KB 中取前 2KB,共 16 行。

// 反例:for 循环,每次仅搬 2KB,共 16 次
for (int i = 0; i < 16; i++) {
    DataCopy(tensorIn[i * copyWidth], tensorGM[i * imgWidth], copyWidth);
}

// 正例:stride 参数,一次搬完 32KB
DataCopyParams params;
params.blockLen   = copyWidth / 8;
params.blockCount = 16;
params.srcStride  = (imgWidth - copyWidth) / 8;
params.dstStride  = 0;
DataCopy(tensorIn, tensorGM, params);

正例只产生一条 DMA 指令,DMA 引擎内部流水处理 16 个块,总体搬运效率远高于 16 次独立调用。


4. 内存优化

4.1 UB Buffer 融合实现连续 Vector 计算

原理说明

当算子包含多次串联的 Vector 计算(前一次输出是后一次输入)时,如果每次计算后都将中间结果搬回 GM、再从 GM 搬入 UB 供下一次使用,会产生大量冗余搬运。由于这些中间数据只在核内使用,完全可以暂存在 UB 上直接传递,省去中间搬运环节。

优化路径

  1. 识别算子中连续的 Vector 计算链。
  2. 为中间结果分配 UB Buffer(使用 TBuf 而非 TQue)。
  3. 将中间搬运环节去掉,前一次输出 Tensor 直接作为后一次输入 Tensor。
  4. 仅保留首次 CopyIn 和末次 CopyOut。

简单案例

算子计算逻辑为 z = ReLU(x + y),包含 Add 和 ReLU 两步。优化前:CopyIn(x,y) → Add → CopyOut(tmp) → CopyIn(tmp) → ReLU → CopyOut(z),共 4 次搬运。优化后:CopyIn(x,y) → Add(结果留在 UB) → ReLU → CopyOut(z),仅 2 次搬运,搬运耗时减少约 50%。


4.2 L0C Buffer 暂存实现矩阵乘结果累加

原理说明

矩阵乘计算结果存放在 L0C(CO1)上。在需要多次矩阵乘并累加结果的场景中,如果每次计算后都将 L0C 结果搬出到 GM 再搬回,搬运开销巨大。L0C 支持原地累加能力(Mmad 指令的 initVal 参数),可直接在 L0C 上对多次矩阵乘结果进行累加,最终一次性搬出。

优化路径

  1. 识别循环内的矩阵乘累加模式。
  2. 第一次 Mmad 使用 initVal=0(清零后计算),后续迭代使用 initVal=1(累加模式)。
  3. 所有迭代完成后,通过 FixPipe 一次性将 L0C 结果搬出。

简单案例

计算 C = A1×B1 + A2×B2,K 方向分两次迭代。优化前每次 Mmad 后搬出到 GM 再加,共 4 次搬运。优化后利用 L0C 累加:第一次 Mmad(initVal=0),第二次 Mmad(initVal=1) 直接在 L0C 上累加,最后一次 FixPipe 搬出,搬运次数减少 75%。


4.3 较小矩阵长驻 L1 Buffer

原理说明

在矩阵乘 C = A × B 的场景中,若 A 矩阵较小、B 矩阵较大,对 B 进行分块处理时每次迭代都需要重新搬入 A。由于 L1 容量远大于 L0A/L0B,可以将 A 一次性搬入 L1 并常驻,后续迭代只需搬运 B 的不同分块,从而减少 A 的重复搬运。

优化路径

  1. 评估较小矩阵是否能完整放入 L1(注意预留空间给大矩阵分块)。
  2. 在首次迭代前将小矩阵搬入 L1,后续迭代复用。
  3. 仅对大矩阵按 L1 剩余空间分块搬运。

简单案例

A 矩阵大小 64KB,B 矩阵大小 2MB,L1 容量 1MB。将 A 常驻 L1(占 64KB),剩余 960KB 用于 B 的分块搬运。B 分 3 次搬入,A 的搬运次数从 3 次降为 1 次,总搬运量减少约 10%。


4.4 TPipe 对象创建位置优化

原理说明

TPipe 对象初始化时会设置全局变量的 TPipe 指针,若 TPipe 定义在 Kernel 类内部,编译器认为 Kernel 对象的内存有被外部修改的风险,会对类内 Scalar 变量采取保守策略,放弃常量折叠和常量传播等编译优化,导致 Scalar 指令数增多、耗时增加。

优化路径

  1. 将 TPipe 对象从 Kernel 类内部移至类外部(如全局变量或作为独立对象传入)。
  2. 确保 TPipe 的内存空间独立于 Kernel 类对象。
  3. 验证编译后 Scalar 指令数是否减少。

简单案例

// 反例:TPipe 在类内部
class KernelAdd {
    TPipe pipe;          // 阻碍编译器优化
    int32_t tileLength;  // 无法被常量折叠
};

// 正例:TPipe 在类外部
TPipe pipe;              // 独立内存空间
class KernelAdd {
    int32_t tileLength;  // 编译器可做常量折叠
};

实测某算子将 TPipe 外移后,Scalar 指令耗时降低约 15%。


5. API 使用优化

5.1 优先使用高阶 API

原理说明

高阶 API(如 Matmul、Conv2d 等)由昇腾官方针对硬件特性深度优化,内部集成了分块策略、数据搬运调度、流水线并行等多层优化逻辑。手动用底层 API 实现同等功能,不仅开发工作量大,而且难以达到同等的优化深度。

优化路径

  1. 优先检查昇腾是否提供对应的高阶 API。
  2. 使用高阶 API 替代手写的底层实现。
  3. 通过高阶 API 提供的模板参数(如 MDL 模板)进一步调优。

简单案例

实现矩阵乘 C = A × B,手动使用 Mmad 底层 API 编写分块循环、搬运调度,总代码约 200 行,实测耗时 120us。替换为 Matmul 高阶 API 后,代码缩减至 30 行,实测耗时 75us,性能提升约 37%。


5.2 Vector 算子灵活运用 Counter 模式

原理说明

Vector 指令通过 mask 控制每次迭代处理哪些元素。Normal 模式下需要手动计算并设置 bitmap 形式的 mask,对于非对齐数据量容易出错且有额外的 mask 计算开销。Counter 模式(SetMaskCount)直接指定处理的元素个数,硬件自动生成 mask,更灵活高效。

优化路径

  1. SetMaskNorm 切换为 SetMaskCount
  2. SetVectorMask<T>(0, elementCount) 直接指定元素个数。
  3. 计算完成后调用 ResetMask 恢复默认。

简单案例

处理 15000 个 half 元素的 Add 计算:

uint32_t totalLen = 15000;
uint32_t numPerRepeat = 128;  // half 类型每次处理 128 个
uint32_t mainRepeat = totalLen / numPerRepeat;  // 117 次
uint32_t tailNum = totalLen % numPerRepeat;      // 24 个尾元素

AscendC::SetMaskCount();
AscendC::SetVectorMask<half>(0, numPerRepeat);
AscendC::Add<half, false>(z, x, y, AscendC::MASK_PLACEHOLDER, mainRepeat, params);
if (tailNum > 0) {
    AscendC::SetVectorMask<half>(0, tailNum);
    AscendC::Add<half, false>(z[mainRepeat*numPerRepeat],
        x[mainRepeat*numPerRepeat], y[mainRepeat*numPerRepeat],
        AscendC::MASK_PLACEHOLDER, 1, params);
}
AscendC::ResetMask();

Counter 模式自动处理尾块对齐,无需手动计算 bitmap,代码更简洁且避免了 mask 计算开销。


5.3 针对不同场景合理使用归约指令

原理说明

数据归约(如 ReduceSum)有多种实现方式:WholeReduceSum 对整个 Tensor 做全局归约,BlockReduceSum 先做 32B 块内归约。对于大数据量的归约,先块内再全局的两级策略比直接全局归约的指令效率更高,因为块内归约充分利用了 Vector 单元的并行能力。

优化路径

  1. 评估数据量大小,选择合适的归约层级。
  2. 大数据量:先 BlockReduceSum 将数据缩减到块级别,再 WholeReduceSum 做全局归约。
  3. 小数据量:直接 WholeReduceSum 即可。

简单案例

对 4096 个 float 元素求和。方案一:两次 WholeReduceSum,实测 100 次循环总耗时 85us。方案二:一次 BlockReduceSum + 一次 WholeReduceSum,实测同样 100 次循环总耗时仅 52us,性能提升约 39%。


6. 流水优化

6.1 基于编程范式实现流水并行

原理说明

AI Core 内部的执行单元(MTE2 搬入、Vector/Cube 计算、MTE3 搬出等)是独立的流水线节点,可以异步并行执行。Ascend C 编程范式将核内逻辑划分为 CopyIn、Compute、CopyOut 三个阶段,通过 Queue 队列机制实现数据在不同阶段间的传递和同步。正确实现此范式后,搬入单元处理第 N+1 批数据时,计算单元可同时处理第 N 批数据。

优化路径

  1. 将算子逻辑拆分为 CopyIn、Compute、CopyOut 三个函数。
  2. 使用 TQueAllocTensor / EnQue / DeQue / FreeTensor 管理数据流转。
  3. 在主循环中依次调用三个阶段函数,Queue 机制自动处理流水依赖。

简单案例

for (int i = 0; i < loopCount; i++) {
    CopyIn(i);    // MTE2 搬入第 i 批数据到 VECIN 队列
    Compute(i);   // Vector 从 VECIN 取数据计算,结果放入 VECOUT 队列
    CopyOut(i);   // MTE3 从 VECOUT 取数据搬出到 GM
}

虽然代码看似串行调用,但由于 MTE2、Vector、MTE3 是独立硬件单元且通过 Queue 解耦,实际执行时第 i+1 次 CopyIn 与第 i 次 Compute 可并行,实现流水掩盖。


6.2 使能 Double Buffer

原理说明

基本流水范式中,若 Queue 仅有一个 Buffer,当计算单元正在使用该 Buffer 时,搬入单元必须等待其释放后才能写入下一批数据,造成搬运和计算无法完全重叠。Double Buffer 为 Queue 分配两个 Buffer(Ping/Pong),搬入单元写入 Buffer A 的同时计算单元可以读取 Buffer B,实现搬运与计算的完全并行。

优化路径

  1. 评估 UB 空间是否足够容纳双份 Buffer(Double Buffer 需要 2 倍内存)。
  2. InitBuffer 的内存块个数从 1 改为 2。
  3. 如果 UB 空间不足,需减小每块的数据量(减小 tileLength)后再使能。
  4. 用 Profiling 工具验证流水线是否确实实现了并行。

简单案例

// 使能前:单 buffer,搬运与计算串行
pipe.InitBuffer(inQueueX, 1, tileLen * sizeof(half));

// 使能后:双 buffer,搬运与计算并行
pipe.InitBuffer(inQueueX, 2, tileLen * sizeof(half));

某 Vector 算子使能 Double Buffer 后,从 Profiling 流水图可见 MTE2 和 Vector 流水线交替执行无空隙,算子耗时从 50us 降至 32us,性能提升约 56%。


6.3 使能 Iterate 异步接口避免 AIC/AIV 同步依赖

原理说明

在 MIX 编程模式中,AIV(Vector 核)调用 Matmul 的 Iterate 接口向 AIC(Cube 核)发送计算任务。同步模式下(sync=true),每次调用都会阻塞 AIV 等待 AIC 完成,产生大量同步等待时间。异步模式下(sync=false),AIV 发送任务后立即返回继续执行后续指令,AIC 独立完成计算,两者并行工作。

优化路径

  1. Iterate<sync=true> 替换为 Iterate<sync=false>
  2. 仅在真正需要使用 Cube 计算结果的位置插入同步指令。
  3. 用仿真流水图验证 AIC 和 AIV 的并行度是否提升。

简单案例

某 Matmul+BiasAdd 融合算子中,Matmul 用 Cube 核完成,BiasAdd 用 Vector 核完成。同步模式下 AIV 空闲等待 AIC,总耗时 100us。切换为异步模式后,AIV 在 AIC 做 Matmul 的同时预先搬入 Bias 数据,总耗时降至 78us,性能提升约 22%。


6.4 减少 PipeBarrier 的使用

原理说明

PipeBarrier<PIPE_ALL> 会强制所有流水线同步等待,即所有已发射的指令全部完成后才能继续。这会彻底打断流水并行,造成所有执行单元短暂停滞。实际上多数场景只需要在特定流水线之间同步,不需要全局同步。

优化路径

  1. 审查代码中所有 PipeBarrier<PIPE_ALL>,评估是否可以替换为更精细的同步。
  2. 仅在确实涉及多条流水线数据依赖时使用 PIPE_ALL
  3. 仅涉及 Vector 流水时使用 PipeBarrier<PIPE_V>,仅涉及 MTE2 时使用 PipeBarrier<PIPE_MTE2>

简单案例

某算子在连续两次 Vector 计算之间插入了 PipeBarrier<PIPE_ALL>,导致 MTE2 也被迫等待。将其改为 PipeBarrier<PIPE_V> 后,MTE2 可在 Vector 同步期间继续搬运下一批数据,算子耗时降低约 10%。


7. Tiling 优化

7.1 L2 Cache 切分策略

原理说明

AI 处理器的 L2 Cache(典型大小约 192MB)读写带宽约 7TB/s,远高于 GM 的约 1.6TB/s。当所有核同时处理的数据总量超过 L2 Cache 容量时,数据无法命中 L2 Cache,被迫从 GM 读写,带宽利用率大幅下降。通过在 Tiling 中将数据按 L2 Cache 大小分批处理,确保每批数据能驻留在 L2 Cache 中,可大幅提升搬运效率。

优化路径

  1. 计算算子输入输出数据的总量,与 L2 Cache 大小比较。
  2. 若总量超过 L2 Cache,将数据按 L2 Cache 容量切分为若干份。
  3. 核函数接收切分索引参数,Host 侧循环调用核函数处理每份数据。
  4. 确保每份数据量 ≤ L2 Cache 大小。

简单案例

输入数据 384MB,L2 Cache 为 192MB,总核数 20。将数据切分为 2 份,每份 192MB:

constexpr int32_t TILE_NUM = 2;
for (int i = 0; i < TILE_NUM; i++) {
    KernelSample op;
    op.Init(x, y, i);  // 第 i 份数据的偏移
    op.Process();
}

每次 20 个核处理 192MB 数据,全部可命中 L2 Cache,避免了 GM 访问瓶颈。


7.2 核间负载均衡

原理说明

L2 Cache 切分后,每份数据需要分配给多个核处理。若数据量不能被核数整除,会出现"拖尾"现象——部分核需要处理额外的数据块,而其他核已经完成并空闲等待。算子的总耗时取决于最慢的核,因此负载不均衡会直接浪费计算资源。

优化路径

  1. 计算每份数据量与核数的整除关系,调整切分份数使其能被核数整除。
  2. 对于 causal 等不规则场景,按有效计算块的数量(而非行数)来分核。
  3. 可适当增加切分份数,牺牲少量 L2 Cache 命中率换取更好的负载均衡。

简单案例

FlashAttention causal 场景下,atten_mask 为下三角。若按行分核(8 行分 9 核),第 1 个核仅计算 1 个有效块,第 8 个核需要计算 8 个有效块,负载极度不均。改为统计有效块总数(共 36 个),按块均匀分给各核(每核 4 个块),负载均衡后整体性能提升显著。


7.3 基本块大小调整

原理说明

每次循环迭代有固定的指令发射、流水启停和同步开销,迭代次数越多总开销越大。在 UB 空间允许的范围内,增大基本块意味着减少迭代次数,从而减少这些固定开销。同时更大的基本块也有利于 Vector/Cube 单元的计算效率。

优化路径

  1. 用 Profiling 数据检查 UB 空间是否有富余(是否用满)。
  2. 在 UB 空间允许范围内,增大 tileLength 或基本块尺寸。
  3. 验证增大基本块后算子功能正确性。
  4. 比较优化前后的迭代次数和总耗时。

简单案例

FlashAttention 算子优化前按 (64, 128) 切分基本块,总共需循环 32 次。检查发现 UB 空间远未用满,将基本块调整为 (128, 128),循环次数减至 16 次,算子性能提升约一倍。


7.4 Matmul 基本块与 MDL 模板优化

原理说明

Matmul 计算中,baseM、baseN、baseK 参数决定了搬运数据的方式和总量。搬运总数据量为:totalCnt = (N/baseN)×M×K + (M/baseM)×K×N。选择不同的 baseM/baseN 组合直接影响搬运效率。此外,MDL(Multi-Data Loading)模板可使 MTE2 一次搬入多个基本块到 A1/B1,后续 MTE1 复用缓存数据,减少搬运次数和流水等待。

优化路径

  1. 根据搬运量公式,选择使 (1/baseM + 1/baseN) 最小的基本块组合。
  2. 在 Matmul 高阶 API 中使能 MDL 模板。
  3. 用 Profiling 数据重点分析 MTE2 流水耗时变化。

简单案例

M=4096, N=5120, K=4096 的 Matmul 算子。使能 MDL 模板后,MTE2 一次搬入多个基本块,搬运次数减少,MTE2 平均耗时从 75.64us 降至 46.24us。一个基本块的计算延迟从 10.899us 降至 5.198us(流水等待大幅减少),算子总耗时从 83.68us 降至 53.4us,性能提升约 36%。


8. 头开销与指令优化

8.1 设置合适的核数和 Kernel 类型

原理说明

每个核在启动时有固定的初始化开销(核启动、TLB 加载、变量初始化等),且核数越多总头开销越大。对于整体耗时在微秒级、单核计算量很小的算子,头开销在总耗时中占比可能很高。同时,若纯 Vector 算子以混合模式启动,Cube 核虽无实际计算却仍产生启动开销,造成浪费。

优化路径

  1. 用 Profiling 数据分析头开销占总耗时的比例。
  2. 若头开销占比高,尝试减少启动核数(增加单核计算量)来找到最佳平衡点。
  3. 纯 Vector 算子设置为 Vector Kernel 类型,纯 Cube 算子设置为 Cube Kernel 类型,避免启动不必要的核。
  4. 通过多次实测不同核数配置,确定性能最优的核数设置。

简单案例

某纯 Vector 算子总耗时 8us,其中头开销 5us(占 62%)。将启动核数从 20 减少到 10,单核计算量翻倍,头开销降至 3us,总耗时降至 6us(计算量 3us),性能提升约 25%。同时将 Kernel 类型从混合模式改为纯 Vector 模式,避免 Cube 核的无效启动,头开销进一步降低 0.5us。


8.2 数据类型与编译优化

原理说明

低精度数据类型(如 half 代替 float)可以减半搬运数据量并提高计算吞吐量,但类型转换本身有开销。编译器在编译 Scalar 代码时,若能识别常量和确定性变量,会进行常量折叠、传播和循环展开等优化,减少 Scalar 指令数。使用 constexprinline 等关键字可以辅助编译器做出更好的优化决策。

优化路径

  1. 评估算法精度容忍度,在可接受范围内使用低精度类型。
  2. 若 half 直接计算精度不达标,可尝试通过变换计算公式避免精度敏感的运算。
  3. constexpr 标记编译期可确定的常量。
  4. 对热路径上的函数使用 __aicore__ inline 标记。

简单案例

某 Activation 算子中间计算使用 float32 类型,数据量 1MB。改为 half 类型后,搬运数据量减至 512KB,MTE2 耗时降低 45%。同时将 tileLength 等参数改为 constexpr,编译器将多条 Scalar 运算在编译期完成,Scalar 指令数减少约 20%。


9. MC² 通算融合优化(高级场景)

原理说明

MC² 通算融合算子将分布式训练中的通信任务(如 AllReduce)与计算任务(如 Matmul)融合为一个算子。核心思想是将输入数据沿某一维度切分为多个子块,子块的计算和通信形成两条独立的流水线。当第一块数据做完计算进入通信阶段时,第二块数据可以同时开始计算,通过这种流水掩盖使通信开销被计算所覆盖(或反之),从而提升整体性能。

优化路径

  1. 分别测量未融合时计算和通信的独立耗时,判断算子是计算 bound 还是通信 bound。
  2. 根据 bound 类型制定切分策略——计算 bound 时让通信被掩盖,通信 bound 时让计算被掩盖。
  3. 在 Tiling 代码中设置切分数量和子块大小。
  4. 结合实测微调切分策略(过多切块会带来额外膨胀开销)。

简单案例

MatmulAllreduce 算子,左矩阵 [M, K],右矩阵 [K, N],输入 half 类型。未融合时 Matmul 耗时 1200us,AllReduce 耗时 674us,串行总耗时 1874us。融合后将 M 轴切分为 4 块,计算和通信流水并行执行,融合算子总耗时 1262us,性能收益 32.7%

参考资料

SIMD算子性能优化:https://www.hiascend.com/document/detail/zh/canncommercial/850/opdevg/Ascendcopdevg/atlas_ascendc_best_practices_10_00010.html

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐