摘要

华为昇腾 AI 计算架构(CANN,Compute Architecture for Neural Networks)是面向 AI 场景的异构计算架构,旨在打通 AI 应用开发的 “端 - 边 - 云” 全流程,提供高效的算子调度、内存管理与硬件加速能力。本文从学术视角出发,系统梳理 CANN 的核心概念、架构设计、开发流程与实践案例,结合代码演示与官方资源链接,为 AI 开发者(尤其是大一新生入门)提供从理论到工程落地的完整指南。

1 引言:CANN 的定位与核心价值

在 AI 算力需求爆发的背景下,异构计算(CPU+AI 加速器)已成为主流方案,但不同硬件架构的适配复杂性、算子效率低下、内存开销过高等问题,严重制约了 AI 应用的落地效率。华为 CANN 架构应运而生,其核心价值体现在三个维度:

  1. 硬件解耦:通过统一的 API 层屏蔽底层 Ascend AI 处理器(如 Ascend 310/910)的硬件差异,开发者无需关注具体硬件细节即可实现跨平台部署;
  2. 效率优化:内置高性能算子库(如 Matrix Matrix Multiplication 算子)、自动算子生成工具(TBE,Tensor Boost Engine)与内存优化机制,将 AI 任务的计算效率提升 30% 以上(数据来源:华为昇腾官方白皮书^1);
  3. 生态协同:深度兼容 MindSpore、TensorFlow、PyTorch 等主流 AI 框架,同时提供 ModelZoo(预置 1000 + 优化模型)与工具链,降低开发门槛。

学术延伸:CANN 的设计理念与 NVIDIA 的 CUDA 架构类似,但更侧重 “全场景 AI”,其异构计算架构模型已被《Journal of Parallel and Distributed Computing》等期刊收录相关研究论文 [^2]。

2 CANN 核心概念与架构设计

2.1 核心术语定义

在深入架构前,需明确 CANN 生态中的关键术语(新手必掌握):

  • Ascend AI 处理器:CANN 架构的硬件载体,分为训练型(如 Ascend 910,支持千亿参数模型训练)与推理型(如 Ascend 310,面向边缘端推理);
  • Runtime:CANN 的运行时核心,负责任务调度、内存管理与硬件资源分配,是连接应用层与硬件层的桥梁;
  • TBE 算子:基于 Tensor Boost Engine 开发的高性能算子,支持自动微分、混合精度计算,是 CANN 效率优化的核心;
  • OM 文件:CANN 的模型文件格式,由原始 AI 模型(如 ONNX、MindIR)通过 ATC 工具(Ascend Tensor Compiler)编译生成,包含优化后的算子序列与内存布局;
  • ACL:Ascend Computing Language,CANN 的应用开发接口,提供 C/C++/Python 三种语言绑定,是开发者调用 CANN 能力的主要方式。

2.2 四层架构设计(从下到上)

CANN 采用分层解耦的架构设计,每层职责明确,具体如下(参考华为昇腾官方架构图 [^3]):

架构层 核心功能 面向角色 关键工具 / 接口
硬件层 提供 AI 计算算力(如 AI Core、AI CPU) 硬件工程师 芯片驱动程序
驱动层 硬件抽象与资源管理 驱动开发者 Driver SDK
框架层 算子库、Runtime、编译优化 算法工程师 / 开发者 CANN SDK、ATC、TBE
应用层 AI 应用开发(训练 / 推理) 应用开发者 ACL、MindSpore/TensorFlow 适配层

学术重点:CANN 的框架层采用 “编译 - 运行” 两阶段优化模型:

  1. 编译时(离线):通过 ATC 工具将原始模型转换为 OM 文件,完成算子融合、内存规划、指令调度优化;
  2. 运行时(在线):Runtime 根据 OM 文件的优化信息,动态分配硬件资源,实现算子的并行执行。

3 CANN 开发环境搭建(实战代码)

作为大一新生,首次接触 CANN 需从环境搭建开始。以下以Ubuntu 20.04 + CANN 7.0.RC1(稳定版)+ Ascend 310(推理卡) 为例,提供完整的搭建步骤与代码。

3.1 前置依赖安装

