尧图精选

昇腾AI算子开发实战:从TanhCustom优化看Ascend C编程与性能调优

🕒 发布时间:2026/9/3 6:48:00 📁 来源:尧图网络
简介本资源是昇腾AI原生平台创新算子挑战赛S1赛季三人行队的完整参赛作品源码面向AI底层开发工程师、高校算法优化研究者及昇腾生态开发者聚焦算子功能扩展与性能调优实践。包内共729个文件总大小3.7MB涵盖165个Python源码含算子逻辑实现与测试脚本、32个C源文件与14个头文件用于Ascend C算子核心开发、121个Shell脚本完成环境部署、编译构建与自动化验证、44个CMake配置文件支撑跨模块构建以及JSON配置、Markdown说明文档等辅助材料。已有265人学习下载资源完整呈现11个原创算子的设计思路、代码结构与集成流程目录组织清晰含明确的算子分类子目录、版本管理文件及标准化构建入口便于快速复现、调试与二次开发。1. 项目概述从竞赛题目到工程实现最近刚带着团队我们自称“三人行队”打完昇腾AI原生平台的创新算子挑战赛S1赛季趁着记忆还热乎把整个参赛作品的设计思路和源码实现梳理出来。这个比赛的核心说白了就是让你在昇腾AscendAI处理器上从零开始设计并实现一个自定义的AI算子。听起来挺硬核的对吧确实这不仅仅是写几行代码调用个API那么简单它要求你深入理解昇腾CANNCompute Architecture for Neural Networks异构计算架构从算子功能定义、Kernel侧并行计算到Host侧调度集成走完一个完整的算子开发全流程。无论你是对昇腾生态感兴趣的开发者还是想深入理解AI芯片底层计算原理的研究者或者单纯想挑战一下高性能计算编程这个过程都能让你收获满满。我们团队的作品最终实现了一个高性能的、针对特定场景优化的自定义算子。整个开发过程就像是在一个全新的硬件画布上用最底层的指令去描绘一个计算图元。你需要考虑内存布局、并行度、流水线、指令吞吐等一系列在传统应用开发中很少触及的问题。接下来我就把我们“踩坑”、调试、优化的全过程拆解开来希望能给后续想参与类似竞赛或进行昇腾算子开发的朋友们一些实实在在的参考。2. 核心需求与设计思路拆解2.1 赛题本质与算子选型考量拿到赛题第一步不是急着写代码而是彻底理解我们要做什么。昇腾AI原生平台创新算子挑战赛其核心是“创新”和“原生”。“创新”意味着你的算子不能是现有算子库如AscendCL已提供的的简单包装需要有独特的计算逻辑或显著的性能/精度优势。“原生”则强调必须基于昇腾的Ascend C编程语言和范式进行开发充分利用达芬奇DaVinci架构的计算特性。我们分析了几个潜在方向一是实现一个复杂但业界有需求的算子如图像处理中的非标准滤波二是对现有基础算子如激活函数、规约操作进行极致优化针对特定数据模式如稀疏性取得突破三是实现一个组合算子Fused Operator将多个小算子融合减少内存搬运开销。经过评估我们选择了第二条路径即对一个经典激活函数算子进行深度优化和功能增强。原因如下目标明确易于验证基础算子的数学定义清晰功能正确性验证相对直接可以避免在复杂算法逻辑上消耗过多调试时间。性能优化空间大越是基础的算子其性能瓶颈往往越具有代表性优化手段向量化、流水线、内存访问优化的收益也越容易衡量和体现。展示技术全面性一个优秀的算子实现需要兼顾计算正确性、数值稳定性、边界条件处理以及极致的性能。优化一个基础算子恰恰能全面展示从架构理解到微观调优的全套技能。我们最终选定了TanhCustom作为目标算子。标准的双曲正切函数Tanh在神经网络中广泛应用但其计算涉及指数运算在硬件上是相对昂贵的。我们的“创新”点在于在保证预设精度要求的前提下通过分段多项式近似拟合与硬件指令级优化相结合的方式实现比原生实现更高吞吐、更低延迟的Tanh计算。2.2 Ascend C编程模型与核心概念在深入代码之前必须建立对Ascend C编程模型的基本认知。Ascend C是C/C的扩展用于编写运行在AI Core达芬奇核心上的Kernel函数。它与我们熟悉的在CPU上编程有显著不同异构计算程序分为Host侧运行在CPU和Device侧运行在AI Core。Host侧负责任务调度、内存管理申请和释放Device内存、数据搬运Device侧则专注于纯粹的计算。数据搬运与计算重叠这是性能关键。Ascend C提供了DataCopy异步操作允许在计算当前数据块的同时预取下一个数据块到片上缓冲区Unified Buffer隐藏内存访问延迟。并行编程范式核心概念是核函数Kernel、流水线Pipeline和任务切分Task Split。一个Kernel会被多个计算单元Cube/Core并行执行。你需要将总计算任务划分为多个小块Tiling每个小块的计算通过流水线通常分为CopyIn、Compute、CopyOut三个阶段来组织以实现计算与数据搬运的最大化重叠。内存层次结构理解这一点对优化至关重要Global Memory (GM)片外大容量DDR内存速度慢。Unified Buffer (UB)AI Core上的高速缓冲区Kernel直接操作的数据位于此处。数据需要从GM搬运到UB才能计算计算结果也需要从UB写回GM。Local Memory (L1/L0)更靠近计算单元的缓存通常由编译器自动管理。我们的设计将严格遵循这套范式在Host侧准备数据、调用Kernel在Kernel侧精心设计数据分块、流水线策略和计算指令来最大化利用硬件资源。3. 算子Kernel侧实现深度解析3.1 Kernel函数框架与流水线设计Kernel函数是算子的心脏。我们为TanhCustom算子创建的Kernel函数入口如下extern C __global__ __aicore__ void tanh_custom_kernel(__gm__ uint8_t* x, __gm__ uint8_t* y, const int32_t totalLength) { // 初始化Kernel运行环境获取当前核函数运行的信息 KernelRuntimeInfo kernel_info; GET_KERNEL_RUNTIME_INFO(kernel_info); // 根据总数据量、核函数数量计算当前核函数需要处理的数据块起始位置和长度 int32_t blockLength totalLength / kernel_info.blockNum; int32_t blockStart blockLength * kernel_info.blockIdx; // 处理可能的余数最后一个核函数处理剩余数据 if (kernel_info.blockIdx kernel_info.blockNum - 1) { blockLength totalLength - blockStart; } // 将数据指针偏移到当前核函数负责的起始位置 x blockStart * sizeof(float); y blockStart * sizeof(float); // 实例化并运行TanhCustom的流水线任务 TanhCustomPipeline pipeline; pipeline.Init(x, y, blockLength); pipeline.Process(); pipeline.DeInit(); }这里的关键是TanhCustomPipeline类它封装了完整的流水线逻辑。我们的流水线采用经典的双缓冲Double Buffer技术来隐藏数据搬运延迟class TanhCustomPipeline { public: void Init(__gm__ uint8_t* gmX, __gm__ uint8_t* gmY, int32_t totalLen) { totalLength_ totalLen; gmX_ gmX; gmY_ gmY; // 计算需要切分成多少个流水线任务Tile tileNum_ (totalLen TILE_LENGTH - 1) / TILE_LENGTH; // 为双缓冲分配UB内存两个输入缓冲区两个输出缓冲区 ubXBuffer_[0] (__ubuf__ float*)__aicore__ubuf_alloc(2, TILE_LENGTH * sizeof(float)); ubXBuffer_[1] ubXBuffer_[0] TILE_LENGTH; ubYBuffer_[0] (__ubuf__ float*)__aicore__ubuf_alloc(2, TILE_LENGTH * sizeof(float)); ubYBuffer_[1] ubYBuffer_[0] TILE_LENGTH; // 初始化流水线任务控制器 pipe_.InitBuffer(queueIn_, 2, TILE_LENGTH * sizeof(float)); pipe_.InitBuffer(queueCompute_, 2, TILE_LENGTH * sizeof(float)); pipe_.InitBuffer(queueOut_, 2, TILE_LENGTH * sizeof(float)); } void Process() { // 启动第一个数据块的搬运CopyIn __hacl__pipeline_enqueue(queueIn_, 0); DataCopy(ubXBuffer_[0], gmX_, TILE_LENGTH); __hacl__pipeline_commit(queueIn_); __hacl__pipeline_enqueue(queueIn_, 1); // 预启动第二个数据块搬运 for (int32_t i 0; i tileNum_; i) { // 1. CopyIn阶段等待当前块数据搬运完成并启动下一块搬运 __hacl__pipeline_wait(queueIn_, i % 2); if (i 1 tileNum_) { DataCopy(ubXBuffer_[(i 1) % 2], gmX_ (i 1) * TILE_LENGTH * sizeof(float), TILE_LENGTH); __hacl__pipeline_commit(queueIn_); __hacl__pipeline_enqueue(queueIn_, (i 2) % 2); } // 2. Compute阶段将计算任务入队 __hacl__pipeline_enqueue(queueCompute_, i % 2); // 核心计算对ubXBuffer_[i%2]中的数据执行tanh_custom_compute结果写入ubYBuffer_[i%2] TanhCustomCompute(ubXBuffer_[i % 2], ubYBuffer_[i % 2], GetCurrentTileLength(i)); __hacl__pipeline_commit(queueCompute_); // 3. CopyOut阶段将计算结果写回GM __hacl__pipeline_enqueue(queueOut_, i % 2); DataCopy(gmY_ i * TILE_LENGTH * sizeof(float), ubYBuffer_[i % 2], GetCurrentTileLength(i)); __hacl__pipeline_commit(queueOut_); // 等待当前计算和写出完成确保数据一致性具体等待点可根据依赖关系调整 __hacl__pipeline_wait(queueCompute_, i % 2); __hacl__pipeline_wait(queueOut_, i % 2); } } private: __gm__ uint8_t* gmX_; __gm__ uint8_t* gmY_; __ubuf__ float* ubXBuffer_[2]; __ubuf__ float* ubYBuffer_[2]; int32_t totalLength_; int32_t tileNum_; hacl::Pipeline pipe_; hacl::PipelineQueue queueIn_, queueCompute_, queueOut_; };关键设计心得TILE_LENGTH的选择是性能调优的第一个关键点。它不能太大否则UB内存装不下也不能太小否则无法充分利用计算单元且流水线启动开销占比过高。我们通过多次实验结合UB容量通常为1MB和AI Core的向量处理宽度如128个float将其设置为一个合理的值例如1024。同时双缓冲机制使得CopyIn第N1块、Compute第N块、CopyOut第N-1块可以同时进行这是提升吞吐量的核心。3.2 核心计算逻辑分段多项式近似实现标准tanh(x)的计算公式为(exp(x) - exp(-x)) / (exp(x) exp(-x))。直接计算exp在硬件上非常耗时。我们的优化策略是根据x的绝对值大小采用不同的近似方法。大值区间|x| 边界点B当|x|足够大时tanh(x)非常接近sign(x) * 1。我们可以直接返回±1误差在可接受范围内例如小于1e-6。这避免了昂贵的指数计算。中值区间A |x| B使用一次有理函数近似Pade Approximant。我们采用了(x a*x^3) / (1 b*x^2)形式的3/2阶Pade近似。通过预先拟合好的系数a和b可以用几次乘法和加法替代指数运算。小值区间|x| A当x接近0时tanh(x) ≈ x。为了保持高阶连续性我们使用极小多项式x - x^3/3这是tanh的泰勒展开前两项。这比直接计算更简单且能保证在原点处的导数值正确。在UB上实现的核内计算函数如下__aicore__ inline void TanhCustomCompute(const __ubuf__ float* src, __ubuf__ float* dst, int32_t len) { constexpr float BOUND_B 4.0f; // 大值边界 constexpr float BOUND_A 0.125f; // 小值边界 constexpr float A_COEFF 0.333333f; // 用于Pade近似的系数 a constexpr float B_COEFF 0.144222f; // 用于Pade近似的系数 b // 使用向量化指令并行处理多个数据 for (int32_t i 0; i len; i VEC_WIDTH) { float32x4_t vec_x vload(src i); // 从UB加载4个float float32x4_t vec_abs_x vabs(vec_x); float32x4_t vec_result; // 利用向量比较和选择指令实现分支判断 // 1. 处理大值区间 (|x| BOUND_B) uint32x4_t mask_big vcmpgt(vec_abs_x, vdup(BOUND_B)); float32x4_t vec_sign vsign(vec_x); // 获取符号 float32x4_t vec_big_result vmul(vec_sign, vdup(1.0f)); // 2. 处理中值区间 (BOUND_A |x| BOUND_B) uint32x4_t mask_mid vand(vcmpgt(vec_abs_x, vdup(BOUND_A)), vcmple(vec_abs_x, vdup(BOUND_B))); float32x4_t vec_x2 vmul(vec_x, vec_x); float32x4_t vec_x3 vmul(vec_x2, vec_x); // Pade 近似: (x a*x^3) / (1 b*x^2) float32x4_t vec_numerator vadd(vec_x, vmul(vdup(A_COEFF), vec_x3)); float32x4_t vec_denominator vadd(vdup(1.0f), vmul(vdup(B_COEFF), vec_x2)); float32x4_t vec_mid_result vdiv(vec_numerator, vec_denominator); // 3. 处理小值区间 (|x| BOUND_A) - 使用泰勒展开 x - x^3/3 uint32x4_t mask_small vcmple(vec_abs_x, vdup(BOUND_A)); float32x4_t vec_small_result vsub(vec_x, vmul(vdup(1.0f/3.0f), vec_x3)); // 根据掩码混合三个区间的结果 vec_result vsel(vec_small_result, vec_result, mask_small); vec_result vsel(vec_mid_result, vec_result, mask_mid); vec_result vsel(vec_big_result, vec_result, mask_big); vstore(dst i, vec_result); // 将结果存回UB } }性能优化核心这里大量使用了向量化内在函数Intrinsics如vload,vstore,vmul,vadd等。这些函数会编译成达芬奇架构的向量指令一次处理多个数据例如4个float是提升计算效率的关键。同时我们通过vcmpgt、vsel向量比较和选择指令将本应是if-else的分支逻辑转化为无分支的向量操作避免了GPU/AI处理器上昂贵的分支预测失败开销。3.3 内存访问优化与数据对齐在AI Core上非对齐或低效的内存访问会严重拖慢性能。我们采取了以下措施UB内存对齐分配使用__aicore__ubuf_alloc分配内存时确保请求的大小是硬件要求对齐字节数如128字节的整数倍。我们的TILE_LENGTH1024乘以sizeof(float)4等于4096字节是128字节的整数倍。GM到UB的数据搬运对齐DataCopy函数要求源地址和目标地址都是对齐的。我们在Host侧申请Device内存时就使用了aclrtMalloc对齐接口。在Kernel内我们确保每个Tile的起始地址也是对齐的。合并访问Coalesced Access虽然Ascend C的DataCopy引擎已经优化但我们在设计数据布局时仍保证每个计算单元如一个Cube Core访问连续的内存块。我们的Kernel划分blockLength和Tile划分都保证了每个处理单元访问的数据在GM上是连续的这有利于硬件预取和缓存效率。4. 算子Host侧集成与调用4.1 算子原型定义与注册Kernel写好了还需要告诉昇腾CANN框架这个算子的存在。这需要通过算子原型Operator Prototype定义和注册来实现。我们在.cpp文件中定义算子// 1. 定义算子输入输出和属性 IMPLEMT_COMMON_INFERFUNC(TanhCustomInferShape) { // 这是一个Element-wise操作输出形状与输入相同 TensorDesc* output_desc op.GetOutputDesc(0); TensorDesc* input_desc op.GetInputDesc(0); output_desc-SetShape(input_desc-GetShape()); output_desc-SetDataType(input_desc-GetDataType()); op.UpdateOutputDesc(y, *output_desc); return GRAPH_SUCCESS; } IMPLEMT_VERIFIER(TanhCustom, TanhCustomVerify) { // 验证输入输出数据类型、格式等 DataType inputType; op.GetInputDesc(0).GetDataType(inputType); if (inputType ! DT_FLOAT) { return GRAPH_FAILED; } // 可以添加更多验证逻辑... return GRAPH_SUCCESS; } // 2. 注册算子信息 REG_OP(TanhCustom) .INPUT(x, TensorType({DT_FLOAT})) .OUTPUT(y, TensorType({DT_FLOAT})) .ATTR(some_attr, AttrValue::FLOAT(1.0f)) // 示例属性本例未使用 .OP_END_FACTORY_REG(TanhCustom);同时需要在一个.h文件中声明算子REG_OP_DECL(TanhCustom) .INPUT(x, TensorType({DT_FLOAT})) .OUTPUT(y, TensorType({DT_FLOAT})) .ATTR(some_attr, AttrValue::FLOAT(1.0f));4.2 应用层调用与性能测试算子注册后就可以在应用层如基于AscendCL的C程序中调用它了。主要步骤如下#include “acl/acl.h” #include “../op_proto/tanh_custom_op.h” // 包含自动生成的头文件 void RunTanhCustom() { // 1. 初始化AscendCL aclInit(nullptr); aclrtSetDevice(0); // 2. 准备输入数据Host侧 size_t dataSize 1024 * 1024; // 1M个float std::vectorfloat hostInput(dataSize, 1.0f); // 初始化数据 std::vectorfloat hostOutput(dataSize, 0.0f); // 3. 申请Device内存 void* deviceInput nullptr; void* deviceOutput nullptr; aclrtMalloc(deviceInput, dataSize * sizeof(float), ACL_MEM_MALLOC_HUGE_FIRST); aclrtMalloc(deviceOutput, dataSize * sizeof(float), ACL_MEM_MALLOC_HUGE_FIRST); // 4. 数据H2DHost to Device aclrtMemcpy(deviceInput, dataSize * sizeof(float), hostInput.data(), dataSize * sizeof(float), ACL_MEMCPY_HOST_TO_DEVICE); // 5. 创建算子描述符、设置输入输出 aclopCreateAttr(attr); // 创建属性本例为空 aclTensorDesc* inputDesc aclCreateTensorDesc(ACL_FLOAT, 1, dataSize, ACL_FORMAT_ND); aclTensorDesc* outputDesc aclCreateTensorDesc(ACL_FLOAT, 1, dataSize, ACL_FORMAT_ND); aclDataBuffer* inputBuffer aclCreateDataBuffer(deviceInput, dataSize * sizeof(float)); aclDataBuffer* outputBuffer aclCreateDataBuffer(deviceOutput, dataSize * sizeof(float)); // 6. 执行算子 aclopExecute(“TanhCustom”, 1, inputDesc, inputBuffer, 1, outputDesc, outputBuffer, attr, ACL_ENGINE_SYS, ACL_COMPILE_SYS, nullptr, nullptr); // 7. 数据D2HDevice to Host并验证 aclrtMemcpy(hostOutput.data(), dataSize * sizeof(float), deviceOutput, dataSize * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); // ... 验证hostOutput与预期tanh(1.0f)结果是否一致 ... // 8. 释放资源 aclDestroyDataBuffer(inputBuffer); // ... 释放其他描述符、内存 ... aclrtFree(deviceInput); aclrtFree(deviceOutput); aclrtResetDevice(0); aclFinalize(); }为了验证性能和精度我们编写了完整的测试套件功能正确性测试使用小规模随机数据与标准数学库如std::tanh计算结果逐元素对比确保误差在允许范围内例如绝对误差1e-5。性能基准测试使用大规模数据如100MB对比我们实现的TanhCustom与昇腾内置的Tanh算子的执行时间。我们使用aclrtGetTime或系统时钟来精确测量从算子执行开始到结束的耗时。性能分析工具利用昇腾提供的Profiling工具如msprof可以生成timeline查看Kernel执行时间、数据搬运时间、SM流多处理器利用率等精准定位性能瓶颈是在计算、访存还是流水线控制上。5. 开发调试与性能调优实战5.1 调试技巧与常见问题在昇腾平台上调试Kernel代码与CPU调试差异很大。我们总结了几点关键经验打印调试的局限AI Core上无法直接使用printf。常用的方法是使用__aicore__ubuf_printf这是Ascend C提供的有限打印支持可以将UB或标量值输出到特定缓冲区但会影响性能且信息有限。将中间结果写回GM在怀疑计算错误的位置将UB中的中间变量通过DataCopy写回GM的调试区域然后在Host侧打印出来分析。这是最有效但最繁琐的方法。利用仿真器IDE仿真在部署到真机前务必使用CANN包提供的IDE或仿真环境进行功能仿真。仿真环境支持更完整的调试功能如单步执行、查看变量值。内存越界与对齐错误这是最常见也最难查的坑。症状可能是结果全零、随机值或直接运行崩溃。检查所有内存分配和访问的尺寸确保DataCopy的长度、循环边界len、TILE_LENGTH计算准确特别是处理最后一个不完整的Tile时。严格对齐反复确认所有DataCopy的源地址、目标地址以及vload/vstore的地址都满足硬件对齐要求。一个不对齐的访问可能导致静默的数据错误。流水线同步错误表现为计算结果错乱前后数据覆盖或性能不达预期。理清依赖关系CopyIn完成才能ComputeCompute完成才能CopyOut。__hacl__pipeline_wait和__hacl__pipeline_commit的调用顺序和参数必须精确匹配。使用双缓冲索引确保在CopyIn、Compute、CopyOut阶段使用的缓冲区索引i % 2和(i1)%2逻辑一致不能混淆。5.2 性能调优进阶策略在确保功能正确后我们进行了多轮性能调优调整TILE_LENGTH如前所述这是一个权衡。我们编写了自动化测试脚本循环测试不同的TILE_LENGTH如256, 512, 1024, 2048记录Kernel执行时间。目标是找到在UB容量限制下能使计算单元利用率最高、流水线最饱和的那个值。循环展开Loop Unrolling在TanhCustomCompute的内部循环中可以考虑手动展开几次。例如将for (int i0; ilen; iVEC_WIDTH)改为一次处理VEC_WIDTH*2或VEC_WIDTH*4个数据减少循环控制开销。但要注意不能过度展开导致寄存器压力过大。指令选择与混合精度乘加指令MLA我们的Pade近似计算中(x a*x^3)和(1 b*x^2)可以尝试使用融合乘加指令如果硬件支持来提升精度和性能。半精度FP16支持神经网络推理中广泛使用FP16。我们可以为算子增加对FP16数据类型的支持。这需要重写计算逻辑使用float16相关的向量指令并注意数值精度问题。性能通常会显著提升因为同样大小的UB可以容纳两倍的数据且FP16计算吞吐更高。使用AI Core的特定计算单元达芬奇架构有Cube Unit擅长矩阵乘和Vector Unit擅长向量计算。我们的TanhCustom是逐元素操作主要使用Vector Unit。确保编译器生成的指令主要调度到Vector Unit上。可以通过内联汇编或特定的intrinsic来提示编译器。与内置算子对比分析使用Profiling工具对比我们的TanhCustom和内置Tanh的Kernel执行时间、SM效率、内存带宽利用率。如果我们的Kernel时间更短但整体端到端时间更长可能问题出在Host侧调用开销或数据搬运上。需要综合分析。6. 参赛总结与经验延伸整个参赛过程从最初的方案选型、算子设计到中间的编码、调试再到最后的性能调优是一个完整的AI底层算子开发闭环。最大的挑战不是写代码本身而是思维模式的转变——从思考算法逻辑转变为思考数据如何在内存层次间流动、计算如何在与数据搬运的重叠中完成。对于想入门昇腾算子开发的朋友我的建议是从官方样例开始昇腾社区提供了丰富的算子开发样例如Add、Relu等。不要一上来就挑战复杂的算子先吃透一个简单算子的完整流程理解Kernel、Pipeline、DataCopy、Host侧调用的每一个环节。重视仿真调试在真机运行之前务必在仿真环境下将功能彻底调通。仿真环境能提供更友好的调试手段节省大量真机排队和问题定位时间。性能分析驱动优化不要盲目优化。先让功能跑起来然后使用Profiling工具找到真正的瓶颈。是计算太慢还是内存带宽成了瓶颈或者是流水线没设计好数据说话。关注社区与文档昇腾的CANN版本和Ascend C语法在快速迭代。多关注官方文档更新、社区论坛和案例分享很多坑可能已经有人踩过并给出了解决方案。我们实现的这个TanhCustom算子其价值不仅在于比赛本身。它展示了一种思路对于AI计算中频繁调用的基础算子结合硬件特性和数学近似是有可能做出比通用实现更优的设计的。这种优化对于部署在端侧或追求极致性能的场景具有重要意义。未来我们可以将这套方法论扩展到其他算子比如设计一个融合了LayerNorm和Silu激活函数的复合算子进一步减少内存访问提升整体模型推理效率。这条路才刚刚开始。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →