news 2026/9/20 2:10:50

CANN Runtime 流级浮点溢出检测实战:从开关配置、状态查询到复位清理的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN Runtime 流级浮点溢出检测实战:从开关配置、状态查询到复位清理的完整指南
  • CANN
  • Ascend
  • 人工智能
  • 任务调度

【免费下载链接】runtime

本项目提供CANN运行时组件和维测功能组件。

项目地址:https://gitcode.com/cann/runtime
点击查看免费下载

导读

本篇文章以 CANN Runtime 开源仓库中的0_overflow_detection示例为核心,系统讲解在昇腾设备上基于 Stream 的浮点溢出检测能力:如何查询并切换 Device 的浮点饱和模式、如何开启并回读流级溢出开关、如何通过固定大小的状态缓冲异步获取溢出状态并同步回 Host,以及如何在退出前完成状态复位与资源清理。读完本文,你将掌握aclrtSetStreamOverflowSwitchaclrtGetOverflowStatusaclrtResetOverflowStatus这一组溢出检测 API 的完整用法、底层调用链与兼容性注意事项,可直接将示例改造接入自己的训练或推理程序。

一、示例概览:什么是流级溢出检测

浮点溢出(Float Overflow)是 AI 训练与推理中常见的数值异常来源,当算子计算过程中出现上溢/下溢等异常值时,往往会导致最终结果不可信。CANN Runtime 提供了一套流级(Stream 级)溢出检测能力:开发者可以针对某个 Stream 开启溢出检测开关,随后在流上算子任务执行完成后,异步查询该流上是否发生过浮点溢出,并在必要时复位溢出状态、重新开始一轮检测。

本示例位于仓库的可靠性(Reliability)样例集下:

  • example/4_reliability/overflow_detection/0_overflow_detection/README_en.md
  • main.cpp
  • run.sh
  • CMakeLists.txt

根据 example/README_en.md 的介绍,4_reliability样例集聚焦可靠性能力,包括溢出检测、错误恢复等场景,是开发者快速上手 Runtime 关键特性的参考入口。本示例演示的核心流程为:流级溢出检测开关的开启 → 溢出状态的查询 → 溢出状态的复位

二、示例功能清单与执行流程

原文档明确了本示例的五个功能要点,逐个展开如下:

  1. 查询当前 Device 的浮点溢出模式,并将其切换为ACL_RT_OVERFLOW_MODE_SATURATION(饱和模式):流级溢出检测依赖饱和模式,因此示例先读取原始模式,再切换到饱和模式,并在结尾恢复原始模式。
  2. 在饱和模式下创建 Stream,开启溢出检测开关,并回读当前开关配置:通过aclrtSetStreamOverflowSwitch(stream, 1)开启、aclrtGetStreamOverflowSwitch确认开关状态。
  3. 分配一块固定的 64 字节 Device 状态缓冲,异步获取一次溢出状态,并同步到 Host:状态缓冲用于承载 Device 侧写入的溢出标志,读取后通过aclrtMemcpy同步回 Host 解析。
  4. 调用aclrtResetOverflowStatus再次查询状态,并在结束时恢复原始的饱和模式:复位后再次查询,用于验证复位语义,最后把 Device 模式还原为运行前的值。
  5. 销毁 Stream、Context 与状态缓冲:按资源依赖逆序释放,完成干净退出。

整体执行顺序可概括为:初始化 → 模式探测与切换 → 建流与开关配置 → 状态缓冲分配 → 溢出状态查询 → 复位与二次查询 → 资源清理。

三、运行环境与产品支持

原文档明确列出了本示例的产品支持矩阵,整理如下:

产品支持情况
Ascend 950PR / Ascend 950DT支持
Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持
Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持

需要说明的是,根据 example/README_en.md 的产品支持表约定,该表仅列出已验证或已明确声明支持状态的产品;未列出的产品并不意味着不支持,若产品被明确不支持,示例 README 会以×标注并附加说明。