首先安装系统依赖与 Python 环境(建议 Python 3.8,CANN 对高版本 Python 兼容性待优化):

bash

运行

# 更新系统源
sudo apt update && sudo apt upgrade -y

# 安装依赖库
sudo apt install -y gcc g++ make cmake git python3.8 python3.8-dev python3.8-venv

# 创建并激活Python虚拟环境(避免环境冲突)
python3.8 -m venv cann-env
source cann-env/bin/activate  # Linux激活命令
# Windows激活命令:cann-env\Scripts\activate

# 安装Python依赖包
pip install --upgrade pip
pip install numpy==1.23.5 protobuf==3.20.3  # 版本需与CANN兼容

3.2 CANN SDK 下载与安装

  1. 前往华为昇腾官网下载 CANN SDK(需注册账号):CANN 下载中心[^4],选择 “7.0.RC1 -> 推理场景 -> Ubuntu 20.04 -> x86_64”;
  2. 解压安装包并执行安装脚本:

bash

运行

# 假设下载的安装包为Ascend-cann-toolkit_7.0.RC1_linux-x86_64.run
chmod +x Ascend-cann-toolkit_7.0.RC1_linux-x86_64.run

# 执行安装(默认安装路径/opt/ascend)
sudo ./Ascend-cann-toolkit_7.0.RC1_linux-x86_64.run --install

3.3 环境变量配置

安装完成后,需配置环境变量(建议写入~/.bashrc,避免每次重启终端重新配置):

bash

运行

# 打开bashrc文件
vim ~/.bashrc

# 在文件末尾添加以下内容(根据实际安装路径调整)
export ASCEND_HOME=/opt/ascend
export CANN_PATH=$ASCEND_HOME/ascend-toolkit/latest
export PATH=$CANN_PATH/bin:$CANN_PATH/python/site-packages/acl:$PATH
export LD_LIBRARY_PATH=$CANN_PATH/lib64:$LD_LIBRARY_PATH
export PYTHONPATH=$CANN_PATH/python/site-packages:$PYTHONPATH

# 生效环境变量
source ~/.bashrc

# 验证安装是否成功(输出CANN版本即成功)
ascend-dmi --version

注意:若未搭载实体 Ascend 硬件,可安装CANN 模拟器(Ascend Virtual NPU)进行开发调试,安装教程参考:CANN 模拟器使用指南[^5]。

4 CANN 核心 API 实践:基于 ACL 的图像推理

ACL(Ascend Computing Language)是 CANN 的核心应用接口,支持从模型加载、数据预处理到推理执行的全流程开发。以下以 “ResNet-50 图像分类” 为例,提供完整的 Python 代码实现(含注释),并解析关键步骤。

4.1 开发流程梳理

基于 ACL 的 AI 推理流程可分为 6 个步骤:

  1. 初始化 ACL 环境;
  2. 加载 OM 模型文件;
  3. 分配输入 / 输出内存(需使用 CANN 的内存管理接口,避免内存泄漏);
  4. 读取并预处理图像数据(如 Resize、Normalize);
  5. 执行模型推理;
  6. 解析推理结果并释放资源。

4.2 完整代码实现

python

运行

import acl
import cv2
import numpy as np

# -------------------------- 1. 全局参数定义 --------------------------
ACL_MEM_MALLOC_HUGE_FIRST = 0  # 内存分配策略:优先大页内存
ACL_MEMCPY_DEVICE_TO_HOST = 2  # 内存拷贝方向:设备端到主机端
MODEL_PATH = "./resnet50.om"   # OM模型路径(需提前用ATC编译)
INPUT_SIZE = (224, 224)        # ResNet-50输入图像尺寸
INPUT_MEAN = [0.485, 0.456, 0.406]  # 图像归一化均值
INPUT_STD = [0.229, 0.224, 0.225]   # 图像归一化标准差

# -------------------------- 2. ACL环境初始化 --------------------------
def init_acl():
    # 初始化ACL资源(指定当前进程绑定的设备ID,默认0)
    ret = acl.init()
    if ret != 0:
        raise Exception(f"ACL init failed, ret={ret}")
    
    # 获取当前设备ID
    device_id = 0
    ret = acl.rt.set_device(device_id)
    if ret != 0:
        raise Exception(f"Set device {device_id} failed, ret={ret}")
    
    # 创建上下文(Context):管理设备资源的核心对象
    context, ret = acl.rt.create_context(device_id)
    if ret != 0:
        raise Exception(f"Create context failed, ret={ret}")
    
    print("ACL environment initialized successfully")
    return device_id, context

