Atlas 300V 24G推理加速卡部署YOLO:从环境搭建到性能调优全指南
最近好几个做视觉应用的朋友不约而同地问我同一个问题Atlas 300V 24G到底算不算运算加速卡这问题乍看有点外行但背后其实藏着一个真实困扰——Atlas这个名字底下产品线太杂AI加速卡、边缘网关、数据平台都叫这个很多人拿到卡之后发现它长得像显卡又没有显示输出口插到服务器上系统也不直接认心里自然就打鼓了。而在AI部署圈子里Atlas 300V 24G这块推理加速卡最近热度确实高尤其“Atlas部署YOLO”几乎成了边缘视频分析项目的标配路径。这篇文章我就把从环境搭建、模型转换到推理调优的完整过程拆开讲一遍给正在选型和已经踩坑入坑的人一个可以直接抄作业的参考。1. 先把身份搞清楚Atlas 300V 24G不是“显卡”而是AI推理加速卡1.1 为什么“运算加速卡”这个疑问会反复出现很多人第一次见到Atlas 300V时习惯性拿它和NVIDIA的GPU做类比装上之后发现nvidia-smi用不了PyTorch里调.cuda()直接报错于是怀疑自己是不是买错了东西。实际上没有买错只是它的工作方式和你熟悉的那套完全不同。从定位上讲Atlas 300V 24G是一块面向数据中心的AI推理加速卡不是通用GPU更不是图形卡。它能做的事情是把训练好的神经网络模型拿过来在NPU神经网络处理器上高效执行前向推理。训练模型这种任务它也能跑但不是它的设计目标硬拿来训练的话效率和成本都不划算。所以准确的说法是它确实是运算加速卡但加速的是“AI推理运算”而不是通用并行计算或者图形渲染。这个区别在硬件设计上体现得很明显。Atlas 300V没有显示输出接口不需要接显示器它存在的意义就是插在服务器里通过PCIe接口和CPU交换数据专职处理视频流、图像、检测框这类负载。你可以把它理解成一条流水线上的专业质检员——不负责搬运不负责包装只盯着产品做检测而且干得比通用工人快得多。1.2 拿到卡之后先把这几项参数读明白我在做实际部署前花了大概半小时把这卡的关键参数理了一遍建议你也这么做因为后面很多调优决策都取决于这几个数字。内存24GB是最直观的亮点。对推理卡来说内存大小决定了你能塞下多少路视频流、多少个模型副本、多大的batch。24GB对于YOLOv5s这种级别的模型来说非常充裕甚至有点奢侈。我实测跑YOLOv5s FP16的OM模型单模型占用也就几百MB24GB意味着你完全可以同时加载几个不同模型或者把batch开到很大。第二个要看的是算力单位。Atlas 300V的算力标注一般用INT8 TOPS不同型号在几十到上百TOPS之间。很多人习惯用GPU的TFLOPS来理解算力这两个单位不能直接比。TFLOPS通常指FP32浮点运算能力而TOPS是指INT8整数运算能力。NPU在INT8上的执行效率远高于FP32这正是推理场景最需要的。所以看到INT8 TOPS标注时不要下意识觉得“数字怎么比GPU小那么多”它们各自擅长的领域不同。第三个需要关注的是DVPP。这是Atlas卡上的硬件编解码与图像处理单元支持视频解码、JPEG解码、图片缩放等操作。做视频流分析时DVPP能帮CPU分担最重的解码负载这是GPU部署YOLO时往往需要单独用硬件解码卡或者牺牲CPU核心才能实现的事。最后一个不那么起眼但很重要功耗。Atlas 300V整卡功耗大概在几十瓦级别比动辄几百瓦的旗舰GPU低很多。这意味着在同一台服务器里你可以插多张卡而不用担心电源和散热扛不住。1.3 TOPS、TFLOPS、INT8与FP16单位换算逻辑别搞混我见过不少人在对比加速卡时把INT8 TOPS直接除以2当成FP16算力这种换算其实没有理论依据因为不同硬件在不同精度下的执行效率完全取决于架构设计。打个比方GPU像一个全能型选手FP32、FP64、INT8都能做但每一项都不是极致优化NPU则像一个专攻INT8的偏科生它对低精度整数运算做了大量并行化和流水线优化所以在推理任务里能把INT8的效率拉到很高。YOLO推理的主体就是卷积和矩阵乘这些运算恰恰能被NPU的AI Core高效执行。以我使用的转换配置为例模型从ONNX转成OM离线模型时我选择了FP16作为网络输出类型但整卡的核心优势仍然体现在INT8上。FP16适合保持精度基线INT8量化适合追求吞吐极限。实际工程中我建议先跑FP16确认整条链路正确之后再考虑INT8量化一上来就INT8容易让你分不清是模型的坑还是量化的坑。2. 搭建运行环境驱动、固件、CANN三段式安装的版本匹配2.1 开始之前先确认宿主机兼容性Atlas 300V是通过PCIe接口插在服务器上的所以第一步不是装软件而是确认宿主机支持。我踩过的第一个坑就是BIOS里PCIe相关设置没调整导致系统能识别但NPU一直报错。建议在装卡之前进BIOS做两件事第一把PCIe链路速度固定到合适档位很多服务器默认是Auto模式某些主板的Auto模式在链路协商时不稳定手动指定后更可靠第二打开Above 4G Decoding选项这个选项允许超过4GB的地址空间映射到PCIe设备如果不打开大内存设备容易出现资源分配失败的问题。宿主机操作系统方面主流Ubuntu和CentOS系都没问题。如果是虚拟机环境建议优先用物理机或者做NPU透传不要指望在普通虚拟化里直接识别到设备那会引出一堆中断和DMA地址映射的麻烦。我在第一次搭建时图省事直接用了已有的VM镜像结果npu-smi info死活看不到卡折腾了半天才意识到是虚拟化环境的问题。2.2 安装顺序驱动→固件→CANN一步都不能乱Atlas的软件栈分三层驱动负责让操作系统识别设备固件负责底层芯片微码CANN是上层的统一编程接口和工具链。正确顺序是先装驱动再升级固件最后安装CANN工具包。驱动安装通常是一个.run或者.deb/.rpm包装完之后需要重启或者执行驱动加载脚本。重启后先别急着装CANN先跑一下npu-smi info确认底层已经通了。这一步非常重要因为如果驱动和固件版本不匹配npu-smi能够看到卡但会显示异常状态这时候装CANN也是白装。CANN安装包一般叫Ascend-cann-toolkit_版本号_linux-架构.run下载后直接执行它会解压到/usr/local/Ascend/ascend-toolkit目录下。安装完成后一定要source对应的环境变量脚本通常是这样source /usr/local/Ascend/ascend-toolkit/set_env.sh我建议把这一行追加到~/.bashrc里因为后面每次跑atc、跑推理程序都要用到这些环境变量漏掉的话会出现找不到so库或者atc命令不存在的报错。2.3 怎么确认环境真的没问题环境是否装好不要靠“感觉”用三件事验证。第一npu-smi info能看到设备列表并且芯片状态为Healthy。如果显示Fault或者Offline说明驱动或固件有问题。第二跑一个官方自带的最小推理样例。CANN安装包里通常会带一些sample比如基于ACL的resnet50推理示例。不要嫌麻烦这个样例通过之后说明驱动、固件、CANN三层之间的通信是通的后续出问题可以非常自信地排除环境层。第三确认运行模式。npu-smi info回显里会显示芯片的运行模式比如推理模式还是训练模式。Atlas 300V是推理卡正常情况下就是推理模式不需要切换。如果你发现模式不对用npu-smi工具手动切换具体命令不同版本略有差异以当前版本帮助为准。2.4 安装顺序搞反了的典型症状这里必须多说一句因为我见过太多人栽在安装顺序上。如果你先装CANN、后装驱动最典型的症状是推理程序启动时加载acl库成功但一调用设备初始化接口就报错错误码类似“device open failed”。这是因为CANN在安装时会检查设备环境没有驱动时它虽然也能装上但初始化逻辑会认为设备不可用。如果驱动版本和CANN版本差距太大症状会更加隐蔽atc转换模型能成功但转换出来的om模型在推理阶段报算子不支持或者执行失败。这类问题排查起来非常费劲因为你很容易怀疑是模型的问题。我的建议是开工之前先把驱动、固件的版本号和CANN版本号记录下来一旦遇到诡异问题第一个排查项就是“三者是否兼容”。官方文档里有一张版本配套表直接对照检查是最省时间的做法。3. 模型转换链路把YOLO从PyTorch世界搬到NPU世界3.1 为什么不能直接把.pt文件扔给Atlas这是很多从PyTorch转过来的人最先产生的疑问。在GPU上你可以直接加载PyTorch模型权重然后用.cuda()把计算搬上显卡但Atlas的NPU不认识PyTorch的运行时它执行的是CANN工具链编译出来的OM离线模型格式。整个转换链路是PyTorch训练好的.pt权重→导出为ONNX中间格式→使用ATC工具把ONNX编译成OM离线模型→推理程序加载OM文件执行。为什么要中间加一道ONNX因为ONNX是开放的中立格式它把模型的计算图和数据流描述出来ATC再针对NPU的硬件特性做算子映射、内存规划、图优化。你可以把这一步理解为“把一份通用设计蓝图转成某个工厂专用的生产指令”。直接给工厂一堆原材料.pt权重它不知道该怎么办。3.2 导出ONNX时的算子避坑导出ONNX是整个流程里最容易出幺蛾子的环节。YOLOv5和YOLOv8的官方仓库都提供了导出脚本命令如下:# YOLOv5 python export.py --weights yolov5s.pt --include onnx --opset 11 --batch-size 1 # YOLOv8 yolo export modelyolov8s.pt formatonnx opset11 dynamicFalse imgsz640这里几个参数要特别注意。第一opset版本我建议固定在11到13之间不要无脑选最新的。opset越高ONNX里可能包含一些比较新的算子ATC转换时如果算子映射表还没跟上就会报“unsupported operator”。第二batch-size固定为1除非你确定后面要用多batch推理否则先用静态shape把链路跑通后面再优化。第三dynamic参数设为False原因下一节会说。导出之后我强烈建议用onnxruntime在CPU上先跑一遍这个ONNX模型给一张测试图做前向推理确认导出的模型本身没问题。这一步能帮你过滤掉很多迷惑性错误——如果ONNX在CPU上输出都是乱的那问题出在导出环节别急着甩锅给ATC。3.3 ATC转换时那几个必填参数的含义ONNX模型准备好之后就到了关键的ATC转换环节。这是我执行过的一个典型命令atc --modelyolov5s.onnx \ --framework5 \ --outputyolov5s_640 \ --soc_versionAscend310P3 \ --input_shapeimages:1,3,640,640 \ --output_typeFP16 \ --insert_op_confaipp.cfg \ --loginfo逐个拆解一下这些参数。--framework5表示输入模型是ONNX格式这个数字是ATC定义的枚举值别改。--soc_version是目标芯片型号。很多人栽在这里因为填错不会在转换时报错但推理时会出现奇怪的执行失败。怎么确认该填什么用npu-smi info -t board -i 0查看板卡信息里面有一个Chip Version之类的字段ATC参数里的soc_version就按这个来填。不同批次的Atlas 300V可能对应不同的soc_version千万不要照抄别人的命令。--input_shape必须和ONNX模型的输入名、维度完全一致。YOLO模型导出后输入名通常是images或input维度是[1,3,640,640]前面设了dynamicFalse这里才能用固定shape。如果导出时保留了动态维度这里的shape描述会复杂很多而且某些算子可能不支持动态shape所以前面我强调固定输入尺寸。--insert_op_conf是AIPP配置文件用来把图像预处理做进模型里下面详细说。3.4 AIPP配置把图像预处理“焊”进模型里AIPP是Atlas推理卡一个很有特色的功能它允许你把一些固定预处理逻辑比如缩放、减均值、归一化、通道顺序调整直接编进OM模型里。这样在推理时你只需要把原始图像数据扔给接口剩下的预处理在硬件上完成省去了host侧的重复计算。以下是一个适配YOLO输入的典型AIPP配置aipp_op { aipp_mode: static input_format: RGB888_U8 src_image_size_w: 640 src_image_size_h: 640 csc_switch: false rbuv_swap_switch: false mean_chn_0: 0 mean_chn_1: 0 mean_chn_2: 0 min_chn_0: 0.003921569 min_chn_1: 0.003921569 min_chn_2: 0.003921569 }我会提醒你AIPP在不同CANN版本里的字段名和语义有细微差异使用前查一下当前版本的接口说明。上面这份配置表达的逻辑是输入是640x640的RGB图像不做通道交换均值0乘以1/255完成归一化。这里有个容易踩的坑通道顺序。YOLO系列在PyTorch里训练时通常使用RGB顺序但OpenCV读图出来是BGR。如果你在host侧读图后直接送进模型就需要通过rbuv_swap_switch或者读图时转换通道来保证顺序一致。两个典型错误是模型训练用的RGB推理时却给BGR检测框全乱或者模型期望BGR却把RGB直接输入结果置信度普遍很低。3.5 转换报错时怎么定位问题ATC转换失败时它会输出一个日志文件默认在atc.log里。很多新人盯着“Error”这个词看半天其实最有效的是看错误发生之前在做什么算子映射。常见错误就那么几类。第一种是“Unsupported operator”意思是某个ONNX算子没法映射到NPU算子解决办法通常是降低opset版本重新导出或者去查这个算子是哪个模块带出来的。第二种是“Input shape mismatch”说明ATC参数里的input_shape和ONNX模型里的定义不一致用Netron打开ONNX文件看输入节点的shape一切以它为准。第三种是内存不足这通常和batch设得太大有关先改成1试试。转换成功之后你会得到一个.om文件。以后部署时只需要这个文件加上对应的预处理逻辑不再需要PyTorch环境。某种意义上这个.om文件才是你在整个服务器上真正要依赖的“模型”本体。4. 推理实现MindX Pipeline和ACL底层各有什么活法4.1 MindX SDK视频流分析场景的开箱路子CANN体系里MindX SDK提供了一套流式编程框架把视频解码、图像缩放、模型推理、结果输出这些环节抽象成一个个plugin你用一份YAML配置把它们串联成一个pipeline程序启动后数据自动在pipeline里流转。对实时视频流分析项目来说MindX SDk是很省事的选择。因为你不需要自己处理从摄像头拉流、硬解、送给NPU、取回结果这一长串细节每个环节都有现成的plugin。一个典型的pipeline长这样pipeline: streamName: yolo_pipeline video_decode: type: mxpi_videodecoder props: output_format: YUV420SP image_resize: type: mxpi_imageresize props: resize_width: 640 resize_height: 640 tensor_infer: type: mxpi_tensorinfer props: model_path: ./yolov5s_640.om这只是结构示意不同版本的plugin名和参数有差异。MindX SDK的优势是上手快缺点是排查问题像一个黑盒当pipeline里某一环出错时它抛出的错误信息有时候非常抽象。所以我的建议是快速原型和产品demo用MindX SDK正式系统如果对延迟、批量、内存复用有精细要求还是直接走ACL更可控。4.2 直接ACL调用适合什么场景ACL是CANN的底层统一API它更接近CUDA runtime。选择ACL而不是MindX SDK通常是因为你有这些需求需要对推理过程做精细控制需要在一个进程里动态加载和卸载模型需要追求极致的多batch性能或者你原本的业务系统是C/Python写的只想嵌入一个轻量推理模块不想引入一整套流式框架。ACL的编程模型不复杂核心就是初始化设备、创建context和stream、加载模型、准备输入输出内存、执行推理、同步等待。它不像CUDA那样要你写kernel函数因为模型的算子计算已经被编译进OM离线模型里了你只需要负责数据搬运和API调用。4.3 一段pyACL推理的最小骨架下面我给一个尽量精简的pyACL推理骨架帮你建立直观感受。这不是可以直接跑的生产代码但核心调用顺序是完整的。import acl import numpy as np # 初始化 acl.init() acl.rt.set_device(0) context acl.rt.create_context(0) stream acl.rt.create_stream() # 加载模型 model_id acl.mdl.load_from_file(yolov5s_640.om) model_desc acl.mdl.create_desc() acl.mdl.get_desc(model_desc, model_id) # 获取输入输出尺寸 input_size acl.mdl.get_desc_size(model_desc, 0) output_size acl.mdl.get_desc_size(model_desc, 1) # 申请device内存 input_ptr, _ acl.rt.malloc(input_size, 2 * 1024 * 1024) output_ptr, _ acl.rt.malloc(output_size, 2 * 1024 * 1024) # host输入数据假设已经从图片预处理得到 input_data np.random.randn(1, 3, 640, 640).astype(np.float16) # 拷贝到device并执行 acl.rt.memcpy(input_ptr, input_size, input_data.tobytes(), input_size, 1) # 1表示H2D acl.mdl.execute(model_id, input_ptr, output_ptr, 0) # 同步并取回结果 acl.rt.synchronize_stream(stream) output_data acl.util.np_from_ptr(output_ptr, output_size, np.uint8)这段代码里有几个细节值得注意内存申请时的对齐值我用的2MB这是ACL的要求memcpy的方向参数要填对H2D是1D2H是2写反了数据流就反了每次execute之后要调用同步接口否则可能拿到的还是旧数据。4.4 输出解析YOLO的后处理其实占了工作量的一半模型输出拿到手之后还得经过一整套后处理才能得到目标框。以YOLOv5s在640x640输入下为例原始输出形状是[1, 25200, 85]25200是三个尺度特征图预测框的总数80x8040x4020x20再乘以每个位置的3个锚框85代表4个框坐标、1个置信度和80个类别分数。你要做的第一件事是阈值过滤。把置信度低于0.25的候选框全部丢弃这一步能砍掉绝大多数无效框。然后对每个保留下来的框解码坐标把归一化的中心点宽高转换成像素坐标。最后是NMS去除同一个目标上的重叠框。NMS可以用OpenCV的cv2.dnn.NMSBoxes也可以用更快的TensorRT NMS插件思路在host侧自己实现。很多人以为模型转换成功等于部署完成其实后处理才是工作量的大头。如果我给你一个实用建议先把后处理的每个环节都用最简单的Python实现跑通确认输出框正确了再去做性能优化。一上来就搞向量化或并行化出错了会非常难排查。5. 实测数据与调优记录24G内存到底能扛多少路YOLO5.1 单卡跑YOLOv5s的基准参考我实际测试时用的环境是Atlas 300V 24GCANN 6.3系列YOLOv5s导出ONNX后再用ATC转成FP16的OM模型。为了排除干扰我把CPU、内存这类宿主侧因素尽量控制住用同一张图循环推理1000次取平均。不同驱动版本、不同散热条件下结果会差不少所以下面数据看相对趋势就好batch单batch耗时(ms)折合单帧耗时(ms)说明15~85~8基准延迟最低415~203.8~5吞吐提升明显826~333.3~4.1进一步提升但收益递减可以看到batch从1提到4单帧均摊时间下降明显再从4提到8收益就不那么大了。原因是NPU的算力资源已经接近饱和继续增大batch只会增加单batch的执行时间均摊收益变小。5.2 多batch、多stream、多线程之间怎么取舍性能调优时先分清你到底是追求低延迟还是高吞吐。实时交互场景优先保证单帧延迟那就batch1离线批量分析或者视频流后处理batch4到8更合适。如果你想同时处理多路视频流还有一个更直观的计算方法。假设模型单帧NPU耗时T毫秒视频流帧率是F那么一路视频流对NPU的占用率大约是T×F/1000。如果模型单帧耗时6ms25fps的流占用率是15%理论上可以跑6到7路再留一些余量给系统开销跑5路比较稳妥。24GB内存在这个场景下根本不是瓶颈真正卡脖子的是算力和解码能力。工程上我还建议结合多线程。内存是够的但NPU执行是排队机制多线程可以同时准备多份输入数据让NPU一直有活干而不是空等宿主侧的数据拷贝。数据搬运和模型执行重叠起来之后整体吞吐才有质变。5.3 DVPP硬件解码的收益与限制如果你部署YOLO是为了做视频流实时分析CPU的解码压力常常比NPU推理压力更先到。没有DVPP时一个1080p 25fps的RTSP流解码就要占掉一个甚至更多CPU核心8路流CPU基本就满了。Atlas 300V的DVPP单元支持硬件视频解码你能把H.264/H.265的码流直接交给DVPP解析成YUV图像帧然后在这个框架内部完成缩放再送进模型。这么做的好处是CPU占用几乎可以忽略NPU专心做推理。但有个限制需要知道DVPP输出的是YUV420SP格式不是OpenCV默认的BGR。所以在给模型之前通常要做颜色空间转换如果转换逻辑写得不对最常见的表现是画面颜色偏绿或者发紫检测框全乱。一种常见做法是让OM模型里AIPP来处理格式转换这样host侧不用参与性能最好。5.4 从“只跑通”到“跑得稳”的长期运行参数项目上线跑几天就出问题的事我见过不少比较典型的几个长期稳定运行调整点大页内存和缓存NPU的设备内存申请释放代价高项目里要复用一个内存池避免每次推理都malloc和free。连接池和线程池同理频繁创建销毁线程会引入抖动。定期检查npu-smi info输出重点关注芯片温度和HugePages-Usage指标。温度过高时芯片会降频推理耗时突然变长这时候优先检查散热风道。另外如果发现设备内存占用只涨不降大概率是某个context或stream没有释放这是ACL编程里的经典内存泄漏排查时重点看异常分支里有没有做资源清理。6. 填坑手册把YOLO跑上Atlas之后最容易翻车的几个环节6.1 动态shape与静态shape之争这是新人最容易掉进去的坑。导出ONNX时如果把dynamic参数设为TrueATC转换时就必须处理动态shape而NPU的算子编译通常针对静态shape做内存规划动态shape会引入额外开销甚至直接报错。我建议一切从简训练完的模型在导出ONNX时固定输入为640x640推理时也统一做letterbox到640x640。如果你的业务需要处理不同分辨率的输入并且你不能接受固定尺寸带来的额外compute再考虑用ATC的多档shape机制。但一定要先评估收益是否真的值得因为多档shape意味着模型体积变大、切换时有额外延迟复杂度会明显上升。6.2 推理输出全是0或者全是乱框的排查路径这个问题十个人里有八个会碰到我列举一个排查顺序。先从输出端往前推。如果所有检测框置信度都是0先怀疑数据拷贝方向或者输出缓冲区读取位置不对。如果置信度正常但框全部超出图像边界多半是后处理解码坐标时忘记把归一化坐标乘回图像尺寸。如果框的位置大致对但类别完全不对基本可以确定是RGB/BGR通道顺序问题。还有一个隐蔽的坑模型在导出时可能已经包含了归一化操作而在推理代码里又做了一次除以255导致输入数值范围异常。排查方法是打印送入模型前的数值分布看看均值、最大值是否合理。如果这一层的预处理和模型训练时的预处理不一致后面一切都白搭。6.3 INT8量化该不该做什么时候做FP16模型跑通之后下一步很多人会想做INT8量化来提升吞吐。量化会带来精度损失到底损失多少取决于模型和校准集。校准集的选择是关键。不要随便拿几十张网上找的图片就做校准校准集要尽量覆盖真实业务场景的目标类型、光线条件、目标尺寸分布。否则量化后模型在测试集上感觉没问题一上真实数据就漏检。我的习惯是先做FP16基线评估记录mAP或者针对业务自定义的指标然后做INT8量化用同一份测试集做对比。如果mAP掉点超过1%就检查校准集是否合理如果掉点仍然明显那就继续用FP16毕竟吞吐量可以通过batch和多线程来弥补。6.4 内存泄漏和context泄漏的典型表现ACL程序长时间运行后一个典型故障是内存占用不断爬升。看到npu-smi info里HugePages-Usage一直涨、不回落基本可以判定是设备侧内存泄漏。常见泄漏源有两个申请了device内存但某个异常分支里没有释放创建了context或者stream程序退出时没有销毁。要根除这类问题我建议所有资源申请都集中管理或用装饰器/析构函数保证释放。还有一个容易被忽略的每次acl.mdl.execute失败后返回的error code要打印出来不要吞掉。很多泄漏都因为失败分支直接return导致已经申请的内存被遗忘。如果你发现设备内存正常但宿主机内存持续增长问题大概率出在host侧的数据拷贝和对象缓存上检查推理循环里是不是不断创建了新的numpy数组或者中间对象把旧的又引用着不放。做完整套Atlas 300V 24G的YOLO部署我最大的体会是硬件本身的性能潜力很大但真正决定项目成败的往往是一些“看起来不重要”的细节——版本匹配、输入尺寸固定、通道顺序、内存资源管理。只要把这条链路的每个环节都理顺从零到跑通并不是难事。最后给准备入手的你一个建议拿到卡之后先不要急着优化把驱动、固件、CANN的版本配套关系记录下来用最保守的配置把流程跑通然后再一步步做性能和稳定性调优。这样即使遇到问题你也能很清楚地知道问题出在哪一层。