前言

昇腾NPU作为华为自研的AI处理器,在深度学习推理和训练场景中展现出强大的计算能力。CANN(Compute Architecture for Neural Networks)是昇腾AI处理器的异构计算架构,为开发者提供了从底层算子开发到上层应用部署的完整工具链。Ascend Development Kit作为CANN生态中的一站式开发套件,集成了SDK、文档、示例代码和工具链,大幅降低了开发者上手昇腾NPU应用的门槛。本指南通过一个完整的图像分类模型推理流程,展示Ascend Development Kit在实际项目中的应用方法,帮助开发者理解如何利用这套工具链加速AI应用的开发和部署。

环境准备与套件安装

在开始实际开发之前,需要完成Ascend Development Kit的环境搭建工作。开发环境包括硬件和软件两个层面:硬件层面需要昇腾NPU设备(如Atlas 200 DK开发者套件或Atlas训练服务器),软件层面则需要安装CANN软件栈和相关的开发工具。

软件环境的安装从获取Ascend Development Kit开始。通过Git克隆仓库获取完整的开发套件源码:

git clone https://atomgit.com/cann/asc-devkit.git
cd asc-devkit

克隆完成后,目录结构中包含多个子模块:samples目录存放示例代码,docs目录包含开发文档,tools目录提供辅助工具。每个模块都针对特定的开发场景提供了完整的参考实现。

环境配置的核心是安装CANN软件栈。CANN提供了多个组件,包括NN推理引擎、计算图引擎、数字视觉预处理模块等。安装过程通过运行套件提供的安装脚本完成:

bash scripts/install_cann.sh --version 8.0.RC1

Install CANN stack with specific version to ensure compatibility with samples.

安装脚本会自动检测系统环境,下载并安装所需的依赖包。安装完成后,需要配置环境变量使开发工具链生效:

source /usr/local/Ascend/ascend-toolkit/set_env.sh
source ~/.bashrc

环境验证是确保安装成功的关键步骤。运行套件提供的环境检查工具:

python scripts/check_environment.py

该脚本会检查NPU设备连接状态、CANN组件版本、Python依赖等关键信息。所有检查项通过后,开发环境即可投入使用。

模型转换与离线推理实战

模型推理是AI应用的核心环节。在昇腾NPU上运行模型推理,需要将训练好的模型转换为适配昇腾硬件的格式。Ascend Development Kit提供了完整的模型转换工具链,支持主流深度学习框架的模型转换。

本节以一个ResNet50图像分类模型为例,演示从ONNX模型到昇腾离线模型的完整转换流程。模型转换的核心工具是ATC(Ascend Tensor Compiler),它能够将原始模型编译成适配昇腾NPU的离线模型文件。

转换前的准备工作包括获取原始模型文件和准备测试数据。从模型仓库下载预训练的ResNet50 ONNX模型:

wget https://modelzoo.obs.cn-north-4.myhuaweicloud.com/official-models/ResNet50/resnet50.onnx

模型下载完成后,使用ATC工具进行模型转换。转换命令需要指定输入节点的形状信息:

atc --model=resnet50.onnx \
    --framework=5 \
    --output=resnet50_om \
    --input_shape="input:1,3,224,224" \
    --soc_version=Ascend310P3

Convert ONNX model to Ascend offline model format with specified input shape for batch inference.

命令参数的含义如下:–framework=5表示输入模型为ONNX格式;–output指定输出模型文件名;–input_shape定义输入张量的维度;–soc_version指定目标昇腾处理器型号。

转换过程会生成.om格式的离线模型文件,该文件包含了昇腾NPU执行推理所需的全部信息,包括计算图结构、算子参数和内存布局。

模型推理的实现通过Ascend Development Kit提供的Python SDK完成。推理代码的核心流程包括模型加载、输入数据准备、推理执行和结果解析四个阶段。以下代码展示了完整的推理实现:

import acl
import numpy as np
from PIL import Image

