news 2026/9/20 2:49:01

CANN ops-nn 算子 aclnnGeluBackward 反向接口详解:GELU 梯度计算的实现与调用实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN ops-nn 算子 aclnnGeluBackward 反向接口详解:GELU 梯度计算的实现与调用实践

CANN ops-nn 算子 aclnnGeluBackward 反向接口详解:GELU 梯度计算的实现与调用实践

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

GELU(Gaussian 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 [1+erf(x/\sqrt{2})] $$

其中 erf 为误差函数,其级数展开式为:

$$ erf(x)=\frac{2}{\sqrt \pi}\sum^{\infty}_{n=0}{\frac{(-1)^n \cdot x^{2n+1}}{n! \cdot (2n+1)}} $$

对正向公式求导后,gradInputgradOutput的关系可以表示为:

$$ 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(1+tanh(\sqrt{2/\pi}(x+0.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)非连续Tensor
gradOutput(aclTensor*)输入求梯度时的权重,即为了将正向输出的 tensor 变为标量所相乘的权重 tensorshape 需要和正向 self 的 shape 满足 broadcast 关系;dtype 与 self 的 dtype 满足数据类型推导规则(参见互推导关系);支持空 TensorFLOAT、FLOAT16、BFLOAT16ND0-8
self(aclTensor*)输入GELU 的正向输入值shape 需要和 gradOutput 的 shape 满足 broadcast 关系;dtype 与 gradOutput 的 dtype 满足数据类型推导规则(参见互推导关系);支持空 TensorFLOAT、FLOAT16、BFLOAT16ND0-8
gradInput(aclTensor*)输出backward 计算的输出,为 GELU 正向入参的梯度值,即对输入进行求导后的结果dtype 与 self 和 gradOutput 进行数据类型推导后的可转换的数据类型(参见互转换关系)一致;shape 与 gradOutput 和 self 进行 broadcast 后的 shape 一致FLOAT、FLOAT16、BFLOAT16ND0-8
workspaceSize(uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小-----
executor(aclOpExecutor**)输出返回 op 执行器,包含了算子计算流程-----

补充说明:对于 Atlas 推理系列产品、Atlas 训练系列产品,数据类型仅支持 FLOAT、FLOAT16(即不包含 BFLOAT16)。

从源码看,上述约束在 aclnn_gelu_backward.cpp 中通过CheckParams逐一落实:

  1. 空指针检查CheckNotNullgradOutputselfgradInput三个入参执行OP_CHECK_NULL
  2. 数据类型推导检查CheckPromoteType调用CheckPromoteTypeGeluBackward,支持列表为DT_FLOATDT_FLOAT16DT_BF16(见源码第 29-30 行的DTYPE_SUPPORT_LIST);
  3. shape 与 broadcast 检查CheckShapeOP_CHECK_MAX_DIM限制最大维度(对应文档的 0-8 维),并通过OP_CHECK_BROADCAST_AND_INFER_SHAPE校验gradOutputself的可广播性,且要求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_name=custom --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::vector<int64_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::vector<T>& hostData, const std::vector<int64_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::vector<int64_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::vector<int64_t> selfShape = {4, 2}; std::vector<int64_t> gradOutputShape = {4, 2}; std::vector<int64_t> gradInputShape = {4, 2}; void* selfDeviceAddr = nullptr; void* gradOutputDeviceAddr = nullptr; void* gradInputDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* gradOutput = nullptr; aclTensor* gradInput = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<int> gradOutputHostData = {1, 1, 1, 1, 1, 1, 1, 1}; std::vector<int> 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::vector<float> 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; }

示例代码关键点解读

  1. 资源初始化(固定写法)aclInit完成 ACL 运行时初始化,aclrtSetDevice绑定设备,aclrtCreateStream创建执行流。示例中deviceId取 0,实际使用时需按目标设备填写。
  2. Tensor 构造CreateAclTensor模板函数完成 host 数据到 device 内存的搬运(aclrtMalloc+aclrtMemcpy),按连续 Tensor 规则计算 strides,并通过aclCreateTensor创建aclTensor。本示例中gradOutputgradInput的 host 数据使用int类型初始化,而gradInputACL_FLOAT类型创建,实际接入真实模型时请保证三者数据类型满足接口的互推导约束(FLOAT/FLOAT16/BFLOAT16)。
  3. 两段式调用:先调用aclnnGeluBackwardGetWorkspaceSize拿到workspaceSizeexecutor;若workspaceSize > 0则在 device 侧aclrtMalloc申请 workspace;再调用aclnnGeluBackward(workspaceAddr, workspaceSize, executor, stream)真正执行计算。
  4. 结果回收与资源释放aclrtSynchronizeStream等待计算完成,aclrtMemcpy将结果回拷 host 侧;最后依次释放aclTensor、device 内存、workspace、stream,并aclrtResetDevice+aclFinalize

从源码看 aclnnGeluBackward 的实现细节

第一段接口的计算流程组装

aclnn_gelu_backward.cpp 中aclnnGeluBackwardGetWorkspaceSize的核心逻辑为:

  1. 创建执行器:通过CREATE_EXECUTOR()创建aclOpExecutor
  2. 参数校验:调用上文所述的CheckParams
  3. 空 Tensor 短路:若selfgradOutput为空 Tensor(IsEmpty()),直接返回workspaceSize = 0与执行器,不做实际计算;
  4. 形状推断BroadcastInferShape得到gradOutputself的 broadcast 后 shape;
  5. 隐式类型提升op::PromoteType(gradOutput, self)推导两个输入提升后的公共数据类型promoteType
  6. 连续性处理:三个输入各自经过l0op::Contiguous转为连续 Tensor;
  7. 平台分支:在 ASCEND910 ~ ASCEND910E 平台走不广播分支,其余平台(含 Atlas A2 系列)走 broadcast 分支——BroadcastTensor在 shape 不一致时插入l0op::BroadcastTogradOutput/self扩展到 broadcast 后的 shape;
  8. 类型转换与计算l0op::Cast将输入转到promoteType,再调用l0op::GeluGrad完成梯度计算,输出再CastgradInput的原 dtype,最终经l0op::ViewCopy写回gradInput(非连续输出场景由 ViewCopy 完成数据搬移);
  9. workspace 计算*workspaceSize = uniqueExecutor->GetWorkspaceSize()汇总整条 l0 计算链所需的 workspace,并通过ReleaseTo(executor)移交执行器。

值得说明的是,头文件注释中标注的接口输入格式支持FRACTAL_NZNC1HWC0ND三种,而接口文档的参数表与底层 GeluGrad 算子定义(见下文)均限定为 ND 格式,从源码看实际计算路径中Contiguous会把各格式统一整理为连续内存视图,用户按 ND 构造 Tensor 即可。

底层 GeluGrad 算子定义与 shape 推导

底层算子 GeluGrad 的定义位于 gelu_grad_def.cpp:三个输入dyxy与一个输出z,数据类型均支持DT_FLOATDT_FLOAT16DT_BF16,格式为 ND,并注册了ascend910bascend310b两种 AICore 配置。

其 shape 推导逻辑位于 gelu_grad_infershape.cpp:输出z的 shape 直接取输入 0(dy)的 shape,即输出梯度与输入梯度同 shape。

tiling 调度策略

gelu_grad_tiling.cpp 实现了基于数据量的多核切分策略,要点如下:

  • 依据 SOC 版本选择计算方式:ASCEND310B使用 tiling key 1(versionNum = 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 实例化KernelGeluGrad<TYPE_DY, TYPE_X, TYPE_Z, Is0versionNum>(gelu_grad.h),其核心计算流程为CopyIn → Compute → CopyOut流水循环。

从源码看,Kernel 并未直接调用erf,而是用多项式系数对误差函数做逼近,再组合出完整梯度公式。以 tiling key 0(Is0versionNum = true,ASCEND910B)为例,系数与计算步骤为:

  • 系数:COEFF0 = -0.0713548162726002527220fCOEFF1 = -1.595769121605730711759fCOEFF2 = 0.2140644488178007fCOEFF3 = 1.595769121605730711759fCOEFF4 = 1.0f
  • 计算过程:先求x^2并分别构造两路多项式exp((COEFF0·x² + COEFF1)·x)(COEFF2·x² + COEFF3)·x,再通过Adds(…, 1.0)DuplicateDiv组合出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 推导。

使用注意事项小结

  1. 必须两段式调用:先aclnnGeluBackwardGetWorkspaceSizeaclnnGeluBackward,workspace 仅当workspaceSize > 0时才需申请;
  2. shape 约束gradOutputself需满足 broadcast 关系,gradInput的 shape 必须等于二者 broadcast 后的 shape;
  3. dtype 约束:三者数据类型需满足互推导/互转换规则,Atlas 推理与训练系列产品上仅支持 FLOAT、FLOAT16;
  4. 支持空 Tensor 与非连续 Tensor:空输入直接短路返回,非连续输入由框架自动转连续并回写;
  5. 确定性计算:默认确定性实现,可放心用于需要结果可复现的训练调试场景。

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

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

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

付费墙与内容访问限制:规则内解锁文章全文的实用策略

/* 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:46:23

MySQL UNION ALL 用法详解:结果集合并、去重与性能优化技巧

1. 先把UNION ALL的定位搞清楚&#xff1a;纵向拼接&#xff0c;不是横向拼接1.1 一句话说清它在做什么mysql里做结果合并&#xff0c;大家最常用到的就是UNION ALL。它的作用可以用一句话概括&#xff1a;把多个SELECT查询结果按行上下堆在一起&#xff0c;拼成一个更大的结果…

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

博图Openness七天打通:环境配置、对象树与自动化工程生成

从我做自动化项目那年开始&#xff0c;TIA Portal 就成了每天都在用的工具。后来接触的项目越来越大&#xff0c;几十台变频器、上千个 IO 点、标准控制逻辑重复出现&#xff0c;每天花在“拖拽组态”上的时间越来越多&#xff0c;我终于开始注意到 TIA Portal Openness 这个接…

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

VxLAN为何撑不起AI算力集群?SRv6确定性网络如何破局

/* 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:43:30

AI编程工具选型指南:Vibe Coding实战对比与避坑建议

最近一段时间&#xff0c;我朋友圈里聊得最多的一个词&#xff0c;就是 Vibe Coding。你说它是新概念吧&#xff0c;其实核心思路早就有了——用自然语言描述需求&#xff0c;让 AI 把代码写出来&#xff0c;人主要负责 review、修修补补和调整方向。但这东西真正落地之后&…

作者头像 李华