华为的 Atlas 系列这几年在国产 AI 加速卡里出镜率越来越高,尤其是 Atlas 300V 推理卡,经常在安防、工业质检、智慧零售这些落地场景里看到。我最早接触 Atlas 是因为客户那边要搞国产化替代,手头一批 YOLO 检测模型要从 GPU 迁到昇腾平台,刚开始也踩了不少坑。这篇就把我从硬件选型到 YOLO 模型落地的一整套经验写下来,尤其针对 Atlas 300V 24G 这张卡,给准备入门昇腾推理的朋友做个参考。
1. 先搞清楚 Atlas 300V 24G 到底是什么
1.1 看名字拆规格:它是一张什么卡
很多人第一次看到“Atlas 300V 24G”这个型号,第一反应是“24G 显存挺大,是不是能当游戏显卡用”。这里必须先说清楚:Atlas 300V 系列是昇腾的 AI 推理加速卡(NPU),不是图形显卡(GPU)。它上面的 24G 不是 GDDR6 显存,而是板载的 DDR4/LPDDR4 内存,专门用来存网络权重和中间特征图,不能输出到显示器,也不能用来跑 OpenGL/DirectX 渲染。
Atlas 300V 是个系列命名,下面有不同型号。你拿到手或者云上看到的规格一般是这样:
| 项目 | 典型规格 |
|---|---|
| 算力单元 | 昇腾 310P 系列 NPU 芯片 |
| 单卡 INT8 算力 | 约 140 TOPS(不同型号有差异) |
| 板载内存 | 24GB(DDR4/LPDDR4X) |
| 内存带宽 | 约 204GB/s 级别 |
| PCIe 接口 | PCIe 4.0 x16 |
| 功耗 | 72W 左右(Max 约 75W) |
| 支持精度 | FP16 / INT8 |
| 部署形态 | 标准半高半长 PCIe 卡,也可被动散热 |
所以回到那个热搜问题:“Atlas 300V 24G 是运算加速卡吗?”答案是:它是一款不折不扣的 AI 运算加速卡,但不是显卡。它和 NVIDIA 的 T4、A30 定位有点像,都是拿来做服务器端推理用的,只是芯片架构和软件栈完全不同。
1.2 和显卡 N 卡/A 卡有什么本质区别
昇腾 NPU 和 GPU 虽然都叫“加速卡”,但内部设计思路完全不一样。GPU 的强项是通用并行计算,CUDA 生态把大量矩阵、向量运算都抽象成可编程的 SM 任务,能跑渲染、能跑科学计算、也能跑深度学习,灵活性非常高。而昇腾的达芬奇架构(Da Vinci)主打的是 AI 专用计算,芯片内部有专门的计算单元(Cube、Vector、Scalar),其中 Cube 单元对矩阵乘这类深度学习最核心的运算做了极度优化。
拿搬运来类比:GPU 像是一个大型综合物流中心,什么货都能运,但每一单都要经历完整的调度流程;昇腾 NPU 更像是一条专门为集装箱设计的自动化流水线,普通杂货它不接,但只要接的就是大集装箱(矩阵乘法),效率极高。这就是为什么 INT8 算力上 Atlas 300V 单卡能做到 140 TOPS 级别,而同等功耗的 GPU 往往做不到这么高——它把“不干活”的部分砍掉了。
但也正因为如此,昇腾 NPU 对算子(OP)的适配要求很严。有些 GPU 上随便写的自定义算子、小众插件,昇腾上可能要自己写 TBE 算子或改用内置算子替代,这是所有从 CUDA 迁移过来的人都要接受的第一课。
1.3 为什么要用 Atlas 跑 YOLO:算力账和成本账
YOLO 系模型是目前工业界用烂了的检测模型,尤其是 YOLOv5、YOLOv8 以及更高版本,结构清晰、部署经验多。它们的特点是:主干的卷积计算量大,但整体网络结构规整,非常适合 NPU 这种专用硬件跑。
拿 YOLOv5s 举例,输入 640x640,FP16 推理单帧大概需要 5~10 GFLOPs 左右的运算量。Atlas 300V 的 FP16 算力在 42 TOPS 左右(INT8 是 140 TOPS),理论上单卡跑 YOLOv5s 的吞吐可以做到很可观。我实测过一批 1080p 视频流,多半路并发场景下,Atlas 300V 跑 YOLOv5s INT8 模型,单卡稳定跑 8~12 路实时流(25FPS 以上)问题不大,具体取决于预处理和后处理是不是也上了昇腾。
成本上,一张 Atlas 300V 24G 的采购价和 T4 这类显卡比有明显优势,而且不依赖国外芯片供应链,在国产化项目里是刚需。但代价是你要把软件栈吃透,毕竟 CANN 和 MindSpore 生态比起 CUDA 来说,成熟度和社区资料密度还有差距。
2. 部署 YOLO 前的关键准备
2.1 硬件与驱动环境清单
在动手之前,先把环境确认好,不然后面每个问题都很难排查。我建议按这个清单核对:
- 服务器:x86 或 ARM(鲲鹏)架构均可,最好是支持 PCIe 4.0 的平台。
- 操作系统:Ubuntu 20.04 / 22.04 LTS 是首选,CentOS 7.6+ 也有人用,但官方 CANN 包对 Ubuntu 的支持最稳。
- 驱动:需要安装昇腾设备驱动(Ascend HDK 里的 driver + firmware)。
- 固件:npu-smi info 能看到固件版本,升级 CANN 之前建议把固件和驱动一起对齐。
- CANN 版本:CANN 6.x 或 7.x,注意不同版本的算子支持和 Pytorch 适配都不一样,后文专门说。
- Python:3.7~3.10 均可,但昇腾的 torch_npu 对 Python 版本有要求,建议用 3.8 或 3.9。
装完驱动后,验证是否识别到卡,命令是:
npu-smi info输出类似下面这样的信息就说明卡和驱动正常:
+----------------------------------------------------------------------------------------------+ | NPU Name Health Power Temp Hugepages Memory | | 0 310P OK 72W 50C - 24G |注意看 Health 是不是 OK,如果显示 Fault 或者温度过高,先查散热和固件版本。
2.2 CANN 软件栈的安装与版本匹配
CANN(Compute Architecture for Neural Networks)是昇腾的软件栈,相当于 CUDA + cuDNN 的角色。它负责把 PyTorch/TensorFlow 的算子调用翻译成 NPU 能执行的指令。安装时最容易出问题的就是版本匹配。
我建议用 CANN toolkit 的社区版,安装包可以从昇腾社区下载。安装流程一般是这样:
# 下载对应版本的 Ascend-cann-toolkit_{version}_linux-{arch}.run chmod +x Ascend-cann-toolkit_7.0.0_linux-x86_64.run ./Ascend-cann-toolkit_7.0.0_linux-x86_64.run --install安装完成后设置环境变量:
source /usr/local/Ascend/ascend-toolkit/set_env.sh这里有三个容易踩的坑:
- 驱动、固件、CANN 三者必须匹配。官方有个兼容性列表,你升级 CANN 后必须检查固件版本是不是在支持范围内,否则跑模型会出现莫名其妙的报错。
- 如果你同时装了 MindSpore 和 PyTorch,注意两者对 CANN 版本要求可能不同,必要时要建多个环境。
- 环境变量不要只在当前 shell 里设置,要写进
/etc/profile.d/或者你的用户.bashrc,不然重启后各种“找不到 libascendcl.so”就来了。
2.3 PyTorch 昇腾迁移:torch_npu 的必要性
现在昇腾上跑 PyTorch 模型,主流方式是加上torch_npu这个适配层。它实现了 PyTorch 后端在 CANN 上的对接,让你在用 PyTorch 写代码的时候,把设备指定成npu就能用昇腾卡计算。安装 torch_npu 时要和 PyTorch 版本对应,比如:
| torch 版本 | torch_npu 版本 | CANN 版本 |
|---|---|---|
| 2.1.0 | 2.1.0.post6 | 7.0.0 |
| 2.3.1 | 2.3.1.post1 | 7.0.0 |
| 2.5.1 | 2.5.1.post1 | 7.1.0 |
装好之后,迁移代码最关键的一行就是把设备字符串改掉:
import torch import torch_npu device = torch.device("npu:0") # 原来写 model.to("cuda") 的地方改成 model.to(device)注意,torch_npu不是完全无缝的。有些算子原生实现有坑,比如某些版本的 upsample、某些自定义的激活函数,跑到 NPU 上会报“算子无法匹配”。我后面会专门讲怎么绕过。
3. YOLO 模型移植:从权重到 om 格式
3.1 业界主流流程:pt -> onnx -> om
昇腾上部署 YOLO 推理,最稳妥的链路是把 PyTorch 的 pt 权重先导出为 ONNX,再用昇腾的 ATC(Ascend Tensor Compiler)工具转换成 om 离线模型。为什么不直接把 pt 跑起来?因为 pt 是基于动态图,推理时每个算子都要重新调度,在 NPU 上效率低;om 是经过离线编译优化的静态图,算子已经排好序、中间显存也做了复用规划,跑起来快很多。
流程画出来是这样:
PyTorch .pt --> ONNX .onnx --(ATC)--> .om --> ACL/OCR 推理PyTorch .pt:你训练好的权重。ONNX .onnx:通用交换格式,这里面的算子必须是昇腾支持的算子集。.om:昇腾的离线模型,实际部署跑的都是它。ACL/OCR:推理时用的运行时 API,可以理解为昇腾的 CUDA Runtime。
导出 ONNX 这一步,YOLOv5 官方代码里自带export.py,很简单:
python export.py --weights yolov5s.pt --include onnx --opset 11注意--opset不要太高,昇腾对低版本 ONNX 算子集兼容更好。如果用的是 YOLOv8,Ultralytics 也支持一行导出:
yolo export model=yolov8s.pt format=onnx opset=11导出完成后,可以先在普通 CPU 上用 onnxruntime 验证一下 onnx 文件是不是能加载、输出 shape 对不对,再去转 om。这一步能省很多瞎排查的时间。
3.2 ATC 转换工具的参数设定(关键)
ATC 是把 ONNX 转成 om 的核心工具,也是我踩坑最多的地方。基础命令如下:
atc --model=yolov5s.onnx \ --framework=5 \ --output=yolov5s_bs1 \ --input_shape="images:1,3,640,640" \ --soc_version=Ascend310P3 \ --output_type=FP16参数含义不复杂,但有几个关键点必须说透:
--framework=5:5 表示 ONNX 输入。这是固定格式,别写错。--input_shape:必须和你的预处理完全一致。YOLOv5 原始模型输入是images:1,3,640,640,但注意——很多开源导出脚本里images这个输入名可能被改成x或者其他,先用onnx.load看一下真实输入名再写,不然 ATC 会报Can not find node by name。--soc_version:这是最容易搞错的地方。Atlas 300V 24G 用的芯片是 310P,但 310P 系列里还分 310P1、310P2、310P3。用npu-smi info可以看到芯片信息;不确定的话可以用python -c "from hccl.utils import *; print(get_soc_version())"或者直接去/usr/local/Ascend/driver/version.info查。写错 soc_version 会导致算子编译异常或者干脆不兼容。--output_type=FP16:昇腾推理通常以 FP16 为主。如果你的网络有精度敏感层(比如某些归一化),可以在 ATC 里指定--precision_mode做混合精度,但大多数 YOLO 场景直接 FP16 没问题。--input_format=NCHW:如果模型是 PyTorch 导出的,默认就是 NCHW;如果是从 TensorFlow 转的,可能是 NHWC,需要显式指定。
转换成功后,输出一个.om文件。看到“ATC run success”并且没有E9999类错误,才算过了第一关。
3.3 动态形状处理与 Anchor 后处理优化
YOLO 有两个必须处理好的点,第一就是模型输入动态性,第二是把输出层的解码和 NMS 跑在哪。
先说动态形状。实际业务里,如果输入图片不是固定 640x640,有两种做法:
- 所有输入都先用 letterbox(等比例缩放+填充)统一缩放到 640x640。这是最简单、最推荐的做法,推理时 batch 固定为 1,用静态 om 模型。
- 如果必须支持动态分辨率,需要在 ATC 转换时指定动态维度:
atc --model=yolov5s.onnx \ --framework=5 \ --output=yolov5s_dynamic \ --input_shape="images:-1,3,-1,-1" \ --dynamic_dims="1,640;1,960;4,640;4,960" \ --soc_version=Ascend310P3注意--dynamic_dims不是任意组合,它有一组候选 shape。如果实际推理时传入的 shape 不在候选里,会直接报错。所以我提醒大家:除非产品形态真的要求多分辨率输入,否则用固定 640x640 + letterbox 是最稳的。
再说 YOLO 的输出。YOLOv5 的输出层原始输出是三个尺度的特征图,shape 分别是(1, 3, 80, 80, 85)、(1, 3, 40, 40, 85)、(1, 3, 20, 20, 85),你需要做解码(坐标反算、置信度过滤、NMS)。如果在 CPU 上用 NumPy 跑解码,你会发现 NPU 上推理只用了 2ms,CPU 后处理却要 8ms,完全倒挂。
我的处理方式是:
- 把坐标解码部分尽可能放到模型内完成,也就是修改 YOLO 的 forward 导出方式,把解码后的框直接输出为
(N, 6)这样的结果(或者输出张量的 logits 让自定义后处理做)。 - NMS 用 ATC 支持的算子组合,比如用
NonMaxSuppression算子集成到图里;如果集成不了,就接受一个小 trick——把原始三个尺度的输出(1, 25200, 85)全量输出到 CPU,再用高效向量化 NumPy 代码做过滤,只在置信度>阈值的那几百个框上做 NMS,这样 CPU 耗时能压到 2ms 以内。
4. 推理代码改造与性能调优
4.1 基于 ACL 的 OM 推理最小示例
拿到 om 模型之后,最常用的推理方式有两种:一种是继续用 MindSpore/OpenCV 生态去调用,另一种是用昇腾 ACL 运行时直接加载 om。ACL 方式最灵活,可控性最高,我简单给一个可运行的最小示例,大家在项目里可以照着改。
import acl import numpy as np import cv2 # 初始化 ret = acl.init() ret = acl.rt.set_device(0) context, ret = acl.rt.create_context(0) # 加载模型 model_path = b"yolov5s_bs1.om" model_id, ret = acl.mdl.load_from_file(model_path) # 获取输入输出尺寸 input_size = acl.mdl.get_num_inputs(model_id) output_size = acl.mdl.get_num_outputs(model_id) input_dims = acl.mdl.get_input_dims(model_id, 0)[0]["dims"] # [1, 3, 640, 640] # 申请 device mem input_data_size = 1 * 3 * 640 * 640 * 4 # float32 output_data_size = 1 * 25200 * 6 * 4 # 按实际模型输出调整 input_mem, ret = acl.rt.malloc(input_data_size, 2) # 2 表示内存对齐 output_mem, ret = acl.rt.malloc(output_data_size, 2) # 构造输入数据(这里以一张经过 letterbox 的 BGR 图为例) img = cv2.imread("test.jpg") img = cv2.resize(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 img = np.ascontiguousarray(img) # 将数据拷贝到 device acl.rt.memcpy(input_mem, input_data_size, img.ctypes.data, input_data_size, 1) # 推理 dataset = acl.mdl.create_dataset() acl.mdl.add_dataset_buffer(dataset, input_mem, input_data_size) output_dataset = acl.mdl.create_dataset() acl.mdl.add_dataset_buffer(output_dataset, output_mem, output_data_size) ret = acl.mdl.execute(model_id, dataset, output_dataset)这里有一堆细节:acl.mdl.add_dataset_buffer需要正确的 tensor desc,输入数据要连续内存、NCHW 排布,颜色通道顺序是 RGB 还是 BGR 要跟训练时对齐,这些在代码里容易踩坑。实际工程里通常会把 ACL 包一层类,做成load_model、inference、release的接口,方便后续维护。
4.2 预处理(resize/letterbox/NCHW)要点
YOLO 系列的训练预处理非常固定:letterbox 等比缩放 + 灰度填充(pad 值通常 114)。如果你推理时直接用cv2.resize把图压成 640x640,检测效果会下降,尤其是目标宽高比和原图差别大时,会出现漏检。
正确的 letterbox 逻辑:
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)): shape = img.shape[:2] r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad = (int(round(shape[1] * r)), int(round(shape[0] * r))) dw = int((new_shape[1] - new_unpad[0]) / 2) dh = int((new_shape[0] - new_unpad[1]) / 2) img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = dh, new_shape[0] - new_unpad[1] - dh left, right = dw, new_shape[1] - new_unpad[0] - dw img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return img, r, (dw, dh)注意最后返回的r和(dw, dh),做框坐标还原时必须要用:
# 假设模型输出框为 (x1, y1, x2, y2),在 640x640 空间 x1 = (x1 - dw) / r y1 = (y1 - dh) / r x2 = (x2 - dw) / r y2 = (y2 - dh) / r这个还原漏了,画出来框位置会飘,我在项目里见过有人调了一下午,最后发现是坐标映射的尺寸没乘回原图。
预处理如果全部在 CPU 上做,也会成为瓶颈。建议用cv2的并行版接口、批量预处理,或者更激进一点,把预处理写到 C++ 里。昇腾的 AIPP(AI Preprocessing)可以在硬件上做裁剪、缩放、色域转换,如果你用 AIPP 功能,需要把 letterbox 参数配到 om 模型里,但这要求模型输入必须是固定尺寸,而且改了配置要重新转 om。小项目我不建议一上来就搞 AIPP,先用 CPU 预处理把流程跑通,再考虑把预处理下沉。
4.3 多路并发与静态 AIPP 优化
Atlas 300V 24G 跑 YOLO 的优势在大并发生推理。单路视频流只用一个进程,利用率通常很低,多路并发才能把 NPU 喂饱。做多路并发时建议按下面思路设计:
- 用多线程或多进程操作同一个 model_id,ACL 本身支持多线程并发执行同一个模型。实践里我常用线程池 + 每线程绑定一张图的预处理,然后并发调用
acl.mdl.execute。 - 如果模型 batch=1,多线程并发时 NPU 利用率能上去;但注意同步锁和内存池,ACL 的
acl.mdl.execute默认是同步的,避免在临界区同时大量调用可能造成卡顿。 - 做动态 batch 也行,但性能和内存规划难度都变大。一般工业项目推荐 batch=1 + 多路线程。
关于 AIPP,还是单独说一下。它是硬件预处理单元,可以把 input 数据在数据拷贝时自动完成缩放、均值减除、通道交换,直接从摄像头裸数据推到 NPU,省掉 CPU 的 BGR2RGB、resize、归一化。配置方式是在转 om 时加一个 aipp 配置文件:
{ "aipp_op": { "aipp_mode": "static", "input_format": "RGB888_U8", "src_image_size_w": 1280, "src_image_size_h": 720, "crop": true, "load_start_pos_h": 0, "load_start_pos_w": 0, "crop_size_w": 640, "crop_size_h": 640, "min_chn_0": 0, "min_chn_1": 0, "min_chn_2": 0, "var_reci_chn_0": 0.003921569, "var_reci_chn_1": 0.003921569, "var_reci_chn_2": 0.003921569 } }这个意思是把 1280x720 的原始图缩放到 640x640(实际 AIPP 的裁剪和缩放需要精确配置 resize 参数,不同 CANN 版本支持程度不一样)。AIPP 好处是大批量视频流时 CPU 占用显著下降,但坏处是灵活度低,我在这里栽过几次,最后都是对照官方 sample 代码一点点调,产品实际跑通后在多路场景才敢上 AIPP。
5. 常见问题与排查实录
5.1 ATC 转 om 时报算子不支持
这是迁移 YOLO 最常见的拦路虎。昇腾的算子库覆盖面已经很广,但某些新版本的 ONNX 算子(比如 Mish、SiLU 在某些变体下的表达)可能没有原生实现。YOLOv5 的导出 ONNX 时如果用了opset=17,某些激活函数会被拆成复杂子图,很容易触发不支持的算子。
我的排查步骤是:
- 看 ATC 报错的具体算子名字,比如
E40001: Unsupported op: HardSwish。 - 回导出阶段改成
--opset=11,往往问题消失。 - 如果必须用高版本 opset,用 onnxsim 简化模型:
python -m onnxsim model.onnx model_sim.onnx,很多冗余算子会被合并或删除。 - 实在不行,把这个算子在网络图上用等价算子替换——比如把 SiLU 替换成
x * sigmoid(x),有时候能绕过去。
5.2 动态维度导致的推理报错
我遇到过客户在训练脚本里用了torch.onnx.export(dynamic_axes={...})导出模型,没有固定 batch,然后转 om 时input_shape没写全,ATC 报:
E10001: Input shape of op[images] is dynamic and not fixed.要么把input_shape固定死,要么只能用dynamic_dims方案,没有第三种选择。另外特别注意:ONNX 模型里的输入名可能被动态轴机制改写成"images"或"input"等,必须在导出后用 Python 打印确认,再决定 ATC 参数里写哪个名字。
5.3 DEVICE 内存不足和模型加载失败
Atlas 300V 是 24G 板载内存,看起来很大,但如果你一次加载多个 om 模型,或者一个模型的输入输出 buffer 分配过大,很容易出现out of memory。这里有一个隐藏坑:ACL 给每个模型分配的工作内存(workspace)在某些版本里是按保守值算的,如果模型网络很宽,单个模型占的内存可能远高于你估算的权重文件大小。
排查办法:
npu-smi info看 MEMORY 占用情况。- 代码里减少预分配 buffer:不用一次性把所有视频帧的 buffer 都申请出来,改成从池里复用。
- 用
acl.mdl.get_second_model_mem_size这类接口查询模型实际需要的内存,设置合理的内存池大小。
5.4 推理结果和 GPU 上对不齐
如果同一个 YOLO 权重在 GPU 上检测正常,在 Atlas 上 FP16 推理时出现漏检、小目标丢失严重,大概率是精度模式的问题。昇腾默认可能把网络全部压到 FP16,如果模型里有对数值范围不敏感但要求高的层,就会出问题。
解决思路:
- 转 om 时加
--precision_mode=allow_mix_precision,让算子级选择 FP16 还是 FP32。 - 如果还不够,对关键层指定
--keep_dtype保持 FP32。 - 如果是 INT8 量化后精度掉太多,先用原始 FP16 om 做 baseline,确认模型本身没问题后再谈量化。
我印象很深的是一个火灾烟雾检测项目,GPU 上 0.9 AP 的模型,迁移到 Atlas 上 AP 掉到 0.7。后来发现是 BoTNet 里某些注意力算子在昇腾上被拆成了分批计算,数值误差叠加。最后用混合精度模式 + 对 attention 的 softmax 层强制 FP32,精度才回到 0.88 左右。
6. 一些建议和后续扩展方向
最后再分享几个实际项目里的体会。
如果你刚接触 Atlas 300V,不要一上来就追求把 YOLOv8 这种最新模型压到极致性能。先把 YOLOv5s 迁移跑通,从导出 ONNX、转 om、ACL 推理到后处理还原,形成一套自己的模板工程,再往 YOLOv8、PaddleDetection 或者其他模型上套。这套流程一旦打通,换模型就只是换个权重和调几个参数的事。
另外,昇腾官方社区其实有不少推理 sample,很多都能直接跑通,遇到问题时多翻 CANN 包自带的 sample 代码——里面关于acl.mdl的用法比任何教程都权威。社区里也有大量工作,解决不了的算子问题,去社区搜算子名比你自己硬啃要快得多。
多路并发时,除了调模型推理,还要注意拉流和解码。我试过 Atlas 300V 上跑 16 路解码 + YOLOv5 推理,瓶颈往往不在 NPU,而在 CPU 的硬解路径不友好。Opencv 的VideoCapture并行解码很容易卡,建议用 FFmpeg 多线程解码或者直接上昇腾的 DVPP 硬解。
如果你打算把它嵌入到生产服务里,优先把 C++ 版 ACL 推理封装成 gRPC/HTTP 服务,Python 版适合原型验证,吞吐和稳定性跟 C++ 版本差距很明显。我第一个线上版本用 Python 多线程,单路延迟没问题,但并发一上来后频繁出现上下文切换导致的不稳定,后来换成 C++ 封装,单机吞吐翻了一倍还多。
Atlas 300V 这台卡虽然不是万能的,但在国产化推理场景里,它确实是目前生态比较成熟、性价比不错的选择。只要有耐心把软件栈啃下来,YOLO 这类检测模型在它上面跑出稳定可靠的效果,是完全可行的。