class ResNet50Inferencer:
    def __init__(self, model_path):
        self.device_id = 0
        acl.init()
        self.context = acl.rt.create_context(self.device_id)
        self.load_model(model_path)
    
    def load_model(self, model_path):
        self.model_id, _ = acl.mdl.load_from_file(model_path)
        self.model_desc = acl.mdl.create_desc()
        acl.mdl.get_desc(self.model_desc, self.model_id)
    
    def preprocess(self, image_path):
        img = Image.open(image_path).resize((224, 224))
        img_array = np.array(img, dtype=np.float32)
        img_array = (img_array - 127.5) / 127.5
        return np.expand_dims(img_array.transpose(2, 0, 1), axis=0)
    
    def infer(self, input_data):
        input_dataset = acl.create_dataset()
        input_buffer = acl.create_buffer(input_data.tobytes())
        acl.add_buffer_to_dataset(input_dataset, input_buffer)
        
        output_dataset = acl.create_dataset()
        acl.mdl.execute(self.model_id, input_dataset, output_dataset)
        
        return acl.get_dataset_buffer(output_dataset, 0)
    
    def cleanup(self):
        acl.mdl.unload(self.model_id)
        acl.rt.destroy_context(self.context)
        acl.finalize()

inferencer = ResNet50Inferencer("resnet50_om.om")
input_data = inferencer.preprocess("test_image.jpg")
output = inferencer.infer(input_data)

推理代码中,ACL(Ascend Computing Language)是昇腾计算语言接口,提供了底层硬件访问能力。初始化阶段调用acl.init()初始化ACL运行时,创建计算上下文。模型加载通过acl.mdl.load_from_file()将离线模型加载到设备内存。预处理阶段将输入图像调整为模型所需的尺寸和归一化格式。推理执行通过acl.mdl.execute()启动NPU计算,结果通过输出数据集获取。

结果解析阶段需要将模型输出转换为可读的类别标签。ResNet50输出1000个类别的概率分布,选择概率最高的类别作为最终预测结果:

def parse_result(output_buffer):
    result = np.frombuffer(output_buffer, dtype=np.float32)
    top5_indices = np.argsort(result)[-5:][::-1]
    with open("imagenet_labels.txt") as f:
        labels = f.read().splitlines()
    for idx in top5_indices:
        print(f"Class: {labels[idx]}, Probability: {result[idx]:.4f}")

结果解析函数从输出缓冲区读取概率分布,使用numpy的argsort函数获取概率最高的五个类别索引,然后从标签文件中读取对应的类别名称并打印。

性能调优与问题诊断

模型推理的初始实现通常无法达到最优性能。Ascend Development Kit提供了丰富的性能分析和调优工具,帮助开发者定位性能瓶颈并进行针对性优化。

性能分析的第一步是使用Profiling工具采集推理过程中的性能数据。开启Profiling功能需要在推理代码中添加相关调用:

prof_config = acl.prof.create_config(
    mode=acl.prof.PROF_MODE_API,
    devices=[0]
)
acl.prof.start(prof_config)

inferencer.infer(input_data)

acl.prof.stop()
acl.prof.destroy_config(prof_config)

Profiling数据采集完成后,使用Ascend Development Kit提供的可视化分析工具查看性能报告:

msprof --export=profiling_result.csv --analysis=profiling_output/

分析工具生成的报告包含多个维度的性能指标:算子执行时间、内存拷贝耗时、设备利用率等。通过分析这些指标,可以定位到影响性能的关键因素。

常见的性能瓶颈包括数据预处理耗时过长、模型输入输出拷贝开销大、算子执行效率低等。针对不同瓶颈,Ascend Development Kit提供了对应的优化方案。

数据预处理优化可以通过DVPP(Digital Vision Pre-Processing)模块实现硬件加速。DVPP是昇腾NPU内置的图像视频处理单元,能够以硬件方式完成图像解码、缩放、格式转换等操作,大幅降低CPU负载。以下代码展示了使用DVPP进行图像预处理的方法:

import acl.dvpp as dvpp

class DVPPPreprocessor:
    def __init__(self):
        self.dvpp_channel = dvpp.create_channel()
        dvpp.init_channel(self.dvpp_channel)
    
    def process(self, image_path):
        # Decode image using hardware decoder
        decode_output = dvpp.jpeg_decode(self.dvpp_channel, image_path)
        
        # Resize using hardware resizer
        resize_config = dvpp.resize_config(
            src_width=decode_output.width,
            src_height=decode_output.height,
            dst_width=224,
            dst_height=224
        )
        resize_output = dvpp.resize(self.dvpp_channel, decode_output, resize_config)
        
        return resize_output