此外,从仓库的构建配置可以推断,arch5162_unsupported_acl_api.def 涉及对部分溢出检测相关 API 的剔除声明(该文件同时出现在aclrtSetStreamOverflowSwitchaclrtGetOverflowStatusaclrtResetOverflowStatus等符号的检索结果中),说明溢出检测能力存在芯片架构与 Runtime 构建形态相关的差异性——这正是示例代码需要做"可选能力探测"(见第六节)的根本原因。

四、构建与运行

4.1 环境准备

运行本示例需要具备:

  • 已安装固件、驱动与 CANN 软件包的计算环境(安装与升级步骤详见 example/README_en.md 的环境准备章节);
  • 满足产品支持矩阵中列出的昇腾设备;
  • 若对仓库src目录源码做了自定义修改,还需按根目录 README_en.md 的指引编译部署 Runtime 后再运行示例。

4.2 编译与运行

按原文档的步骤执行:

# 将 ${install_root} 替换为 CANN 安装根目录,默认安装目录为 /usr/local/Ascend source ${install_root}/cann/set_env.sh export ASCEND_INSTALL_PATH=${install_root}/cann # 构建并运行 bash run.sh

其中run.sh内部完成的工作如下(详见 run.sh):

  1. 加载${ASCEND_INSTALL_PATH}/bin/setenv.bash环境脚本;
  2. 在示例目录下创建build目录并执行 CMake 配置,传入-DASCEND_CANN_PACKAGE_PATH=${_ASCEND_INSTALL_PATH}
  3. 执行make -j$(nproc)完成编译;
  4. 运行生成的./build/main可执行文件。

对应的 CMakeLists.txt 展示了编译要点:

  • include_directories引入${ASCEND_CANN_PACKAGE_PATH}/include与样例公共头文件目录;
  • 编译选项为-O2 -std=c++17 -D_GLIBCXX_USE_CXX11_ABI=0 -Wall -Werror
  • 链接${ASCEND_CANN_PACKAGE_PATH}/lib64/libascendcl.so,即 AscendCL 动态库。

五、关键 API 与调用链解析

本示例涉及的核心 API 均声明于 include/external/acl/acl_rt.h,以下逐一解析。

5.1 浮点溢出模式枚举

aclrtFloatOverflowMode定义了 Device 侧浮点溢出的处理模式(见 acl_rt.h):

