尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
CANN ops-nn 算子 aclnnGeluBackward 反向接口详解:GELU 梯度计算的实现与调用实践
CANN ops-nn 算子 aclnnGeluBackward 反向接口详解GELU 梯度计算的实现与调用实践【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nnGELUGaussian Error Linear Unit激活函数在 Transformer 类模型中被广泛使用其反向传播梯度的高效计算直接影响训练性能。本文以 CANN 神经网络算子库 ops-nn 中 aclnnGeluBackward 接口文档 为主线结合仓库源码完整讲解该算子的数学原理、两段式 API 用法、参数与返回码约束并深入 Kernel 层剖析其多项式近似实现与 tiling 调度策略。读者阅读后将掌握在 Atlas A2 训练/推理系列产品上正确调用 aclnnGeluBackward 完成 GELU 反向计算的完整方法并理解该算子从 aclnn 接口到底层 NPU 核函数调用的全链路实现。产品支持情况与算子定位aclnnGeluBackward 用于在昇腾 NPU 上完成 GELU 激活函数的反向传播计算其产品支持情况如下产品是否支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品√该算子在 ops-nn 仓库中位于 experimental/activation/gelu_grad 目录贡献记录显示其为新增的 GeluGrad 算子对应底层算子类型OpType为GeluGrad。算子定义为三输入一输出dy反向梯度、x正向输入、y正向输出与输出z输入梯度详细原型信息见 README.md。功能说明与数学原理接口功能aclnnGeluBackward 的功能是完成 GELU 的反向传播根据反向梯度gradOutput与正向输入self计算得到输入self的梯度gradInput。计算公式GELU 正向计算公式为其中 x 可以为标量或者 Tensor$$ Gelu(x)x \cdot \Phi(x)x/2 \cdot [1erf(x/\sqrt{2})] $$其中 erf 为误差函数其级数展开式为$$ erf(x)\frac{2}{\sqrt \pi}\sum^{\infty}_{n0}{\frac{(-1)^n \cdot x^{2n1}}{n! \cdot (2n1)}} $$对正向公式求导后gradInput与gradOutput的关系可以表示为$$ gradInput gradOutput \cdot (\frac{1}{2}\frac{1}{2} \cdot erf(\frac{x}{\sqrt2})\frac{x}{\sqrt{2\pi}} \cdot e^{-\frac{x^2}{2}}) $$即 GELU 的导数为 $\Phi(x) x \cdot \varphi(x)$ 的形式其中 $\Phi$ 为标准正态分布累积分布函数、$\varphi$ 为标准正态分布概率密度函数。GELU 的 tanh 近似计算公式为$$ Gelu(x)0.5x(1tanh(\sqrt{2/\pi}(x0.044715x^3))) $$注意上述公式是接口的数学语义定义。实际 Kernel 实现并不直接计算erf而是采用多项式系数对erf进行逼近详见下文Kernel 实现与近似算法一节因此在极个别边界输入上可能出现与标准erf实现的微小数值差异。两段式函数原型与 CANN 其他 aclnn 算子一致aclnnGeluBackward 采用两段式接口设计必须先调用aclnnGeluBackwardGetWorkspaceSize获取计算所需 workspace 大小以及包含了算子计算流程的执行器再调用aclnnGeluBackward执行计算。第一段接口原型aclnnStatus aclnnGeluBackwardGetWorkspaceSize( const aclTensor *gradOutput, const aclTensor *self, const aclTensor *gradInput, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口原型aclnnStatus aclnnGeluBackward( void *workspace, uint64_t workspace_size, aclOpExecutor *executor, const aclrtStream stream)两个接口的声明位于 aclnn_gelu_backward.h头文件注释中给出了完整的 Mermaid 计算流程gradOutput/self/gradInput先分别经过l0op::Contiguous转为连续 Tensor再送入l0op::GeluGrad计算结果经l0op::ViewCopy写回gradInput。aclnnGeluBackwardGetWorkspaceSize 参数说明参数表参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorgradOutputaclTensor*输入求梯度时的权重即为了将正向输出的 tensor 变为标量所相乘的权重 tensorshape 需要和正向 self 的 shape 满足 broadcast 关系dtype 与 self 的 dtype 满足数据类型推导规则参见互推导关系支持空 TensorFLOAT、FLOAT16、BFLOAT16ND0-8√selfaclTensor*输入GELU 的正向输入值shape 需要和 gradOutput 的 shape 满足 broadcast 关系dtype 与 gradOutput 的 dtype 满足数据类型推导规则参见互推导关系支持空 TensorFLOAT、FLOAT16、BFLOAT16ND0-8√gradInputaclTensor*输出backward 计算的输出为 GELU 正向入参的梯度值即对输入进行求导后的结果dtype 与 self 和 gradOutput 进行数据类型推导后的可转换的数据类型参见互转换关系一致shape 与 gradOutput 和 self 进行 broadcast 后的 shape 一致FLOAT、FLOAT16、BFLOAT16ND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程-----补充说明对于 Atlas 推理系列产品、Atlas 训练系列产品数据类型仅支持 FLOAT、FLOAT16即不包含 BFLOAT16。从源码看上述约束在 aclnn_gelu_backward.cpp 中通过CheckParams逐一落实空指针检查CheckNotNull对gradOutput、self、gradInput三个入参执行OP_CHECK_NULL数据类型推导检查CheckPromoteType调用CheckPromoteTypeGeluBackward支持列表为DT_FLOAT、DT_FLOAT16、DT_BF16见源码第 29-30 行的DTYPE_SUPPORT_LISTshape 与 broadcast 检查CheckShape中OP_CHECK_MAX_DIM限制最大维度对应文档的 0-8 维并通过OP_CHECK_BROADCAST_AND_INFER_SHAPE校验gradOutput与self的可广播性且要求gradInput的 shape 恰好等于二者 broadcast 后的 shape。返回值aclnnStatus返回状态码具体参见 aclnn返回码。第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 gradOutput、self、gradInput 是空指针ACLNN_ERR_PARAM_INVALID161002gradOutput、self、gradInput 的数据类型和数据格式不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002gradOutput、self、gradInput 的维度关系不满足可 broadcast 原则ACLNN_ERR_PARAM_INVALID161002gradOutput、self、gradInput 的数据类型不满足数据类型推导规则从实现来看CheckParams的返回顺序与文档描述一致先判空返回ACLNN_ERR_PARAM_NULLPTR再做 dtype 推导与 shape 校验返回ACLNN_ERR_PARAM_INVALID。aclnnGeluBackward 参数说明参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnGeluBackwardGetWorkspaceSize 获取executor输入op 执行器包含了算子计算流程stream输入指定执行任务的 Stream返回值aclnnStatus返回状态码具体参见 aclnn返回码。第二段接口的实现非常精简见 aclnn_gelu_backward.cpp 第 205-211 行调用框架统一的CommonOpExecutorRun(workspace, workspace_size, executor, stream)完成计算调度。约束说明确定性计算aclnnGeluBackward 默认确定性实现即相同输入在多次运行中产生一致的计算结果利于调试与结果复现。调用示例以下示例代码摘自接口文档演示了完整的调用流程。具体编译和执行过程请参考编译与运行样例。仓库中另有可直接运行的 test_aclnn_gelu_grad.cpp 示例以及通过bash build.sh --run_example gelu_grad eager cust --vendor_namecustom --experimental命令一键运行的方式见 README.md。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_gelu_backward.h #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vectorint64_t shape) { int64_t shape_size 1; for (auto i : shape) { shape_size * i; } return shape_size; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法,资源初始化 auto ret aclInit(nullptr); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclInit failed. ERROR: %d\n, ret); return ret); ret aclrtSetDevice(deviceId); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtSetDevice failed. ERROR: %d\n, ret); return ret); ret aclrtCreateStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtCreateStream failed. ERROR: %d\n, ret); return ret); return 0; } template typename T int CreateAclTensor( const std::vectorT hostData, const std::vectorint64_t shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtMalloc failed. ERROR: %d\n, ret); return ret); // 调用aclrtMemcpy将host侧数据拷贝到device侧内存上 ret aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtMemcpy failed. ERROR: %d\n, ret); return ret); // 计算连续tensor的strides std::vectorint64_t strides(shape.size(), 1); for (int64_t i shape.size() - 2; i 0; i--) { strides[i] shape[i 1] * strides[i 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor aclCreateTensor( shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1. 固定写法device/stream初始化, 参考acl API手册 // 根据自己的实际device填写deviceId int32_t deviceId 0; aclrtStream stream; auto ret Init(deviceId, stream); CHECK_RET(ret 0, LOG_PRINT(Init acl failed. ERROR: %d\n, ret); return ret); // 2. 构造输入与输出需要根据API的接口自定义构造 std::vectorint64_t selfShape {4, 2}; std::vectorint64_t gradOutputShape {4, 2}; std::vectorint64_t gradInputShape {4, 2}; void* selfDeviceAddr nullptr; void* gradOutputDeviceAddr nullptr; void* gradInputDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* gradOutput nullptr; aclTensor* gradInput nullptr; std::vectorfloat selfHostData {0, 1, 2, 3, 4, 5, 6, 7}; std::vectorint gradOutputHostData {1, 1, 1, 1, 1, 1, 1, 1}; std::vectorint gradInputHostData {0, 0, 0, 0, 0, 0, 0, 0}; ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); ret CreateAclTensor( gradOutputHostData, gradOutputShape, gradOutputDeviceAddr, aclDataType::ACL_INT32, gradOutput); CHECK_RET(ret ACL_SUCCESS, return ret); ret CreateAclTensor(gradInputHostData, gradInputShape, gradInputDeviceAddr, aclDataType::ACL_FLOAT, gradInput); CHECK_RET(ret ACL_SUCCESS, return ret); // 3. 调用CANN算子库API需要修改为具体的API uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnGeluBackward第一段接口 ret aclnnGeluBackwardGetWorkspaceSize(gradOutput, self, gradInput, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnGeluBackwardGetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* workspaceAddr nullptr; if (workspaceSize 0) { ret aclrtMalloc(workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(allocate workspace failed. ERROR: %d\n, ret); return ret); } // 调用aclnnGeluBackward第二段接口 ret aclnnGeluBackward(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnGeluBackward failed. ERROR: %d\n, ret); return ret); // 4. 固定写法同步等待任务执行结束 ret aclrtSynchronizeStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtSynchronizeStream failed. ERROR: %d\n, ret); return ret); // 5. 获取输出的值将device侧内存上的结果拷贝至host侧需要根据具体API的接口定义修改 auto size GetShapeSize(gradInputShape); std::vectorfloat resultData(size, 0); ret aclrtMemcpy( resultData.data(), resultData.size() * sizeof(resultData[0]), gradInputDeviceAddr, size * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(copy result from device to host failed. ERROR: %d\n, ret); return ret); for (int64_t i 0; i size; i) { LOG_PRINT(aclnnGeluBackward result[%ld] is: %f\n, i, resultData[i]); } // 6. 释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(gradOutput); aclDestroyTensor(self); aclDestroyTensor(gradInput); // 7. 释放device资源需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(gradOutputDeviceAddr); aclrtFree(gradInputDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例代码关键点解读资源初始化固定写法aclInit完成 ACL 运行时初始化aclrtSetDevice绑定设备aclrtCreateStream创建执行流。示例中deviceId取 0实际使用时需按目标设备填写。Tensor 构造CreateAclTensor模板函数完成 host 数据到 device 内存的搬运aclrtMallocaclrtMemcpy按连续 Tensor 规则计算 strides并通过aclCreateTensor创建aclTensor。本示例中gradOutput与gradInput的 host 数据使用int类型初始化而gradInput以ACL_FLOAT类型创建实际接入真实模型时请保证三者数据类型满足接口的互推导约束FLOAT/FLOAT16/BFLOAT16。两段式调用先调用aclnnGeluBackwardGetWorkspaceSize拿到workspaceSize与executor若workspaceSize 0则在 device 侧aclrtMalloc申请 workspace再调用aclnnGeluBackward(workspaceAddr, workspaceSize, executor, stream)真正执行计算。结果回收与资源释放aclrtSynchronizeStream等待计算完成aclrtMemcpy将结果回拷 host 侧最后依次释放aclTensor、device 内存、workspace、stream并aclrtResetDeviceaclFinalize。从源码看 aclnnGeluBackward 的实现细节第一段接口的计算流程组装aclnn_gelu_backward.cpp 中aclnnGeluBackwardGetWorkspaceSize的核心逻辑为创建执行器通过CREATE_EXECUTOR()创建aclOpExecutor参数校验调用上文所述的CheckParams空 Tensor 短路若self或gradOutput为空 TensorIsEmpty()直接返回workspaceSize 0与执行器不做实际计算形状推断BroadcastInferShape得到gradOutput与self的 broadcast 后 shape隐式类型提升op::PromoteType(gradOutput, self)推导两个输入提升后的公共数据类型promoteType连续性处理三个输入各自经过l0op::Contiguous转为连续 Tensor平台分支在 ASCEND910 ~ ASCEND910E 平台走不广播分支其余平台含 Atlas A2 系列走 broadcast 分支——BroadcastTensor在 shape 不一致时插入l0op::BroadcastTo将gradOutput/self扩展到 broadcast 后的 shape类型转换与计算l0op::Cast将输入转到promoteType再调用l0op::GeluGrad完成梯度计算输出再Cast回gradInput的原 dtype最终经l0op::ViewCopy写回gradInput非连续输出场景由 ViewCopy 完成数据搬移workspace 计算*workspaceSize uniqueExecutor-GetWorkspaceSize()汇总整条 l0 计算链所需的 workspace并通过ReleaseTo(executor)移交执行器。值得说明的是头文件注释中标注的接口输入格式支持FRACTAL_NZ、NC1HWC0、ND三种而接口文档的参数表与底层 GeluGrad 算子定义见下文均限定为 ND 格式从源码看实际计算路径中Contiguous会把各格式统一整理为连续内存视图用户按 ND 构造 Tensor 即可。底层 GeluGrad 算子定义与 shape 推导底层算子 GeluGrad 的定义位于 gelu_grad_def.cpp三个输入dy、x、y与一个输出z数据类型均支持DT_FLOAT、DT_FLOAT16、DT_BF16格式为 ND并注册了ascend910b与ascend310b两种 AICore 配置。其 shape 推导逻辑位于 gelu_grad_infershape.cpp输出z的 shape 直接取输入 0dy的 shape即输出梯度与输入梯度同 shape。tiling 调度策略gelu_grad_tiling.cpp 实现了基于数据量的多核切分策略要点如下依据 SOC 版本选择计算方式ASCEND310B使用 tiling key 1versionNum 1其余平台使用 tiling key 0对应ASCEND910B且 BF16 输入仅支持ASCEND910B/ASCEND310B两个平台以 UB 内存ubSize、block 大小blockSize与数据类型长度推导单次搬运的数据个数tileDataNum当数据量小到可被单核容纳时tileDataNum inputNum仅用 1 个核否则按每个核至少 32B 数据的原则确定参与计算的核数coreNum将输入按 32B 对齐后均匀切分到各核区分大核/小核前tailBlockNum个核多分一块并为每个核计算完整的 tile 搬运次数与尾块数据个数写入GeluGradTilingData通过context-SetBlockDim(coreNum)设置核数并为l0计算链申请系统级 workspace。Kernel 实现与近似算法Kernel 层入口在 gelu_grad.cpp根据 tiling key 实例化KernelGeluGradTYPE_DY, TYPE_X, TYPE_Z, Is0versionNumgelu_grad.h其核心计算流程为CopyIn → Compute → CopyOut流水循环。从源码看Kernel 并未直接调用erf而是用多项式系数对误差函数做逼近再组合出完整梯度公式。以 tiling key 0Is0versionNum trueASCEND910B为例系数与计算步骤为系数COEFF0 -0.0713548162726002527220f、COEFF1 -1.595769121605730711759f、COEFF2 0.2140644488178007f、COEFF3 1.595769121605730711759f、COEFF4 1.0f计算过程先求x^2并分别构造两路多项式exp((COEFF0·x² COEFF1)·x)与(COEFF2·x² COEFF3)·x再通过Adds(…, 1.0)、Duplicate、Div组合出1/(exp(…)1)形式的 sigmoid 结构经若干Mul组合后与dy相乘最后Cast回输出类型CAST_RINT舍入写入z对于 FLOAT16/BFLOAT16 输入先Cast到 float 计算中间量以提升精度计算完成后再Cast回原类型避免低精度中间累积误差。Kernel 通过BUFFER_NUM 2的双缓冲队列隐藏搬移开销并依据 tiling 数据区分大小核与尾块处理保证整段数据被完整计算。单元测试验证仓库提供了基于 gtest 的 op_api 单测 test_aclnn_gelu_grad.cpp覆盖 float32、float16以及 bf16 等场景构造gradOutput为全 1、self取值于 [-1, 1] 的 ND Tensor校验aclnnGeluBackwardGetWorkspaceSize返回ACL_SUCCESS并对输出gradInput设置了 0.001/0.01 量级的精度阈值验证了接口参数校验与数值正确性同时包含 Ascend950 平台用例以及 shape 满足 broadcast 关系如{1,5}与{2,5}的用例。另有 infershape 单测 验证 shape 推导。使用注意事项小结必须两段式调用先aclnnGeluBackwardGetWorkspaceSize后aclnnGeluBackwardworkspace 仅当workspaceSize 0时才需申请shape 约束gradOutput与self需满足 broadcast 关系gradInput的 shape 必须等于二者 broadcast 后的 shapedtype 约束三者数据类型需满足互推导/互转换规则Atlas 推理与训练系列产品上仅支持 FLOAT、FLOAT16支持空 Tensor 与非连续 Tensor空输入直接短路返回非连续输入由框架自动转连续并回写确定性计算默认确定性实现可放心用于需要结果可复现的训练调试场景。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

前端无障碍性实战:从语义化HTML到键盘导航的完整指南

前端无障碍性实战:从语义化HTML到键盘导航的完整指南

1. 无障碍性到底是什么:别把它当成“后补工作”1.1 四类障碍用户画像:你的网站可能正在“赶走”他们前端无障碍性(Accessibility,社区习惯简称为 a11y)听起来像是个特别“正确”但离业务很远的话题,但实际上…

📅 2026/9/20 2:49:08
Hugo Taxonomy 的 Get 方法:从分类对象中按术语名精确取回加权页面集合

Hugo Taxonomy 的 Get 方法:从分类对象中按术语名精确取回加权页面集合

Hugo Taxonomy 的 Get 方法:从分类对象中按术语名精确取回加权页面集合 【免费下载链接】hugo The world’s fastest framework for building websites. 项目地址: https://gitcode.com/gh_mirrors/hu/hugo Taxonomy.Get 是 Hugo 模板引擎中用于从 Taxonomy …

📅 2026/9/20 2:49:08
小爱音箱免费听全网音乐完整指南:XiaoMusic 部署与用法

小爱音箱免费听全网音乐完整指南:XiaoMusic 部署与用法

小爱音箱免费听全网音乐完整指南:XiaoMusic 部署与用法 【免费下载链接】xiaomusic 使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。 项目地址: https://gitcode.com/GitHub_Trending/xia/xiaomusic 上周日早晨,我对客厅的音箱说"小…

📅 2026/9/20 2:44:08
MORE NEWS

更多资讯

📰

GetQzonehistory:3 步把 QQ 空间历史说说完整导出到本地

GetQzonehistory:3 步把 QQ 空间历史说说完整导出到本地 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 翻手机时看到一张大学时拍的照片,想回 QQ 空间找它当时配…

📰

IsaacLab 电机执行器配置速查:DCMotor参数单位与选型避坑一次讲清

IsaacLab 电机执行器配置速查:DCMotor参数单位与选型避坑一次讲清 【免费下载链接】IsaacLab Unified framework for robot learning with multi-physics/renderer support 项目地址: https://gitcode.com/GitHub_Trending/is/IsaacLab 本文是一份 IsaacLab …

📰

Matlab图像去噪实战:从噪声建模到PSNR/SSIM评价与深度学习

简介:一份基于Matlab的图像去除噪声研究PDF,面向Matlab图像处理初学者、课程设计者、本科及研究生科研人员,也适合寻找专业指导类参考文献的读者。资源围绕图像降噪这一核心问题,对比小波滤波、平均值滤波、形态学滤波、中值滤波四…

📰

COSCon十周年首日观察:开源技术风向与项目实践指南

每年一到这时候,社区里的老朋友就开始在群里问:COSCon今年在哪个城市,议程出了没,要不要一起去。COSCon,也就是中国开源年会,是开源圈每年规模最大、也最像“线下认亲会”的活动。今年是第十届,…

📰

WinTools/SoftMgr顽固残留深度清除指南

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

📰

Pandoc `four_space_rule` 扩展解析:plain 输出如何恢复 pandoc 2.0 的四空格列表缩进

文档开发工具CLI 【免费下载链接】pandoc Universal markup converter 项目地址: https://gitcode.com/gh_mirrors/pa/pandoc 点击查看 免费下载 four_space_rule 是 pandoc 中一个"复古"型的 Markdown 扩展,它把列表解析与输出的缩进规则恢复…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