# -------------------------- 3. 加载OM模型 --------------------------
def load_model(model_path):
    # 加载OM模型到内存
    model_id, ret = acl.mdl.load_from_file(model_path)
    if ret != 0:
        raise Exception(f"Load model {model_path} failed, ret={ret}")
    
    # 获取模型描述信息(如输入/输出数量、维度)
    model_desc = acl.mdl.create_desc()
    ret = acl.mdl.get_desc(model_desc, model_id)
    if ret != 0:
        raise Exception(f"Get model desc failed, ret={ret}")
    
    # 获取输入/输出数量
    input_num = acl.mdl.get_num_inputs(model_desc)
    output_num = acl.mdl.get_num_outputs(model_desc)
    print(f"Model loaded: input_num={input_num}, output_num={output_num}")
    
    return model_id, model_desc, input_num, output_num

# -------------------------- 4. 图像预处理 --------------------------
def preprocess_image(image_path):
    # 读取图像(BGR格式)
    img = cv2.imread(image_path)
    if img is None:
        raise Exception(f"Read image {image_path} failed")
    
    # 调整尺寸(保持比例,填充黑边)
    h, w = img.shape[:2]
    scale = min(INPUT_SIZE[0]/w, INPUT_SIZE[1]/h)
    new_w, new_h = int(w*scale), int(h*scale)
    img_resized = cv2.resize(img, (new_w, new_h))
    
    # 填充黑边(使图像尺寸为INPUT_SIZE)
    pad_left = (INPUT_SIZE[0] - new_w) // 2
    pad_right = INPUT_SIZE[0] - new_w - pad_left
    pad_top = (INPUT_SIZE[1] - new_h) // 2
    pad_bottom = INPUT_SIZE[1] - new_h - pad_top
    img_padded = cv2.copyMakeBorder(
        img_resized, pad_top, pad_bottom, pad_left, pad_right,
        cv2.BORDER_CONSTANT, value=(0, 0, 0)
    )
    
    # 格式转换:BGR -> RGB,HWC -> CHW
    img_rgb = cv2.cvtColor(img_padded, cv2.COLOR_BGR2RGB)
    img_chw = img_rgb.transpose(2, 0, 1)  # 维度从(224,224,3)转为(3,224,224)
    
    # 归一化:(img - mean) / std
    img_norm = (img_chw / 255.0 - np.array(INPUT_MEAN).reshape(3,1,1)) / np.array(INPUT_STD).reshape(3,1,1)
    
    # 数据类型转换:float32(CANN算子默认输入类型)
    img_float32 = img_norm.astype(np.float32)
    return img_float32

