ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Atlas 300V实战:从环境搭建到YOLO推理部署全攻略

Atlas 300V实战:从环境搭建到YOLO推理部署全攻略 最近一直在折腾 Atlas 300V 24G 这张卡起因是手头有个项目需要在边缘侧跑 YOLO 目标检测客户点名要华为昇腾的方案。说实话刚开始我对这类 NPU 推理加速卡也有点拿不准——它到底算不算“运算加速卡”和 GPU 的工作方式差异大不大部署 YOLO 是不是把模型扔上去就能跑这一路踩下来我觉着值得把完整过程整理出来给打算上手 Atlas 的朋友们做个参考。先说结论Atlas 300V 24G 确实是一块运算是加速卡但它加速的是“推理”这个环节不是拿来当通用 GPU 用的。它能稳定跑到 140 TOPS 的 INT8 算力配 24GB 显存对 YOLOv5、YOLOv8 这类目标检测模型来说单卡拉十几路 1080p 视频流做实时推理完全没问题。但这中间要过的坎不少——环境搭建、模型转换、推理代码、性能调优每一步都有讲究。这篇文章我就按自己的实操顺序把从硬件认知到 YOLO 真正跑起来的全过程拆开讲包括我踩过的坑和最后的调优手段。1. Atlas 300V 24G 到底算不算“运算加速卡”先把这个概念掰扯清楚1.1 这卡的硬件底子Ascend 310P 芯片和它的定位Atlas 300V 是华为昇腾系列里专门做推理加速的 PCIe 卡300V 24G 就是带了 24GB 显存的版本。核心芯片是 Ascend 310P有 310P3 等型号整个卡的设计目标很明确在尽量低的功耗下把深度学习推理吞吐做到最大化。看几个关键参数就知道它的脾气单卡 INT8 算力约 140 TOPSFP16 约 70 TFLOPS显存 24GB带宽足够喂饱多路视频分析场景典型功耗 72W 左右被动散热或带个小风扇就行接口是标准 PCIe 4.0 x16服务器和工作站都能插这类卡和普通游戏卡、数据中心 GPU 最大的区别是它不做渲染也不擅长通用并行计算里的复杂分支逻辑它把大量晶体管面积都给了矩阵乘法和卷积运算单元专精于神经网络的推理计算。对 YOLO 这种卷积一堆、矩阵运算密集的模型来说反而是最理想的硬件形态。1.2 和 GPU、普通加速卡的本质区别很多朋友上来就问它能不能像 CUDA 那样写点自定义算子跑任意计算答案是能但别期望太高。Ascend 平台有自己的编程模型 AscendCL也支持自定义算子开发但那是 TBE/Ascend C 那套 DSL学习成本比 CUDA 高不少而且最划算的用法是“已有模型转成离线模型直接调”不是啥都自己写。打个比方GPU 像一个全能手艺人你给他图纸他啥都能做但费电、还得管他吃好喝好驱动、CUDA 环境Atlas 300V 更像一条专为推理定制的流水线原材料模型经过编译器优化后变成标准件OM 模型再往流水线上一放效率极高、能耗极低但你想临时改流水线做点别的事那就费劲了。所以我给它的定义是它是推理加速卡不是通用计算卡。你用它跑 YOLO、跑检测、跑分类、跑 OCR、跑分割推理它是一把好手你指望它像 GPU 一样做训练、做通用数据并行计算那是选错对象了。1.3 24G 显存带来什么实际好处24GB 在推理卡里算很大了。这意味着单个大模型 大 batch 推理不局促。比如 YOLOv8x 这种参数量大的模型FP16 权重也就两百多 MB24G 显存可以同时驻留几十个模型实例或者把 batch size 拉到 32、64。多路视频流场景不慌。每一路视频流独立做检测时模型权重是共享的显存占用增长主要来自中间特征图和预处理 buffer24G 能轻松支撑 20~30 路并发。给动态 shape 留了余量。芯片推理时如果输入分辨率有波动中间张量会变大显存大一点就不容易 OOM。不过这里要提醒一句Atlas 300V 的显存和主机内存是物理隔离的数据得通过 PCIe 拷贝进去所以实际跑多路流的时候瓶颈往往在“搬数据”而不是“算不过”。这个后面讲代码和调优会再展开。2. 部署 YOLO 前必须搞定的环境驱动、固件、CANN 一个都不能少2.1 安装顺序比版本号更重要昇腾的软件栈分好几层NPU 驱动driver、固件firmware、CANN 工具包包含推理运行时、算子库、ATC 模型转换工具、以及 NNRT神经网络运行时纯推理场景可以只装这个。我第一次装的时候没注意顺序驱动装完直接装 CANN结果 npu-smi 能识别到卡但加载模型一直报错 runtime 初始化失败。正确的顺序是先装驱动再装固件有的版本驱动包自带固件用 --full 参数装全检查/usr/local/Ascend/driver/version.info确认驱动和固件版本装 CANN toolkit开发编译环境纯跑推理还要装 nnrt运行时设环境变量source /usr/local/Ascend/ascend-toolkit/set_env.sh版本配套上我遇到过 Atl 300V 和 310P 对 CANN 版本有最低要求如果你从昇腾社区下的最新版 CANN 工具包和驱动固件一般是兼容的但如果你用的板卡是特定的 300V 型号比如 300V Pro最好去昇腾社区“固件与驱动”页面找到对应配套版本下载别贪新。2.2 用 npu-smi 判断设备是否健康驱动装好之后第一件事是运行npu-smi info能看到类似下面这样的输出------------------------------------------------------------------------------------------- | npu-smi 24.0.rc1 Version: 24.0.rc1 | ---------------------------------------------------------------------------------------- | NPU Name | Health | Power | HBM-Usage | | 0 300V | OK | 35.2W | 0% / 24GB | ----------------------------------------------------------------------------------------重点关注几个点Health 必须是 OK如果显示 Fault检查供电和 PCIe 链路HBM-Usage 是显存占用跑模型后应该能看到占用上涨温度超过 85°C 就要注意散热了300V 被动散热版在服务器里要靠风道2.3 最容易忽略的权限问题昇腾的设备节点是/dev/davinci*默认属主是 root普通用户直接跑推理会报“open device failed”。我在生产环境部署时用的是添加 udev 规则的方式让普通用户也能访问sudo tee /etc/udev/rules.d/99-ascend.rules EOF KERNELdavinci*, MODE0666 KERNELascend_manage*, MODE0666 EOF sudo udevadm control --reload如果是在容器里跑记得启动容器时挂载/dev/davinci0以及/usr/local/Ascend目录还要加--device/dev/davinci0这类参数。3. 模型转换是重头戏从 PyTorch 的 YOLO 到 OM 离线模型3.1 为什么要转成 OM不直接用 ONNX我们平时做推理习惯了 PyTorch 直接model(img)出结果但在昇腾 NPU 上PyTorch 的算子没法直接执行。Atlas 的推理芯片只认一种格式——OMOffline Model它是经过 ATCAscend Tensor Compiler工具链优化后的离线模型文件里面包含了算子映射、内存规划、图优化等全部信息NPU 拿到就能直接调度执行。所以标准流程是PyTorch 模型 → ONNX → ATC 转 OM → AscendCL 加载执行。中间 ONNX 是个“通用语言”ATC 负责把 ONNX 的算子翻译成昇腾达芬奇架构能跑的指令。3.2 导出 ONNX 时的关键细节这一步看着简单其实坑不少。我以 YOLOv5/YOLOv8 为例说说导出时要注意的点。固定输入尺寸。YOLO 的原始推理通常要等比例缩放和 padding如果导出成动态分辨率ATC 转换时有些算子例如 Resize会变得非常复杂性能也差。我习惯直接固定成 640×640 或 1280×1280后续 AIPP 里做裁剪或缩放。opset 版本。昇腾对 ONNX opset 支持范围有限太新的 opset 可能不支持。我一般用 opset 11~13太老或太新都容易遇到算子不兼容。简化模型。用python -m onnxsim把 ONNX 里的冗余节点、常量折叠掉ATC 转换成功率会高很多。GraphSurgeon 也可以但 onnxsim 更省事。输出节点。YOLO 的原始输出是多个尺度的特征图比如 80×80、40×40、20×20每个特征图后面还要做 Decode 才得到框。在导出 ONNX 时我建议把 Decode 部分尽量留在模型外面输出的就是特征图这样模型更纯粹也便于后续在 CPU 上做 NMS。当然你想让模型直接吐框也行但要自己写 Decode 算子麻烦不说还容易报算子不支持。举个例子YOLOv5 导出 ONNX 的命令大致是python export.py --weights yolov5s.pt --include onnx --opset 11 --img-size 640 640导出成功后用onnxsim简化一下python -m onnxsim yolov5s.onnx yolov5s_sim.onnx3.3 ATC 转换命令参数背后的含义接下来是核心命令。我的 ATC 转换配置通常长这样atc --modelyolov5s_sim.onnx \ --framework5 \ --outputyolov5s_om \ --soc_versionAscend310P3 \ --input_shapeimages:1,3,640,640 \ --insert_op_confaipp.cfg \ --output_typeFP32 \ --input_formatNCHW \ --loginfo逐项解释--framework5表示输入是 ONNX1 是 Caffe2 是 MindSpore5 是 ONNX--soc_version必须和实际芯片一致。Atlas 300V 是 Ascend 310P3 或类似型号用npu-smi info可以看到具体型号。写错了会直接报“soc version mismatch”之类的错误--input_shape固定输入尺寸这里1,3,640,640表示 batch1、通道 3、高宽 640--insert_op_conf是 AIPPAI PreProcessing配置文件用来把图像预处理下沉到 NPU 做比如减均值、除方差、缩放、色域转换这部分后面会细说--output_typeFP32决定输出张量的数据类型。如果模型里转成 FP16 输出会损失精度我一般让输出保持 FP32反正最后后处理也要在 CPU 做转 float 还得花时间3.4 AIPP 配置把预处理“免费”给到 NPUYOLO 的预处理通常是读图 → BGR → 缩放 → 归一化 → 减均值除方差 → 转 NCHW。这套流程如果全在 CPU 做每一帧都要消耗好几个毫秒多路视频时 CPU 很容易打满。昇腾的解法是 AIPP把缩放、减均值、归一化这些操作前移到 NPU 侧利用芯片内置的图像处理单元完成。我的aipp.cfg长这样aipp_op { aipp_mode: static input_format: RGB888_U8 src_image_size_w: 640 src_image_size_h: 640 csc_switch: true rbuv_swap_switch: false crop: false normalize_switch: true mean_chn_0: 0 mean_chn_1: 0 mean_chn_2: 0 min_chn_0: 0 min_chn_1: 0 min_chn_2: 0 var_reci_chn_0: 0.00392156862745098 var_reci_chn_1: 0.00392156862745098 var_reci_chn_2: 0.00392156862745098 }说明一下input_format: RGB888_U8是因为我的输入源本来就是 RGB如果你的视频帧是 YUV比如摄像头硬解出来的是 YUV420SP这里有专门的 YUV 格式选项处理效率更高省掉了 CPU 上的颜色转换。normalize_switch: truevar_reci_chn_0的 1/255 实现归一化。如果你的 YOLO 训练时用了 0.5 均值和 0.5 方差那种归一化方式把 mean 和 var 字段改成对应值即可。用 AIPP 之后CPU 侧传给 NPU 的就是一张原始 RGB 图像模型内部自动完成归一化和缩放CPU 的负担骤降。这也是 Atlas 卡片做多路视频分析的核心优势之一。3.5 转换失败怎么办日志和常见误区ATC 转换失败是家常便饭报错信息有时也很劝退。我最常遇到的两类算子不支持。比如某些新版的 SiLU、hardswish 算子在旧 CANN 上不支持解决方法是升级 CANN或者改 ONNX 算子实现把 SiLU 拆成 sigmoid 乘 x再重新导出。动态 shape 相关错误。提示“input shape is dynamic”或者“the shape is not fixed”基本是因为 ONNX 里有 Resize 或者 Gather 导致中间维度和输入 shape 挂钩。解决方法是固定输入尺寸或者在导出 ONNX 时就把 Resize 参数固化。排查时务必用--loginfo重新跑一遍 ATC它会打印详细日志尤其留意警告和 error 行。日志里能看到是哪个算子不支持再去针对性处理比盲猜高效得多。4. 推理代码怎么写AscendCL 的完整调用链路4.1 AscendCL 的基本编程模式模型拿到手后写推理程序。昇腾的推理接口叫 AscendCLAscend Computing LanguageC 和 Python 都能调但 Python 的 pyACL 封装比较薄本质上和 C 是一一对应的。这里我以 C 为例讲整个调用链。核心流程分六步初始化aclInit(nullptr)做完要aclFinalize()收尾选设备aclrtSetDevice(0)0 是设备号和npu-smi info里看到的 NPU ID 对应建 Context 和 StreamaclrtCreateContext和aclrtCreateStream一个 Stream 就像一条任务流水线模型执行、数据拷贝都按顺序排在 Stream 里加载模型aclmdlLoadFromFile(yolov5s_om.om)返回一个模型 ID如果想一次加载多个模型实例可以用aclmdlLoadFromFileWithMem指定内存池准备输入输出用aclmdlCreateDataset和aclDataBuffer把输入内存包起来。输入内存要用aclrtMalloc申请不能随便用 malloc因为 NPU 要直接访问这块内存执行推理aclmdlExecuteAsync(modelId, inputDataset, outputDataset, stream)然后aclrtSynchronizeStream(stream)等它算完4.2 输入数据怎么喂进 NPU前面提到 AIPP 会自动做预处理那我们在 CPU 侧要做的就很简单把图像数据整理成 AIPP 要求的格式比如 RGB888_U8宽高 640×640连续放一块内存里然后把这部分内存封装成 DataBuffer 放到 Input Dataset 里。注意几个细节如果输入是 640×640×3 的 RGB 图内存要aclrtMalloc申请 640×640×3 字节并把图像数据memcpy进去如果想增加吞吐可以一次喂多张图batch N内存大小就是 N×640×640×3输入 shape 在 ATC 转换时对应改成images:N,3,640,640多路视频流时我习惯每个线程自己持有独立的内存 buffer避免数据竞争4.3 输出解析和一阶段后处理AscendCL 的输出也是一堆 DataBuffer每个 DataBuffer 对应模型的一个输出张量。YOLO 的输出一般是三组特征图比如 1×255×80×80、1×255×40×40、1×255×20×20拿到数据后要把它们 reshape 成标准维度再做 Decode 和 NMS。我写的后处理大致是// 输出特征图解析以 80x80 为例 // data_ptr 指向 1x255x80x80 的 float 数据 // 每个特征点对应 3 个 anchor每个 anchor 有 85 个值x,y,w,h,obj_conf,cls_conf... // 第一步按置信度阈值过滤低分框 // 第二步剩余框做非极大抑制 NMS // 第三步还原到原图坐标注意 AIPP 里是否做了缩放坐标要对回来这一步用 OpenCV 或者手写都能做我在多路并发场景下会用 TBB 或 OpenMP 把三个尺度的解析并行化。对于 YOLOv8 这种无 anchor 结构的模型解析逻辑稍有不同但整体思路一致。4.4 多线程、多路视频流的并发设计Atlas 300V 一个很常见的用法是同时处理多路 RTSP 视频流。我的做法是一个视频流对应一个业务线程每个线程创建独立的 Context 和 StreamAscendCL 里一个 Context 是设备上的独立执行空间在线程内循环执行“拉流 → 解码 → AIPP 推理 → 后处理 → 输出框”。注意一个线程一个 Stream的方式能最大化并发。因为 NPU 有多个 AI Core计算核心Stream 之间可以并行调度如果所有线程共用一个 Stream任务会在同一个队列里排队吞吐会打折扣。4.5 一个精简的推理示例这里给一个精减过的 C 推理核心代码片段省去异常处理和资源释放的部分方便理解全链路#include acl/acl.h void run_inference(const cv::Mat img, uint32_t modelId) { // 1. 创建 stream aclrtStream stream; aclrtCreateStream(stream); // 2. 用 aclrtMalloc 申请输入内存并拷贝图像数据 void* inputBuf nullptr; size_t inputSize 640 * 640 * 3; aclrtMalloc(inputBuf, inputSize, ACL_MEM_MALLOC_NORMAL_ONLY); memcpy(inputBuf, img.data, inputSize); // 3. 构造输入 dataset aclmdlDataset* inputDataset aclmdlCreateDataset(); aclDataBuffer* inputData aclCreateDataBuffer(inputBuf, inputSize); aclmdlAddDatasetBuffer(inputDataset, inputData); // 4. 构造输出 dataset输出 shape 和大小由模型决定先用 aclmdlGetOutputSizeByName 获取 aclmdlDataset* outputDataset aclmdlCreateDataset(); // ... 为每个输出创建 data buffer // 5. 异步执行推理 aclmdlExecuteAsync(modelId, inputDataset, outputDataset, stream); aclrtSynchronizeStream(stream); // 6. 此时输出数据已经在 outputDataset 里拿去后处理 // 7. 释放资源 aclmdlDestroyDataset(inputDataset); aclmdlDestroyDataset(outputDataset); aclrtFree(inputBuf); aclrtDestroyStream(stream); }实际项目里还要处理视频解码昇腾有 DVPP 硬件解码单元能直接把 H.264/H.265 流硬解成 YUV 帧直接喂给 AIPPCPU 零参与这块功能很强大不过引入 DVPP 会增加复杂度如果需要我再单独讲。5. 跑起来之后遇到的坑逐条排查记录5.1 画面全部黑边或框位偏移AIPP 尺寸没对齐第一次跑通推理后处理画框时发现坐标偏差很大框的位置整体往右下偏。后来检查发现是 AIPP 的src_image_size_w配置成 640但实际喂进去的原图是 1920×1080AIPP 按 640×640 的源图像大小去解析数据相当于把 640×1920 的“狭长图”硬塞进去了当然全乱了。正确的做法如果图像源是 1920×1080要么把src_image_size_w/h设置成原图尺寸并开启crop: true让 AIPP 在内部裁剪成 640×640要么在 CPU 侧先把图 resize 到 640×640再喂给 AIPP。我在项目里选的是第二种——CPU 侧做一次 resize 到 640×640AIPP 那边src_image_size_w/h也设置成 640×640两边对齐了框就准了。5.2 DVPP 图像对齐导致的“花边”或者性能骤降昇腾的硬件解码和图像缩放模块DVPP/VPC对输入图像宽高有严格对齐要求缩放时宽高必须是 16 对齐有的版本要求 32 甚至 128 对齐。如果你的输入是 1920×10801080 不是 16 的倍数用 DVPP 缩放就可能报错或者补边。我之前在 1080p 流上直接调用 VPC 缩放结果性能突然掉到原来的三分之一查了半天发现是分辨率没有对齐硬件走了极其慢的降级路径。解决办法先在 CPU 侧把 1920×1080 补边到 1920×10881080 补到 16 的倍数或者干脆缩放到 640×640。这块的规则比较死直接查对应 CANN 版本的 VPC 接口文档最稳妥。5.3 推理结果 NaN 或全是零归一化参数搞错有次换了 AIPP 配置后模型的输出置信度一片零。排查发现var_reci_chn_0我写成了0.00392少了小数点后两位相当于把输入像素除以 255 之后又缩小了 100 倍特征图全变成接近 0 的值YOLO 自然什么都检测不到。在这里必须提醒AIPP 的归一化是逐通道进行的顺序是 BGR 还是 RGB 要跟模型训练时对齐。YOLOv5 训练时一般是 RGB 归一化到 0~1所以 AIPP 输入格式选了 RGB888_U8mean 设 0var_reci 设 1/255输出和 PyTorch 推理结果一致。如果发现输出比 PyTorch 低不少先检查 AIPP 的 mean/var 数值。5.4 显存HBM占用异常上涨正常情况下一张 YOLOv5s 的 OM 模型加上运行时要占几百 MB 到 1~2GB 显存但如果你反复加载/卸载模型或者开了多个 Context 又没有释放24G 也会被吃满后续加载模型直接报“out of memory”。建议用npu-smi info定期盯占用。如果发现泄漏多半是aclmdlUnload没调、Context 没释放或者 DataBuffer 没有全部销毁。这跟 C 里忘 delete 是一个道理只是 NPU 侧不报段错误而是悄悄吃掉显存。5.5 单卡不饱和模型执行时延高但 NPU 利用率只有 30%这是多路流场景常见的情况。看npu-smi info里的 AI Core 利用率只有 30%~40%但帧率已经上不去了。说明瓶颈不在芯片算力而在数据搬运或者后处理。我遇到的主要原因有输入图像用内存拷贝喂进设备PCIe 带宽成了瓶颈后处理在 CPU 上跑得太慢线程被 NMS 卡住没法及时给 NPU 送下一帧只建了一个 Stream单帧按串行执行多路流都堵在一起优化方向开启多 Stream、用 DVPP 硬解码加缩放数据不经过 CPU、后处理适当裁剪比如把置信度阈值调高过滤掉大量低分候选框。6. 调优手段和性能测试如何把这张卡榨出真实水平6.1 该测哪些指标FPS、时延、CPU 占比部署完成后性能测试得有一套规范。我习惯测这几项吞吐量FPS连续处理 N 帧算平均每秒多少帧。单路流上做推理时延和帧率多路流上测总吞吐。单帧时延ms从图像送入输入侧到拿到框的平均耗时。实时视频场景一般要求 30ms 内否则画面会有明显卡顿。CPU 占比昇腾方案的优势就是 CPU 非常闲如果发现 CPU 占用很高说明预处理或后处理设计有冗余。我自己的测试结果大概是这样服务器 CPU 为 Xeon 金牌CANN 8.0 版本YOLOv5s OM 模型场景分辨率Batch单帧平均时延吞吐单路视频流640×64018.2ms约 120 FPS4 路并发640×6401/路-约 300 FPS单卡大 batch640×640822ms约 360 FPS注意实际数值和 CANN 版本、模型大小、服务器配置都有关系但可以看出单张 300V 24G 处理多路 YOLOv5s 是富余的甚至能支撑中低分辨率的多路 YOLOv8s。6.2 多 Stream 并发利用率上不去的最直接解药如果你发现 AI Core 利用率只有 30%最简单粗暴有效的方案就是开多 Stream。把同一个模型在多个 Stream 上并发执行NPU 会自动把任务调度到不同的 AI Core 上。我的做法是开 4 个线程每个线程一个 Stream每个 Stream 里独立跑推理在 300V 上通常能跑到 70% 以上的利用率。6.3 Batch 推理吞吐优先场景的利器如果应用场景允许累积多张图一起推理比如离线批量检测、定时聚合处理可以把 batch 加大。ATC 转换时指定--input_shapeimages:8,3,640,640推理时一次性塞 8 帧。显存够大带宽充足吞吐能提升 2~3 倍。实时视频流场景也可以做“攒批”但这会增加延迟需要权衡。6.4 把视频解码也交给芯片DVPP 的威力300V 板卡自带硬解码支持 H.264/H.265能把 RTSP 流的解码算力完全从 CPU 剥离开。配合 AIPP整条链路可以做到“视频流 → 芯片直接出框”CPU 只负责 NMS 和业务逻辑。这一步我强烈推荐做能感觉整张卡的设计意图就是为海量视频分析而生的不用硬解码等于浪费一半功力。6.5 和 GPU 对比的实际感受手里有块 T4 也做过同样的 YOLOv5s 推理对比下来单帧时延Atlas 300V 和 T4 相当8~10ms 级别功耗T4 要 70W 左右300V 也是这个级别差不多多路并发300V 的 DVPP 硬解在处理视频流时优势很明显CPU 占用远低于 GPU 方案生态成熟度CUDA 生态毫无疑问更完善昇腾的算子覆盖也在快速补齐但遇到冷门算子还是要折腾所以选择时别只看算力数字得结合你的场景。如果是通用 AI 平台、什么模型都可能跑GPU 容错率高如果是固定的视频检测业务、追求低功耗和多路并发Atlas 300V 性价比很高。写在最后的一点个人心得Atlas 300V 24G 这款卡我从最初的“有点怀疑”到后面当主力推理设备用中间最大的感受是昇腾这套东西不是拿过来就能像 CUDA 一样顺手它的门槛主要在前面这一段——环境搭建、模型转换、算子适配一旦跨过去推理性能和稳定性是绝对能打的。它确实是一块运算加速卡而且是一块为“推理”这个专项场景打磨得相当深的加速卡。如果看完这篇文章你也准备试着部署 YOLO我建议先拿一张 300V、装好环境用最简单的 YOLOv5s 模型跑通全流程再逐步加多路视频、加 Batch、加 DVPP 硬解。每一步都很折腾但折腾完之后你会对 NPU 的编程模型有非常深的理解。后面如果有机会我再写一下 DVPP 硬解码的具体用法和自定义算子的开发踩坑那些是更进阶也更少人碰的内容。
返回列表