使用DVPP进行预处理后,图像处理吞吐量可以从每秒数百张提升到数千张,性能提升明显。

模型输入输出的内存拷贝优化可以通过内存复用和异步传输实现。Ascend NPU支持设备内存和主机内存之间的异步拷贝,在推理计算的同时进行数据传输,隐藏数据搬移延迟:

def async_infer(self, input_data):
    # Allocate device memory
    device_mem = acl.rt.malloc_host(input_data.nbytes)
    
    # Async copy input to device
    stream = acl.rt.create_stream()
    acl.rt.memcpy_async(device_mem, input_data.tobytes(), stream)
    
    # Execute inference after copy completes
    acl.rt.synchronize_stream(stream)
    acl.mdl.execute(self.model_id, input_dataset, output_dataset)
    
    return output

异步拷贝通过创建计算流并在流上执行内存传输操作,然后通过流同步确保数据传输完成后再执行推理计算。

算子执行效率优化需要分析具体算子的性能表现。对于性能较差的算子,可以考虑使用Ascend Development Kit提供的自定义算子开发框架进行优化实现。自定义算子开发支持C++和Tik两种开发模式,分别适用于不同复杂度的算子实现需求。

多模型并发部署实践

实际应用场景中,往往需要在同一设备上部署多个模型,实现不同功能的组合。Ascend Development Kit提供了完善的多模型管理机制,支持在同一设备上并发运行多个模型实例。

多模型部署的核心是模型实例管理。每个加载的模型实例占用一定的设备内存资源,需要合理规划内存分配以避免资源冲突。Ascend Development Kit的内存管理机制支持静态内存池和动态内存池两种模式,静态内存池在模型加载时预分配,动态内存池在推理过程中按需申请。

以下代码展示了多模型并发加载和推理的实现:

class MultiModelManager:
    def __init__(self, device_id=0):
        acl.init()
        self.device_id = device_id
        self.context = acl.rt.create_context(device_id)
        self.models = {}
    
    def load_model(self, name, model_path):
        model_id, _ = acl.mdl.load_from_file(model_path)
        self.models[name] = model_id
        print(f"Model {name} loaded with ID {model_id}")
    
    def infer(self, model_name, input_data):
        if model_name not in self.models:
            raise ValueError(f"Model {model_name} not loaded")
        
        model_id = self.models[model_name]
        # Execute inference
        acl.mdl.execute(model_id, input_dataset, output_dataset)
        return output
    
    def unload_model(self, name):
        if name in self.models:
            acl.mdl.unload(self.models[name])
            del self.models[name]

manager = MultiModelManager()
manager.load_model("resnet50", "resnet50_om.om")
manager.load_model("yolov5", "yolov5_om.om")

多模型并发执行需要考虑资源竞争问题。当多个模型同时请求设备资源时,Ascend调度器会根据优先级和到达顺序进行调度。开发者可以通过设置模型优先级来控制调度顺序:

priority = acl.mdl.PRIORITY_HIGH
acl.mdl.set_priority(model_id, priority)

多模型场景下的内存管理是另一个关键问题。不同模型对内存的需求不同,需要根据实际推理batch size和输入输出尺寸计算内存需求。Ascend Development Kit提供了内存估算工具:

atc --mode=1 --om=resnet50_om.om --output=memory_analysis.txt

该工具输出的内存分析报告包含各模型运行所需的设备内存大小、输入输出缓冲区大小等信息,帮助开发者合理规划内存资源。

应用集成与部署方案

完成模型开发和测试后,推进步骤是将推理能力集成到实际应用中。Ascend Development Kit支持多种集成方式,包括Python API集成、C++ API集成和容器化部署。

Python API集成适用于快速原型开发和轻量级应用场景。通过pip安装ACL Python包后,即可在Python应用中直接调用推理接口。以下是一个简单的Flask服务示例,将模型推理能力封装为HTTP接口:

from flask import Flask, request, jsonify
import acl

app = Flask(__name__)

@app.route('/infer', methods=['POST'])
def infer():
    image_file = request.files['image']
    image_path = f"/tmp/{image_file.filename}"
    image_file.save(image_path)
    
    input_data = inferencer.preprocess(image_path)
    output = inferencer.infer(input_data)
    result = parse_result(output)
    
    return jsonify({"predictions": result})