typedef enum aclrtFloatOverflowMode { ACL_RT_OVERFLOW_MODE_SATURATION = 0, // 饱和模式:溢出时数值钳制到可表示范围 ACL_RT_OVERFLOW_MODE_INFNAN, // 溢出时输出 Inf/NaN,保留异常数值 ACL_RT_OVERFLOW_MODE_UNDEF, // 未定义/默认值 } aclrtFloatOverflowMode;

示例代码在打印日志时,通过OverflowModeToString辅助函数将枚举值映射为可读字符串(ACL_RT_OVERFLOW_MODE_SATURATION/ACL_RT_OVERFLOW_MODE_INFNAN/ACL_RT_OVERFLOW_MODE_UNDEF/ACL_RT_OVERFLOW_MODE_UNKNOWN)。

5.2 设备饱和模式管理

  • aclrtGetDeviceSatMode(aclrtFloatOverflowMode* mode):查询当前 Device 的饱和模式;
  • aclrtSetDeviceSatMode(aclrtFloatOverflowMode mode):设置目标饱和模式。

声明见 acl_rt.h。从 ACL 实现层看(src/acl/aclrt_impl/device.cpp):

aclError aclrtGetDeviceSatModeImpl(aclrtFloatOverflowMode* mode) { ACL_LOG_INFO("start to execute aclrtGetDeviceSatMode"); ACL_REQUIRES_NOT_NULL_WITH_INPUT_REPORT(mode); rtFloatOverflowMode_t rtMode = RT_OVERFLOW_MODE_UNDEF; ACL_REQUIRES_RTS_OK(rtGetDeviceSatMode(&rtMode)); *mode = static_cast<aclrtFloatOverflowMode>(rtMode); ... } aclError aclrtSetDeviceSatModeImpl(aclrtFloatOverflowMode mode) { ACL_LOG_INFO("start to execute aclrtSetDeviceSatMode, mode is %s", acl::GetFloatOverflowModeDesc(mode)); ACL_REQUIRES_RTS_OK(rtSetDeviceSatMode(static_cast<rtFloatOverflowMode_t>(mode))); ... }

可见 ACL 层是薄封装:aclrtGetDeviceSatMode最终调用 Runtime 的rtGetDeviceSatModeaclrtSetDeviceSatMode最终调用rtSetDeviceSatMode,二者通过ACL_REQUIRES_RTS_OK宏将 Runtime 返回值转换为 ACL 错误码并上报日志。

5.3 流级溢出开关

  • aclrtSetStreamOverflowSwitch(aclrtStream stream, uint32_t flag):开启或关闭指定流上的溢出检测,flag0表示关闭、1表示开启;
  • aclrtGetStreamOverflowSwitch(aclrtStream stream, uint32_t* flag):查询指定流当前的溢出开关状态,返回0表示关闭、其他值表示开启。

声明与参数语义见 acl_rt.h。

5.4 溢出状态获取与复位

  • aclrtGetOverflowStatus(void* outputAddr, size_t outputSize, aclrtStream stream)异步获取指定流的溢出状态,写入outputAddr指向的 Device 侧地址;
  • aclrtResetOverflowStatus(aclrtStream stream)异步复位指定流的溢出状态。

两个接口的 Restriction 都明确指出:调用后必须调用aclrtSynchronizeStream确保流上任务执行完成,再继续后续操作(见 acl_rt.h)。这是示例代码中每次查询/复位后紧跟aclrtSynchronizeStream的规范依据。

从 ACL 实现层看(src/acl/aclrt_impl/device.cpp):

aclError aclrtGetOverflowStatusImpl(void* outputAddr, size_t outputSize, aclrtStream stream) { ACL_PROFILING_REG(acl::AclProfType::AclrtGetOverflowStatus); ACL_LOG_INFO("start to execute aclrtGetOverflowStatus, outputSize = %lu", outputSize); ACL_REQUIRES_NOT_NULL_WITH_INPUT_REPORT(outputAddr); ACL_REQUIRES_RTS_OK(rtGetDeviceSatStatus(outputAddr, outputSize, static_cast<rtStream_t>(stream))); ... } aclError aclrtResetOverflowStatusImpl(aclrtStream stream) { ACL_PROFILING_REG(acl::AclProfType::AclrtResetOverflowStatus); ACL_LOG_INFO("start to execute aclrtResetOverflowStatus"); ACL_REQUIRES_RTS_OK(rtCleanDeviceSatStatus(static_cast<rtStream_t>(stream))); ... }

可以确认的底层调用链为:aclrtGetOverflowStatus → rtGetDeviceSatStatusaclrtResetOverflowStatus → rtCleanDeviceSatStatus。同时这两个接口带有ACL_PROFILING_REG埋点,说明溢出状态查询/复位会纳入 ACL Profiling 统计。

5.5 状态缓冲的数据格式

示例以 64 字节固定大小缓冲承载溢出状态。状态缓冲的首 4 个字节为溢出标志位(uint32_t),通过ReadOverflowFlag函数std::copy_n拷贝解析:

constexpr size_t kOverflowStatusBufferSize = 64; uint32_t ReadOverflowFlag(const uint8_t* statusBuffer) { uint32_t overflowFlag = 0; std::copy_n(statusBuffer, sizeof(overflowFlag), reinterpret_cast<unsigned char*>(&overflowFlag)); return overflowFlag; }

该缓冲分配在 Device 侧(aclrtMalloc+ACL_MEM_MALLOC_HUGE_FIRST),由 Device 写入,再通过aclrtMemcpyACL_MEMCPY_DEVICE_TO_HOST方向同步回 Host 栈上数组进行解析。

六、示例代码逐段解析

6.1 可选能力探测机制

由于溢出检测能力并非在所有产品/所有 Runtime 构建上都可用,示例定义了一个关键辅助函数HandleOptionalOverflowRet,将"能力探测失败"与"真正的执行错误"区分对待:

int32_t HandleOptionalOverflowRet(const char* apiName, aclError ret, const char* reason) { if (ret == ACL_SUCCESS) { return 0; } if (ret == ACL_ERROR_RT_FEATURE_NOT_SUPPORT) { WARN_LOG("%s is unavailable in the current environment (ret=%d): %s", apiName, static_cast<int32_t>(ret), reason); return 1; // 能力不支持:告警后按探测结束处理 } ERROR_LOG("Operation failed: %s returned error code %d", apiName, static_cast<int32_t>(ret)); return -1; // 真实错误:终止示例 }

该函数对ACL_ERROR_RT_FEATURE_NOT_SUPPORT(错误码 207000)返回1(环境不支持,仅告警),对其他错误码返回-1(真实失败)。调用方据此决定是提前优雅退出还是报错终止,这与原文档"Known Issues"一节描述的兼容性行为完全对应。

6.2 初始化与上下文管理

const int32_t deviceId = 0; CHECK_ERROR(aclInit(nullptr)); // ACL 初始化 CHECK_ERROR(aclrtSetDevice(deviceId)); // 绑定 Device 0 CHECK_ERROR(aclrtCreateContext(&context, deviceId)); // 创建 Context

示例使用aclInit/aclFinalize完成初始化与去初始化,aclrtSetDevice/aclrtResetDeviceForce管理 Device。值得注意的是,示例在结束时使用aclrtResetDeviceForce清理本进程占用的 Device 资源——按照 example/README_en.md 的约定,单机单进程示例通常使用aclrtResetDeviceForce,而多进程 IPC 示例则使用aclrtResetDevice以避免影响同主机上的其他进程。

6.3 模式探测与切换

const int32_t getModeStatus = HandleOptionalOverflowRet( "aclrtGetDeviceSatMode(&originalMode)", aclrtGetDeviceSatMode(&originalMode), "the current device/runtime does not expose device saturation mode"); if (getModeStatus < 0) { return -1; } if (getModeStatus > 0) { WARN_LOG("Overflow detection is unavailable in the current environment, sample finished after probing."); return 0; } const int32_t setModeStatus = HandleOptionalOverflowRet( "aclrtSetDeviceSatMode(ACL_RT_OVERFLOW_MODE_SATURATION)", aclrtSetDeviceSatMode(ACL_RT_OVERFLOW_MODE_SATURATION), "stream overflow detection requires saturation mode and is only supported on specific products/runtime builds"); if (setModeStatus < 0) { return -1; } if (setModeStatus > 0) { WARN_LOG("Overflow detection is unavailable in the current environment, sample finished after probing."); return 0; } saturationModeChanged = true; CHECK_ERROR(aclrtGetDeviceSatMode(&currentMode)); INFO_LOG("Device saturation mode switched from %s to %s.", OverflowModeToString(originalMode), OverflowModeToString(currentMode));

这段代码体现了两个设计要点:一是先保存原始模式originalMode),为结尾恢复做准备;二是探测失败即优雅退出——若当前设备/Runtime 不暴露饱和模式能力,示例记录告警后结束(return 0),避免在不受支持的平台上继续执行而产生误导性失败。

6.4 创建流并配置溢出开关

CHECK_ERROR(aclrtCreateStream(&stream)); streamCreated = true; const int32_t setOverflowSwitchStatus = HandleOptionalOverflowRet( "aclrtSetStreamOverflowSwitch(stream, 1)", aclrtSetStreamOverflowSwitch(stream, 1), "the current environment does not expose stream-level overflow detection even in saturation mode"); if (setOverflowSwitchStatus < 0) { return -1; } if (setOverflowSwitchStatus > 0) { WARN_LOG("Stream overflow detection is unavailable in the current environment, sample finished after probing."); return 0; } uint32_t queriedSwitch = 0; CHECK_ERROR(aclrtGetStreamOverflowSwitch(stream, &queriedSwitch)); INFO_LOG("Overflow switch=%u", queriedSwitch);

开启开关后立即回读,确认queriedSwitch == 1,保证后续状态查询建立在开关确实生效的前提下。

6.5 分配状态缓冲并查询溢出状态

CHECK_ERROR(aclrtMalloc(reinterpret_cast<void**>(&statusDevice), kOverflowStatusBufferSize, ACL_MEM_MALLOC_HUGE_FIRST)); statusAllocated = true; uint8_t statusHost[kOverflowStatusBufferSize] = {}; uint32_t overflowFlag = 0; CHECK_ERROR(aclrtGetOverflowStatus(statusDevice, kOverflowStatusBufferSize, stream)); CHECK_ERROR(aclrtSynchronizeStream(stream)); // 确保异步状态写入完成 CHECK_ERROR(aclrtMemcpy(statusHost, sizeof(statusHost), statusDevice, kOverflowStatusBufferSize, ACL_MEMCPY_DEVICE_TO_HOST)); overflowFlag = ReadOverflowFlag(statusHost); INFO_LOG("Overflow status before reset=%u", overflowFlag);

这是一个典型的异步状态读取四步法

  1. aclrtMalloc分配 Device 侧状态缓冲(ACL_MEM_MALLOC_HUGE_FIRST优先大页分配);
  2. aclrtGetOverflowStatus将查询任务下发到流上(异步);
  3. aclrtSynchronizeStream同步等待,确保状态已写入 Device 缓冲(对应头文件中 Restriction 的强制要求);
  4. aclrtMemcpyDEVICE_TO_HOST)将状态同步回 Host,解析首 4 字节溢出标志。

6.6 复位并二次查询

CHECK_ERROR(aclrtResetOverflowStatus(stream)); CHECK_ERROR(aclrtSynchronizeStream(stream)); std::fill_n(statusHost, kOverflowStatusBufferSize, static_cast<uint8_t>(0)); overflowFlag = 0; CHECK_ERROR(aclrtGetOverflowStatus(statusDevice, kOverflowStatusBufferSize, stream)); CHECK_ERROR(aclrtSynchronizeStream(stream)); CHECK_ERROR(aclrtMemcpy(statusHost, sizeof(statusHost), statusDevice, kOverflowStatusBufferSize, ACL_MEMCPY_DEVICE_TO_HOST)); overflowFlag = ReadOverflowFlag(statusHost); INFO_LOG("Overflow status after reset=%u", overflowFlag);

复位同样采用"下发 → 同步 → 拷贝 → 解析"的流程。二次查询前先清零 Host 侧缓冲,避免残留数据影响对"复位后状态"的解读,是值得借鉴的防御性写法。

6.7 逆序资源清理

示例在 lambda 结束后统一进行资源清理,严格按照逆序释放(状态缓冲 → 恢复饱和模式 → 销毁 Stream → 销毁 Context → 复位 Device → Finalize),并使用状态位(statusAllocatedsaturationModeChangedstreamCreated等)保证只在相应资源确实建立后才执行释放:

if (statusAllocated) { const aclError freeRet = aclrtFree(statusDevice); ... } if (saturationModeChanged) { const aclError restoreModeRet = aclrtSetDeviceSatMode(originalMode); // 恢复原始饱和模式 ... } if (streamCreated) { const aclError destroyStreamRet = aclrtDestroyStream(stream); ... } if (contextCreated) { const aclError destroyContextRet = aclrtDestroyContext(context); ... } if (deviceSet) { const aclError resetDeviceRet = aclrtResetDeviceForce(deviceId); ... } if (aclInitialized) { const aclError finalizeRet = aclFinalize(); ... } if (finalResult == 0) { INFO_LOG("Overflow detection sample finished successfully."); }

该模式确保即便中途出错,已分配的资源也能被回收,并且不会遗漏对 Device 饱和模式的恢复——这是"探测/临时修改系统状态后必须还原"的最佳实践。

七、示例输出解读

正常运行(无溢出发生)时,示例输出如下:

[INFO] Device saturation mode switched from ACL_RT_OVERFLOW_MODE_INFNAN to ACL_RT_OVERFLOW_MODE_SATURATION. [INFO] Overflow switch=1 [INFO] Overflow status before reset=0 [INFO] Overflow status after reset=0 [INFO] Overflow detection sample finished successfully.

逐行解读:

  • 第一行确认模式从ACL_RT_OVERFLOW_MODE_INFNAN切换为ACL_RT_OVERFLOW_MODE_SATURATION(原始模式值以实际设备为准);
  • 第二行确认流级溢出开关已开启(Overflow switch=1);
  • 第三、四行分别打印复位前后的溢出标志,本例流上未执行可能溢出的算子任务,因此前后均为0,这也演示了"状态查询与复位"两个动作本身可正常执行;
  • 第五行表示示例全流程成功完成。

八、已知问题与兼容性注意事项

原文档的 Known Issues 部分是本示例最关键的实战提醒,需重点理解:

  • aclrtSetStreamOverflowSwitchACL_RT_OVERFLOW_MODE_SATURATIONACL_RT_OVERFLOW_MODE_INFNAN两种模式下均可使用,即溢出开关本身不强制要求饱和模式;但示例选择先切换到饱和模式,是为了演示"模式切换 + 开关配置"的完整组合流程。
  • 若当前产品不支持该能力,相关接口可能返回ACL_ERROR_RT_FEATURE_NOT_SUPPORT (207000)。此时示例的做法是:记录告警日志,完成资源清理后退出(返回成功),而不是以失败状态退出——这在多形态 Runtime 环境下非常实用。
  • 从代码实现看,aclrtSetStreamOverflowSwitchaclrtGetOverflowStatusaclrtResetOverflowStatus均出现在 arch5162_unsupported_acl_api.def 的检索结果中,可以推断部分芯片架构的 Runtime 构建会剔除这些符号,因此在交叉部署场景(如更换设备型号)时务必先做能力探测。

九、总结与延伸阅读

本示例以最小可运行程序演示了 CANN Runtime 流级溢出检测的完整闭环:模式探测与切换(aclrtGetDeviceSatMode/aclrtSetDeviceSatMode)→ 开关配置(aclrtSetStreamOverflowSwitch/aclrtGetStreamOverflowSwitch)→ 异步状态查询(aclrtGetOverflowStatus+aclrtSynchronizeStream+aclrtMemcpy)→ 状态复位(aclrtResetOverflowStatus)→ 逆序资源清理与模式还原,并提供了完备的环境不支持降级路径,可直接作为可靠性检测模块的接入模板。

若要在真实训练/推理链路中落地,可在此基础上于"开启开关"与"查询状态"之间插入实际算子下发,并在before reset/after reset两次查询之间执行一轮关键计算,即可实现对某一阶段数值溢出的事后监控。

延伸阅读建议:

  • 可靠性样例集总览:example/4_reliability/README_en.md
  • 样例工程通用运行指南:example/README_en.md
  • 溢出检测相关 API 声明:include/external/acl/acl_rt.h
  • ACL 实现层与底层调用链:src/acl/aclrt_impl/device.cpp
  • CANN
  • Ascend
  • 人工智能
  • 任务调度

【免费下载链接】runtime

本项目提供CANN运行时组件和维测功能组件。

项目地址:https://gitcode.com/cann/runtime
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 2:10:32

蓝牙GFSK频谱深度解析:从BR/EDR到BLE 5.x的版本差异与测试要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 2:08:15

401 invalid_api_key?TaoToken + Roo Code 这样核对该模型 ID

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 2:06:01

VMware Workstation 17安装Windows10虚拟机全流程与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 2:01:10

VS Code 中 opencode AI 代理插件安装配置与使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 2:01:08

安防监控技术标书实战指南:参数计算、协议实现与等保设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华