# -------------------------- 5. 模型推理 --------------------------
def infer_model(model_id, model_desc, input_data):
    # 1. 分配输入内存(设备端内存,需使用ACL接口)
    input_desc = acl.mdl.get_input_desc(model_desc, 0)  # 获取第一个输入的描述
    input_dtype = acl.mdl.get_data_type(input_desc)    # 获取输入数据类型
    input_size = acl.mdl.get_input_size_by_index(model_desc, 0)  # 获取输入数据大小(字节)
    
    # 分配设备端内存(ACL_MEM_MALLOC_HUGE_FIRST:优先大页内存,提升效率)
    input_device_ptr, ret = acl.rt.malloc(input_size, ACL_MEM_MALLOC_HUGE_FIRST)
    if ret != 0:
        raise Exception(f"Malloc input device memory failed, ret={ret}")
    
    # 2. 将主机端输入数据拷贝到设备端
    ret = acl.rt.memcpy(input_device_ptr, input_size, input_data.ctypes.data, input_size, ACL_MEMCPY_HOST_TO_DEVICE)
    if ret != 0:
        raise Exception(f"Copy input data to device failed, ret={ret}")
    
    # 3. 准备输入数据结构(ACL要求的输入列表格式)
    input_ptr_list = [input_device_ptr]
    input_size_list = [input_size]
    inputs = acl.mdl.create_dataset()
    for ptr, size in zip(input_ptr_list, input_size_list):
        data = acl.create_data_buffer(ptr, size)
        ret = acl.mdl.add_dataset_buffer(inputs, data)
        if ret != 0:
            raise Exception(f"Add input buffer to dataset failed, ret={ret}")
    
    # 4. 分配输出内存并准备输出数据结构
    outputs = acl.mdl.create_dataset()
    output_size_list = []
    output_device_ptr_list = []
    
    for i in range(acl.mdl.get_num_outputs(model_desc)):
        output_desc = acl.mdl.get_output_desc(model_desc, i)
        output_size = acl.mdl.get_output_size_by_index(model_desc, i)
        
        # 分配输出设备端内存
        output_device_ptr, ret = acl.rt.malloc(output_size, ACL_MEM_MALLOC_HUGE_FIRST)
        if ret != 0:
            raise Exception(f"Malloc output device memory failed, ret={ret}")
        
        # 添加到输出列表
        output_device_ptr_list.append(output_device_ptr)
        output_size_list.append(output_size)
        data = acl.create_data_buffer(output_device_ptr, output_size)
        ret = acl.mdl.add_dataset_buffer(outputs, data)
        if ret != 0:
            raise Exception(f"Add output buffer to dataset failed, ret={ret}")
    
    # 5. 执行模型推理
    ret = acl.mdl.execute(model_id, inputs, outputs)
    if ret != 0:
        raise Exception(f"Model inference failed, ret={ret}")
    
    # 6. 读取推理结果(从设备端拷贝到主机端)
    output_host_data = []
    for i in range(acl.mdl.get_num_outputs(model_desc)):
        output_buffer = acl.mdl.get_dataset_buffer(outputs, i)
        output_device_ptr = acl.data_buffer_get_addr(output_buffer)
        output_size = output_size_list[i]
        
        # 分配主机端内存用于存储输出结果
        output_host_ptr = acl.rt.malloc_host(output_size)
        if output_host_ptr is None:
            raise Exception(f"Malloc host memory for output failed")
        
        # 设备端 -> 主机端拷贝
        ret = acl.rt.memcpy(output_host_ptr, output_size, output_device_ptr, output_size, ACL_MEMCPY_DEVICE_TO_HOST)
        if ret != 0:
            raise Exception(f"Copy output data to host failed, ret={ret}")
        
        # 转换为numpy数组(ResNet-50输出为1000类的概率,形状为(1,1000))
        output_data = np.frombuffer(acl.util.ptr_to_bytes(output_host_ptr, output_size), dtype=np.float32).reshape(1, 1000)
        output_host_data.append(output_data)
        
        # 释放主机端内存
        acl.rt.free_host(output_host_ptr)
    
    # 7. 释放资源(避免内存泄漏)
    acl.mdl.destroy_dataset(inputs)
    acl.mdl.destroy_dataset(outputs)
    acl.rt.free(input_device_ptr)
    for ptr in output_device_ptr_list:
        acl.rt.free(ptr)
    
    return output_host_data

# -------------------------- 6. 主函数 --------------------------
def main(image_path):
    try:
        # 初始化ACL环境
        device_id, context = init_acl()
        
        # 加载OM模型
        model_id, model_desc, input_num, output_num = load_model(MODEL_PATH)
        
        # 图像预处理
        input_data = preprocess_image(image_path)
        
        # 模型推理
        output_data = infer_model(model_id, model_desc, input_data)
        
        # 解析结果(获取概率最大的类别索引)
        pred_label = np.argmax(output_data[0])
        pred_prob = np.max(output_data[0])
        print(f"Inference result: label={pred_label}, probability={pred_prob:.4f}")
        
    except Exception as e:
        print(f"Error: {str(e)}")
    
    finally:
        # 释放资源(无论推理成功与否,必须释放)
        if 'model_desc' in locals():
            acl.mdl.destroy_desc(model_desc)
        if 'model_id' in locals():
            acl.mdl.unload(model_id)
        if 'context' in locals():
            acl.rt.destroy_context(context)
        if 'device_id' in locals():
            acl.rt.reset_device(device_id)
        acl.finalize()
        print("ACL resources released successfully")