if __name__ == '__main__':
    inferencer = ResNet50Inferencer("resnet50_om.om")
    app.run(host='0.0.0.0', port=5000)

C++ API集成适用于对性能要求较高的生产环境。C++ API提供了更底层的控制能力和更低的调用开销。编译C++推理程序需要链接ACL库和相关依赖:

g++ -o infer_demo infer_demo.cpp \
    -I/usr/local/Ascend/ascend-toolkit/latest/include \
    -L/usr/local/Ascend/ascend-toolkit/latest/lib64 \
    -lascendcl -lacl_op_model -lge_runner

容器化部署是现代云原生应用的主流部署方式。Ascend Development Kit提供了Docker镜像构建支持,可以将推理应用打包为容器镜像进行分发和部署。Dockerfile示例:

FROM ascendhub.huawei.com/public-ascendhub/ascend-toolkit:latest
COPY resnet50_om.om /app/models/
COPY infer_app.py /app/
WORKDIR /app
CMD ["python", "infer_app.py"]

容器运行时需要配置昇腾NPU设备映射,使容器能够访问宿主机的NPU设备:

docker run --device=/dev/davinci0 \
           --device=/dev/davinci_manager \
           -v /usr/local/Ascend/driver:/usr/local/Ascend/driver \
           -p 5000:5000 \
           ascend-infer-app

通过容器化部署,推理应用可以在Kubernetes等容器编排平台上实现弹性伸缩和高可用部署,满足生产环境的稳定性和扩展性要求。

效率对比分析

维度 使用前 使用后 差异来源
环境搭建时间 4-6小时 30-60分钟 一键安装脚本自动化配置流程
模型转换成功率 约60%手动调试 95%以上自动转换 ATC工具内置兼容性处理
推理性能调优周期 1-2周反复测试 2-3天完成优化 Profiling工具精准定位瓶颈
多模型部署复杂度 需要手动管理资源冲突 框架自动调度管理 内存池和任务队列机制
文档查阅效率 分散在多个文档库 集中在统一套件仓库 文档与代码同步更新

错误处理与调试技巧

开发过程中难免遇到各种错误和异常情况。Ascend Development Kit提供了完善的错误处理机制和调试工具链,帮助开发者快速定位和解决问题。

ACL接口的错误处理遵循统一的返回值模式。每个API调用都会返回错误码,开发者需要检查返回值以确定操作是否成功:

ret = acl.mdl.execute(model_id, input_dataset, output_dataset)
if ret != acl.ACL_SUCCESS:
    error_msg = acl.get_error_message()
    print(f"Inference failed with error: {error_msg}")
    # Handle error or raise exception

常见的错误类型包括内存分配失败、模型加载失败、输入输出尺寸不匹配等。针对不同类型的错误,Ascend Development Kit提供了对应的诊断工具和解决建议。

内存相关错误通常表现为内存不足或内存访问越界。使用内存分析工具可以查看当前的内存使用情况:

npu-smi info -t memory -i 0

该命令输出设备内存的总量、已用量和使用率等信息,帮助判断是否存在内存泄漏或内存不足问题。

模型加载失败的常见原因包括模型文件损坏、模型格式不支持、版本兼容性问题等。通过ATC工具的详细日志可以定位具体原因:

atc --model=model.onnx --output=model \
    --framework=5 --soc_version=Ascend310P3 \
    --log=debug --display_error=1

开启详细日志后,ATC会输出模型解析和编译过程中的详细信息,包括每个算子的处理状态和可能的错误原因。

运行时错误可以通过日志系统进行追踪。Ascend Development Kit支持多级别的日志输出,从INFO级别到DEBUG级别,开发者可以根据需要调整日志级别:

acl.set_log_level(acl.LOG_DEBUG)

日志输出会记录推理过程中的关键事件,包括内存分配、算子执行、数据传输等,便于追踪问题发生的具体环节。

持续集成与自动化测试

生产级应用需要建立完善的持续集成和自动化测试流程。Ascend Development Kit提供了测试框架支持,帮助开发者构建可靠的测试体系。

单元测试针对推理流程的各个组件进行独立测试。测试用例应该覆盖正常场景和边界场景,包括:

import unittest

class TestInference(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.inferencer = ResNet50Inferencer("resnet50_om.om")
    
    def test_preprocess(self):
        # Test image preprocessing
        input_data = self.inferencer.preprocess("test_image.jpg")
        self.assertEqual(input_data.shape, (1, 3, 224, 224))
    
    def test_infer(self):
        # Test inference execution
        input_data = self.inferencer.preprocess("test_image.jpg")
        output = self.inferencer.infer(input_data)
        self.assertIsNotNone(output)
    
    def test_invalid_input(self):
        # Test error handling for invalid input
        with self.assertRaises(ValueError):
            self.inferencer.preprocess("nonexistent.jpg")

if __name__ == '__main__':
    unittest.main()

性能测试需要评估推理延迟和吞吐量等关键指标。使用Ascend Development Kit提供的基准测试工具可以获取标准化的性能数据:

import time

def benchmark(inferencer, image_path, iterations=1000):
    latencies = []
    input_data = inferencer.preprocess(image_path)
    
    # Warm-up
    for _ in range(10):
        inferencer.infer(input_data)
    
    # Benchmark
    for _ in range(iterations):
        start = time.time()
        inferencer.infer(input_data)
        latencies.append(time.time() - start)
    
    avg_latency = sum(latencies) / len(latencies) * 1000
    throughput = 1.0 / (avg_latency / 1000)
    print(f"Average latency: {avg_latency:.2f}ms")
    print(f"Throughput: {throughput:.1f} FPS")

持续集成流程可以将测试自动化,每次代码提交后自动运行测试套件。以下是一个简单的CI配置示例:

# .gitlab-ci.yml
stages:
  - test
  - benchmark

unit_test:
  stage: test
  script:
    - python -m pytest tests/unit/
  tags:
    - ascend-npu

performance_test:
  stage: benchmark
  script:
    - python benchmark.py --iterations 1000
  artifacts:
    reports:
      performance: performance.json

通过持续集成,可以确保代码质量和性能不会因为后续修改而退化,为应用的稳定运行提供保障。


模型转换与离线推理实战

模型推理是AI应用的核心环节。在昇腾NPU上运行模型推理,需要将训练好的模型转换为适配昇腾硬件的格式。Ascend Development Kit提供了完整的模型转换工具链,支持主流深度学习框架的模型转换。

本节以一个ResNet50图像分类模型为例,演示从ONNX模型到昇腾离线模型的完整转换流程。模型转换的核心工具是ATC(Ascend Tensor Compiler),它能够将原始模型编译成适配昇腾NPU的离线模型文件。

转换前的准备工作包括获取原始模型文件和准备测试数据。从模型仓库下载预训练的ResNet50 ONNX模型:

wget https://modelzoo.obs.cn-north-4.myhuaweicloud.com/official-models/ResNet50/resnet50.onnx

模型下载完成后,使用ATC工具进行模型转换。转换命令需要指定输入节点的形状信息:

atc --model=resnet50.onnx \
    --framework=5 \
    --output=resnet50_om \
    --input_shape="input:1,3,224,224" \
    --soc_version=Ascend310P3

命令参数的含义如下:–framework=5表示输入模型为ONNX格式;–output指定输出模型文件名;–input_shape定义输入张量的维度;–soc_version指定目标昇腾处理器型号。

转换过程会生成.om格式的离线模型文件,该文件包含了昇腾NPU执行推理所需的全部信息,包括计算图结构、算子参数和内存布局。

模型推理的实现通过Ascend Development Kit提供的Python SDK完成。推理代码的核心流程包括模型加载、输入数据准备、推理执行和结果解析四个阶段。以下代码展示了完整的推理实现:

import acl
import numpy as np
from PIL import Image

class ResNet50Inferencer:
    def __init__(self, model_path):
        self.device_id = 0
        acl.init()
        self.context = acl.rt.create_context(self.device_id)
        self.load_model(model_path)
    
    def load_model(self, model_path):
        self.model_id, _ = acl.mdl.load_from_file(model_path)
        self.model_desc = acl.mdl.create_desc()
        acl.mdl.get_desc(self.model_desc, self.model_id)
    
    def preprocess(self, image_path):
        img = Image.open(image_path).resize((224, 224))
        img_array = np.array(img, dtype=np.float32)
        img_array = (img_array - 127.5) / 127.5
        return np.expand_dims(img_array.transpose(2, 0, 1), axis=0)
    
    def infer(self, input_data):
        input_dataset = acl.create_dataset()
        input_buffer = acl.create_buffer(input_data.tobytes())
        acl.add_buffer_to_dataset(input_dataset, input_buffer)
        
        output_dataset = acl.create_dataset()
        acl.mdl.execute(self.model_id, input_dataset, output_dataset)
        
        return acl.get_dataset_buffer(output_dataset, 0)
    
    def cleanup(self):
        acl.mdl.unload(self.model_id)
        acl.rt.destroy_context(self.context)
        acl.finalize()

inferencer = ResNet50Inferencer("resnet50_om.om")
input_data = inferencer.preprocess("test_image.jpg")
output = inferencer.infer(input_data)

推理代码中,ACL(Ascend Computing Language)是昇腾计算语言接口,提供了底层硬件访问能力。初始化阶段调用acl.init()初始化ACL运行时,创建计算上下文。模型加载通过acl.mdl.load_from_file()将离线模型加载到设备内存。预处理阶段将输入图像调整为模型所需的尺寸和归一化格式。推理执行通过acl.mdl.execute()启动NPU计算,结果通过输出数据集获取。

结果解析阶段需要将模型输出转换为可读的类别标签。ResNet50输出1000个类别的概率分布,选择概率最高的类别作为最终预测结果:

def parse_result(output_buffer):
    result = np.frombuffer(output_buffer, dtype=np.float32)
    top5_indices = np.argsort(result)[-5:][::-1]
    with open("imagenet_labels.txt") as f:
        labels = f.read().splitlines()
    for idx in top5_indices:
        print(f"Class: {labels[idx]}, Probability: {result[idx]:.4f}")

结果解析函数从输出缓冲区读取概率分布,使用numpy的argsort函数获取概率最高的五个类别索引,然后从标签文件中读取对应的类别名称并打印。

效率对比分析

维度 使用前 使用后 差异来源
环境搭建时间 4-6小时 30-60分钟 一键安装脚本自动化配置流程
模型转换成功率 约60%手动调试 95%以上自动转换 ATC工具内置兼容性处理
推理性能调优周期 1-2周反复测试 2-3天完成优化 Profiling工具精准定位瓶颈
多模型部署复杂度 需要手动管理资源冲突 框架自动调度管理 内存池和任务队列机制
文档查阅效率 分散在多个文档库 集中在统一套件仓库 文档与代码同步更新

错误处理与调试技巧

开发过程中难免遇到各种错误和异常情况。Ascend Development Kit提供了完善的错误处理机制和调试工具链,帮助开发者快速定位和解决问题。

ACL接口的错误处理遵循统一的返回值模式。每个API调用都会返回错误码,开发者需要检查返回值以确定操作是否成功:

ret = acl.mdl.execute(model_id, input_dataset, output_dataset)
if ret != acl.ACL_SUCCESS:
    error_msg = acl.get_error_message()
    print(f"Inference failed with error: {error_msg}")
    # Handle error or raise exception

常见的错误类型包括内存分配失败、模型加载失败、输入输出尺寸不匹配等。针对不同类型的错误,Ascend Development Kit提供了对应的诊断工具和解决建议。

内存相关错误通常表现为内存不足或内存访问越界。使用内存分析工具可以查看当前的内存使用情况:

npu-smi info -t memory -i 0

该命令输出设备内存的总量、已用量和使用率等信息,帮助判断是否存在内存泄漏或内存不足问题。

模型加载失败的常见原因包括模型文件损坏、模型格式不支持、版本兼容性问题等。通过ATC工具的详细日志可以定位具体原因:

atc --model=model.onnx --output=model \
    --framework=5 --soc_version=Ascend310P3 \
    --log=debug --display_error=1

开启详细日志后,ATC会输出模型解析和编译过程中的详细信息,包括每个算子的处理状态和可能的错误原因。

运行时错误可以通过日志系统进行追踪。Ascend Development Kit支持多级别的日志输出,从INFO级别到DEBUG级别,开发者可以根据需要调整日志级别:

acl.set_log_level(acl.LOG_DEBUG)

日志输出会记录推理过程中的关键事件,包括内存分配、算子执行、数据传输等,便于追踪问题发生的具体环节。


仓库地址:https://atomgit.com/cann/asc-devkit

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