# 运行推理(替换为你的图像路径)
if __name__ == "__main__":
    main("./test_image.jpg")

4.3 关键步骤解析

  1. 内存管理:CANN 要求输入 / 输出内存必须通过acl.rt.malloc()(设备端)或acl.rt.malloc_host()(主机端)分配,不可使用 Python 原生np.zeros(),否则会导致设备无法访问内存;
  2. 模型编译:上述代码中的resnet50.om需通过 ATC 工具将 ONNX 模型转换,转换命令如下:

    bash

    运行

    atc --model=resnet50.onnx --framework=5 --output=resnet50 --input_format=NCHW --input_shape="actual_input_1:1,3,224,224" --log=info
    
    (参数说明:--framework=5表示 ONNX 框架,--input_shape指定输入批量与维度);
  3. 结果解析:ResNet-50 输出为 1000 个类别的概率,需结合 ImageNet 类别映射表(ImageNet 类别表[^6])获取具体类别名称。

5 CANN 进阶:TBE 算子开发(学术难点)

对于追求更高性能的开发者,CANN 提供 TBE(Tensor Boost Engine)工具链,支持自定义高性能算子。TBE 算子基于 TVM(Tensor Virtual Machine)优化框架,可通过自动调度生成硬件友好的指令序列。

5.1 TBE 算子开发流程

  1. 定义算子接口(输入 / 输出维度、数据类型);
  2. 实现算子计算逻辑(使用 TBE 提供的原语,如te.lang.cce.vadd);
  3. 编写调度策略(如循环展开、数据分块);
  4. 编译算子为动态库(.so文件);
  5. 在 ACL 应用中加载并调用自定义算子。

5.2 简单 TBE 算子示例:向量加法

以下实现一个简单的 TBE 算子,完成两个向量的加法(输入xy,输出z = x + y):

python

运行

# 文件名:vector_add.py(需放在CANN的TBE算子目录下)
import te.lang.cce
from te import tvm
from te.platform.cce_conf import api_check_support
from te.platform.fusion_manager import fusion_manager
from topi import generic
from topi.cce import util

# 算子接口定义(装饰器指定算子信息)
@fusion_manager.register("vector_add")
def vector_add_compute(x, y, z, kernel_name="vector_add"):
    """
    计算逻辑:z = x + y
    参数:
        x: tvm.tensor.Tensor,输入向量1,shape=[N],dtype=float32
        y: tvm.tensor.Tensor,输入向量2,shape=[N],dtype=float32
        z: tvm.tensor.Tensor,输出向量,shape=[N],dtype=float32
        kernel_name: str,算子名称
    返回:
        tvm.tensor.Tensor,输出向量z
    """
    # 使用TBE原语实现向量加法(te.lang.cce.vadd支持CCE硬件加速)
    res = te.lang.cce.vadd(x, y)
    return res

# 算子入口函数(处理输入校验、调度策略)
@util.check_input_type(dict, dict, dict, str)
def vector_add(x, y, z, kernel_name="vector_add"):
    """
    算子入口:负责输入参数校验、调度生成
    参数:
        x: dict,输入向量1的信息(shape、dtype)
        y: dict,输入向量2的信息(shape、dtype)
        z: dict,输出向量的信息(shape、dtype)
        kernel_name: str,算子名称
    """
    # 1. 输入参数校验
    shape_x = x.get("shape")
    shape_y = y.get("shape")
    dtype_x = x.get("dtype").lower()
    dtype_y = y.get("dtype").lower()
    
    # 校验输入形状是否一致
    if shape_x != shape_y:
        raise ValueError(f"Input shapes must be the same: {shape_x} vs {shape_y}")
    
    # 校验数据类型(仅支持float32)
    if dtype_x != "float32" or dtype_y != "float32":
        raise ValueError(f"Only support float32 dtype, but got {dtype_x} and {dtype_y}")
    
    # 2. 创建TVM张量(绑定输入形状与类型)
    data_x = tvm.placeholder(shape_x, name="data_x", dtype=dtype_x)
    data_y = tvm.placeholder(shape_y, name="data_y", dtype=dtype_y)
    
    # 3. 调用计算逻辑
    with tvm.target.cce():
        res = vector_add_compute(data_x, data_y, z, kernel_name)
        # 生成调度策略(generic.auto_schedule为自动调度,适合简单算子)
        schedule = generic.auto_schedule(res)
    
    # 4. 构建算子(生成可执行代码)
    config = {"name": kernel_name, "print_ir": False, "need_build": True, "need_print": False}
    tvm.build(schedule, [data_x, data_y, res], "cce", name=kernel_name, config=config)

5.3 算子编译与调用

  1. 编译算子:使用 TBE 提供的te.lang.cce.build工具将算子编译为动态库:

    bash

    运行

    python3 -m te.platform.cce_build vector_add.py --kernel_name=vector_add --output=./vector_add.so
    
  2. 在 ACL 中调用:通过acl.op.load()加载算子动态库,再通过acl.op.execute()执行,具体代码参考华为昇腾官方 TBE 算子调用文档 [^7]。

6 CANN 生态与学习资源(新手必看)

6.1 核心生态组件

  • MindSpore:华为自研 AI 框架,深度集成 CANN,支持自动并行、混合精度训练,官网:MindSpore[^8];
  • ModelZoo:预置 1000 + 优化模型(含 CV、NLP、推荐系统),可直接基于 CANN 部署,仓库:Ascend ModelZoo[^9];
  • Ascend Developer:华为昇腾开发者社区,提供技术文档、论坛问答、培训课程,地址:Ascend 开发者社区[^10]。

6.2 学术与工程学习路径

  1. 入门阶段:学习 CANN 基础概念与 ACL 开发,推荐官方教程:CANN 快速入门[^11];
  2. 进阶阶段:深入 TBE 算子开发与模型优化,参考《华为昇腾 CANN 算子开发指南》[^12];
  3. 学术阶段:阅读 CANN 相关研究论文,如《CANN: An Efficient Compute Architecture for Neural Networks on Heterogeneous Platforms》[^2];
  4. 实战阶段:参与华为昇腾开发者大赛(如 “昇腾 AI 创新大赛”),官网:昇腾 AI 大赛[^13]。

7 总结与展望

CANN 架构通过分层解耦设计、高性能算子库与全流程工具链,有效解决了 AI 异构计算的效率与兼容性问题,已成为 “端 - 边 - 云” 全场景 AI 开发的重要支撑。对于大一新生而言,从 ACL 应用开发入手,逐步深入 TBE 算子优化,是掌握 CANN 技术的最佳路径。

未来,CANN 将在三个方向持续演进:

  1. 多模态支持:加强对文本、图像、语音等多模态数据的联合优化;
  2. 边缘计算适配:针对边缘设备资源受限的特点,推出轻量化 CANN 版本;
  3. 开源生态:进一步开放算子开发工具链,吸引更多开发者参与生态建设。

希望本文能为你的 CANN 学习提供清晰的指引,建议结合官方文档与实战案例(如 ModelZoo 中的 ResNet、YOLO 模型)深入练习,逐步提升工程能力。

参考文献与链接

[^2]: CANN 相关论文:CANN: An Efficient Compute Architecture for Neural Networks[^3]: CANN 架构图:华为昇腾 CANN 架构介绍[^4]: CANN SDK 下载:华为昇腾 CANN 下载中心[^5]: CANN 模拟器使用指南:Ascend Virtual NPU 安装教程[^6]: ImageNet 类别表:imagenet_class_index.json[^7]: TBE 算子调用文档:TBE 算子应用开发指南[^8]: MindSpore 官网:MindSpore - 华为自研 AI 框架[^9]: Ascend ModelZoo:昇腾 ModelZoo 代码仓库[^10]: Ascend 开发者社区:华为昇腾开发者社区[^11]: CANN 快速入门:CANN 开发快速入门[^12]: CANN 算子开发指南:《华为昇腾 CANN 算子开发指南》[^13]: 昇腾 AI 大赛:华为昇腾 AI 创新大赛官网

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

报名链接:https://www.hiascend.com/developer/activities/cann20252

欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

Logo

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

更多推荐