【寒武纪(6)】MLU推理加速引擎MagicMind,最佳实践(一)

news2024/12/24 20:47:47

文章目录

    • MagicMind 依赖
  • 示例
    • C++ 编程模型
      • sample_ops/sample_add 算子操作
  • 混合精度
  • 部署
    • 多模型部署
    • 单模型多实例部署
    • 多卡部署
  • 最佳实践
    • 1、性能指标
      • 吞吐率
      • 延时
      • 工具 mm_run
      • 性能优化
    • 2内存工具
    • Profiler工具
    • 3性能和精度差异说明

MagicMind 依赖

在这里插入图片描述
MM 是将训练好的模型转换成统一计算图表示,附带有优化、部署能力。
在这里插入图片描述

示例

/usr/local/neuware/samples/magicmind

C++ 编程模型

本示例基于MagicMind C++ API,展示如何构造一个算子,并进行运行部署前的编译工作,包括:

  1. 创建输入tensors。
  2. 构建网络。
  3. 创建模型。
  4. 序列化模型,生成[op_name]_model

sample_ops/sample_add 算子操作

int main(int argc, char **argv) {
  // init
  auto builder = SUniquePtr<magicmind::IBuilder>(magicmind::CreateIBuilder());
  CHECK_VALID(builder);
  auto network = SUniquePtr<magicmind::INetwork>(magicmind::CreateINetwork());
  CHECK_VALID(network);

  // create input tensor
  DataType input1_dtype = DataType::FLOAT32;
  Dims input1_dims = Dims({-1, -1, -1, -1});
  magicmind::ITensor *input1_tensor = network->AddInput(input1_dtype, input1_dims);
  CHECK_VALID(input1_tensor);

  DataType input2_dtype = DataType::FLOAT32;
  Dims input2_dims = Dims({-1, -1, -1, -1});
  magicmind::ITensor *input2_tensor = network->AddInput(input2_dtype, input2_dims);
  CHECK_VALID(input2_tensor);

  // create add node
  IElementwiseNode *Add = network->AddIElementwiseNode(input1_tensor,input2_tensor,IElementwise::ADD);

  //using Add default paramters, you can set each attribute's value.
  //CHECK_STATUS(Add->SetAlpha1([value]));
  //CHECK_STATUS(Add->SetAlpha2([value]));

  // mark network output
  for (auto i = 0; i < Add->GetOutputCount(); i++) {
    auto output_tensor = Add->GetOutput(i);
    CHECK_STATUS(network->MarkOutput(output_tensor));
  }

  // create model
  auto model = SUniquePtr<magicmind::IModel>(builder->BuildModel("add_model", network.get()));
  CHECK_VALID(model);

  // save model to file
  CHECK_STATUS(model->SerializeToFile("add_model"));

  return 0;
}

这段代码是使用C++编写的,它使用了magicmind库(一种深度学习库)来创建一个简单的神经网络模型。以下是代码的逐行解释:

  1. int main(int argc, char **argv) {:这是主函数的开始,所有C++程序的执行都从这里开始。
  2. // init:初始化一些变量。
  3. auto builder = SUniquePtr<magicmind::IBuilder>(magicmind::CreateIBuilder());:创建一个IBuilder对象,用于构建模型。
  4. CHECK_VALID(builder);:检查builder是否有效。
  5. auto network = SUniquePtr<magicmind::INetwork>(magicmind::CreateINetwork());:创建一个INetwork对象,它代表了一个神经网络。
  6. CHECK_VALID(network);:检查network是否有效。
  7. // create input tensor:创建一个输入张量。
  8. DataType input1_dtype = DataType::FLOAT32;:定义输入1的数据类型为FLOAT32。
  9. Dims input1_dims = Dims({-1, -1, -1, -1});:定义输入1的维度。这里的-1表示该维度可以是任意大小。
  10. magicmind::ITensor *input1_tensor = network->AddInput(input1_dtype, input1_dims);:在神经网络中添加一个输入,并获取其对应的ITensor指针。
  11. CHECK_VALID(input1_tensor);:检查input1_tensor是否有效。
  12. 以相同的方式为第二个输入创建ITensor。
  13. // create add node:创建一个add节点,该节点将两个输入相加。
  14. IElementwiseNode *Add = network->AddIElementwiseNode(input1_tensor,input2_tensor,IElementwise::ADD);:在神经网络中添加一个元素级加法节点,将两个输入相加。
  15. //using Add default paramters, you can set each attribute's value.:这行代码注释掉了对Add节点的一些属性的设置。你可以根据需要设置这些属性值。
  16. // CHECK_STATUS(Add->SetAlpha1([value]));:这行代码注释掉了对Add节点Alpha1属性的设置。你可以用实际的值替换[value],或者完全移除这行代码。
  17. // CHECK_STATUS(Add->SetAlpha2([value]));:这行代码注释掉了对Add节点Alpha2属性的设置。你可以用实际的值替换[value],或者完全移除这行代码。
  18. // mark network output:标记网络输出。
  19. for (auto i = 0; i < Add->GetOutputCount(); i++) {:遍历所有Add节点的输出。
  20. auto output_tensor = Add->GetOutput(i);:获取当前输出。
  21. CHECK_STATUS(network->MarkOutput(output_tensor));:将此输出标记为网络的输出。
  22. }:结束for循环。
  23. // create model:创建模型。
  24. auto model = SUniquePtr<magicmind::IModel>(builder->BuildModel("add_model", network.get()));:使用builder和network对象创建一个新的模型,并将其命名为"add_model"。
  25. CHECK_VALID(model);:检查model是否有效。
  26. // save model to file:将模型保存到文件。
  27. CHECK_STATUS(model->SerializeToFile("add_model"));:将模型序列化并保存为"add_model"文件。
  28. return 0;:程序成功执行完毕,返回0。

混合精度

见:【寒武纪(6)】MLU推理加速引擎MagicMind,最佳实践(二)

部署

真实的业务场景中,多模型、多线程、多卡部署业务的需求。本次介绍在满足这些应用场景下的模型部署需求。
在这里插入图片描述

多模型部署

一种典型的业务场景为在同一个MLU设备上同时部署多个模型。在MM 上,用户通过创建多个模型对象来完成多模型部署。首先创建IEngine将指令和常量数据部署Device。可以创建多个Engine,部署到同一个设备或多个设备。基于IContext 执行环境,绑定Context输入和输出地址,最后使用Enqueue执行。

例如我们部署Resnet50和Efficientnet两个模型。

IMode *modelA = CreateModel();
modelA ->DeserializeFromFile(resnet_model_file);
IEngine *engineA = modelA ->CreateIEngine();
…
IMode *modelB = CreateModel();
modelB ->DeserializeFromFile(effi_model_file);
IEngine *engineA = modelB ->CreateIEngine();
…

对于多线程部署多模型, CreateIEngine后不允许使用fork创建信息的子进程。因为主进程运行时有部分全局资源,fork的子进程为无效拷贝,导致无法运行。需要使用spawn创建新进程。
在这里插入图片描述

单模型多实例部署

另一种典型的业务,同一个MLU设备上同时单个模型并发地处理不同地输入实例,不同实例共享该设备地指令和常量数据。通过一个Engine创建多个Context。并发执行不同实例。

1、实例A和实例B运行在不同Context上面。
2、实例A和实例B使用不同队列,可以在MLU并行执行(如果物理资源足够地情况下)
3、为了进一步加速上面地代码,主机端可以启多个线程并发执行两个不同地实例。
4、 Context是互斥资源。多线程使用Enqueue异步调用Context是不允许。

在这里插入图片描述

多卡部署

一种典型的业务场景为,单个模型部署到多个MLU设备,实现数据并行。
通过同一个模型,创建不同Engine,部署到多个设备上。
1、引擎A和引擎B运行在不同MLU上面。
2、共享主机端指令、模型数据,主机内存不会随着引擎数增加而明显增长。

在这里插入图片描述

最佳实践

1、性能指标

在这里插入图片描述

吞吐率

单位时间可以完成多少推理请求。为了实现高吞吐率,业务层需要实现三级流水,让不同请求之间的计算和内存拷贝并行起来。
在这里插入图片描述

在这里插入图片描述

延时

每次推理请求的总响应时间,
在这里插入图片描述

工具 mm_run

在这里插入图片描述在这里插入图片描述
1、c 编程使用clock_gettime() 函数。
2、CNRT 提供的Notifier 接口统计异步队列执行时间。具体看手册说明《Cambricon-MagicMind-Best-Practices-Guide-CN-v1.5.0.pdf》

在这里插入图片描述

性能优化

1、使用混合精度。追求性能,使用qint8_mixed)float16进行推理。具体设置为在build_config 配置精度percision_config。

2、BatchSize 调优
单个推理示例IContext 独占 MLU 时,增大 batch size 提升吞吐率。原因是:

  • 小batch size 无法充分利用MLU 核,增大batchsize可以提升利用率
  • 一些公共的开销,如从DDR 加载常量数据,能通过增大数据并行度(batch size)被更好的利用。

如果业务对延时要求不那么严格,可尝试等待一段时间,将T时间内的积攒一起提交给IContext 处理,提升吞吐率。

2、batch size 对齐
这个和硬件有关,将 batch size 设置为硬件cluster 的整数倍能提升吞吐率。设备支持的最大cluster 可以通过cndrv的cnDeviceGetAttribute 获得。

3、多实例并发
多个 IContext 并发提升吞吐率。用一个IEngine 创建多个推理实例,多个实例可以绑定不同的cnrtQuene,并发地执行在同一个设备。

有如下风险需要注意:

  • 占用更多地cpu和mlu资源
  • 不同实例之间会争抢cluster 资源,导致每个实例地推理延时存在波动,某些情况下回导致整体地吞吐率还不如使用单个实例增大batchsize。

对于多实例并大地资源冲突地手段是为每个推理实例划分独立的计算资源,当前寒武纪的驱动API 已经提供轻量级的计算资源隔离机制,visible cluster。可以绑定当前线程所使用的物理cluster,做到资源独占

4、模型输入的形状
设置固定形状时,网络输入维度是固定的,传入的Dims 是固定的,magicmind 会专门做统一计算图编译优化。在buildConfig 中的graph_shape_mutable 设置为false。

如果设置为可变,可以设置维度最大值和最小值,有助于优化。
在这里插入图片描述

5、使用偏好的物理布局
大部分硬件在架构设计上会有偏好布局,在软件层面合理安排数据的物理布局可以极大的提升系统性能,降低系统功耗 。例如选择NHWC 和NCHW布局。(对这个不懂的可以看我之前的文章《OpenCV读取图像时按照BGR的顺序HWC排列,PyTorch按照RGB的顺序CHW排列》),

推荐如下:
1、杜宇物理布局敏感的算子,尽可能选择channel last物理布局,NHWC
2、整个网络的输入和输出,尽可能选择channel last 布局,NHWC

在这里插入图片描述

输入输出,pytorch 导入的模型,默认布局是NCHW,在预处理允许输出布局为NHWC数据的情况下,我们可将推理部分的输入和输出的物理布局调整为NHWC,从而提升推理性能。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

6、优化预处理
将部分预处理操作合并到推理模型,尝试使用算子融合,提升推理加速。目前支持的预处理为:减均值除方差(标准化)

(1)如果对输出结果范围有要求,用归一化。   (2)如果数据较为稳定,不存在极端的最大最小值,用归一化。
  (3)如果数据存在异常值和较多噪音,用标准化,可以间接通过中心化避免异常值和极端值的影响。

在这里插入图片描述

可在BuildConfig 中设置insert_bn_before_firstnode 配置来使用该功能,下面展示一下通过一个json文件实现:
在这里插入图片描述

7、提供运行时资源提升算子融合性能,BuildConfig 中配置archs 或produce_names 进一步提升网络运行时性能。
在这里插入图片描述
在这里插入图片描述

8、图优化
图优化策略可提升整体推理性能,但是部分图优化策略可能会改变计算顺序,影响数据分布。某些看似等价的数学变换,在使用低精度推理的情况下,存在精度溢出的可能性,影响网络最终精度。因此MagicMind会将某些会影响精度的图优化策略默认设置为关闭,并提供使能配置,允许用户权衡精度和性能。

在这里插入图片描述

9、特定网络优化

对Transformer 和BERT ,模型的输入序列长度不超过128,支持官方的Transformer-Base 和oytorch官方的Bert-base/large ,会特定的统合优化Pass 会被使能,模型的网络片段替换为高度优化的底层算子。
在这里插入图片描述
10、自定义算子
用户通过BANG C 语言实现高性能MLU算子,实现定制化的性能优化

11、使能 Kernel capture
存在某些场景,主机下发任务的速度比MLU执行任务的速度慢,从而导致MLU空闲,整体性能不理想。使用kernel capture 功能,可以在首先场景下环节主机端下发任务慢的问题。原理如下:在模型的kernel 执行顺序和kernel参数固定的情况下,提前执行模型中可以被捕获的kernel 并缓存对应的执行参数,在后续推理迭代中一致复用捕获的kernel以及执行参数,洁身主机端下发任务的时间。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述

12、 使用 conv scale fusion
在某些场景,例如分通道量化网络中,会在每个卷积算子后面添加一个乘法加法孙子,此时由于卷积算子无法直接融合乘法加法算子,性能较融合差,才外还会阻塞器后算子向前融合,例如导致后续出现Relu/ReluN 算子也无法融合进卷积算子,性能较差,此时可以尝试 convs cale fusion。

该算子会改变内部运算顺序,可能影响精度结果。

图优化策略可提升整体推理性能,但是部分图优化策略可能会改变计算顺序,影响数据分布。某些看似等价的数学变换,在使用低精度推理的情况下,存在精度溢出的可能性,影响网络最终精度。因此MagicMind会将某些会影响精度的图优化策略默认设置为关闭,并提供使能配置,允许用户权衡精度和性能。

13、conv qcast fusion

在量化网络中,将卷积算子的scale 参数与量化转数的scale 参数进行折叠,折叠过程中将转数算子的scale参数作用于卷积算子大的bias 数据上,使卷积和量化转数算子融合。会影响精度。支持持普通Relu。

在这里插入图片描述

14、设置计算偏好模式

有 自动,fast 和高精度三种模式。

15、绑定CPU 核心
减少CPU 上下文切换。使用 Linux 的 taskset 命令将不同进程设置成亲和到不同CPU 上运行,目前已知的会争抢CPU 资源的两个进程是 MagicMind 部署接口业务进程和寒武纪驱动进程 sbts_work。绑定到CPU 如下:
在这里插入图片描述

16、AutoTuning 优化

该功能是在模型编译阶段,通过在MLU 板卡上抓取融合算子的运行时间,为后续算子融合的评估流程提供真实的算子运行时间数据,使得系统在算子融合是选择运行最快的算子融合方案,最终提升模型性能。

实验证明,大幅度提升性能
包含depthwise convoution的模型,例如 inception -V4
包含多分枝结构的模型,看i如SENet50

在这里插入图片描述
在这里插入图片描述

2内存工具

在这里插入图片描述
1、测量CPU 内存占用
使用valgrind Massif 堆栈分析器测量程序堆栈大小,使用Massif-Visualizer 浏览数据图表
在这里插入图片描述以下是运行 ResNet50设置混合精度的CPU内存峰值379M:

在这里插入图片描述

在这里插入图片描述

2、测量 MLU 内存占用
使用QueryConstDataSize 接口
在这里插入图片描述在这里插入图片描述
3、优化内存占用

  • 提前释放常量数据
  • 算子库裁剪

在这里插入图片描述在这里插入图片描述在这里插入图片描述

Profiler工具

定位是主机侧问题、或者Device侧问题,或者定位内存占用问题。使用Profiler工具,用于并行度调优、性能分析和图形化性能展示。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3性能和精度差异说明

当用户通过不同的 解析Parse—> 校准(Calibrate)---->构图(Build)---->运行(Run),多相同模型会产生不同差距。可以从三个阶段分别说明原因。

1、解析
解析颗粒度可能存在差异。

  • 用户原生矿机和MagicMind 的解析和各自的图优化差异,例如MM使用公共子表达式CSE消除优化,消除冗余表达,原生框架不一致时,会产生差异,导致网络性能差异,

  • 原生框架对MM API的映射和MM解析原生框架API存在差异

2、校准
量化参数产生过程前的优化差异可能通过改变算网络结构影响设计量化算子(Conv,MatMul)的输入数据的分布,从而导致量化统计和参数的差异。
针对QAT/用户量化校准/MMjiaozhun 等情况,可能存在以下原因:

  • Conv-BatchNorm/Scale 结构的执行顺序变更,将乘加挪移至滤波器/bias 后,该优化在量化统计前发生将影响统计结果。
  • Concat算子链接多个Conv-MatMul乘加激活分支,将Contact挪至滤波器,该优化在量化参数统计前发生将影响统计结果。
    上述或类似于统计前可完成的优化,无法在量化统计后的量化网络完成,否者会带来精度的较大损失。

3、构图
构图姐u但根据用户的配置项对网络进行修改从而导致性能/精度差异,如果两种构图方式性能/精度存在差异,需要核对以下方面是否对齐

  • 模型输入形状是否固定。在这里插入图片描述

  • 图像预处理存在差异。是否使能图像预处理(输入数据归一化)功能,会导致图结构差异。在这里插入图片描述

  • 输入输出及算子物理布局差异。是否构图阶段对网络的输入和输出的Layout进行转换会影响图结构从而影响性能。

  • 构图计算精度差异。例如,MM使用half精度推理模型,会将可常量折叠的分支使用高精度float计算,而此时用户构图使用half计算可常量折叠的分支,会带来两者的精度差异。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1275645.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

异常详解~

Java提供的异常机制使程序的变得更加健壮【健壮性】&#xff0c;程序不会那么容易崩溃 异常详解 1.异常的基本概念 Java语言中&#xff0c;将程序执行过程中发生的不正常的情况称为异常 注&#xff1a;程序中的语法错误和逻辑错误不是异常 2.一个小case快速了解异常 public c…

FileInputStream 与 FileOutputStream

IO流&#xff1a;存贮和解决数据的存储方案 以上都是抽象类&#xff0c;不能创建实例 FileOutputStream 操作本地的文件&#xff0c;把文件写入本地磁盘 步骤&#xff1a; 1.创建字节输出流对象 参数是字符串表示的路径或者是File对象都是可以的如果文件不存在会创建一个新的…

ChatGPT探索:提示工程详解—程序员效率提升必备技能【文末送书】

文章目录 一.人工智能-ChatGPT1.1 ChatGPT简介1.2 ChatGPT探索&#xff1a;提示工程详解1.2 提示工程的优势 二.提示工程探索2.1 提示工程实例&#xff1a;2.2 英语学习助手2.3 Active-Prompt思维链&#xff08;CoT&#xff09;方法2.4 提示工程总结 三.文末推荐与福利3.1《Cha…

防孤岛装置在光伏发电、燃气发电等新能源并网供电系统的应用

• AM5SE-IS防孤岛保护装置主要适用于35kV、10kV及低压380V光伏发电、燃气发电等新能源并网供电系统。 • 当发生孤岛现象时&#xff0c;可以快速切除并网点&#xff0c;使本站与电网侧快速脱离&#xff0c;保证整个电站和相关维护人员的生命安全**。 保护功能** ● 三段式过流…

【2024年趋势】推荐5个好用的产品手册制作工具

随着科技的快速发展&#xff0c;人们对于网站产品手册的需求也日益增加。一份详细且易用的产品手册可以帮助用户更好地了解和使用产品&#xff0c;提高用户满意度和忠诚度。之前整理了一期关于2023年我推荐的一些知识库软件&#xff0c;已经12月了&#xff0c;最近我也去关注了…

Ranger安装和使用

Ranger部署 1.准备 1.1 编译 Ranger编译&#xff08;已经编译过的话&#xff0c;直接看1.2&#xff09; 1.1.1 准备到Ranger官网下载ranger的源码&#xff1a;http://ranger.apache.org/download.html 1.1.2 Ranger编译的过程实在非虚拟机环境下完成的&#xff0c;下载好r…

简单了解下IP的全球划分【笔记】

国际互联网号码分配机构 (The Internet Assigned Numbers Authority&#xff0c;简称IANA&#xff09;。它是互联网名称与数字地址分配机构&#xff08;The Internet Corporation for Assigned Names and Numbers&#xff0c;简称ICANN&#xff09;旗下的一个机构&#xff0c;主…

wpf devexpress 使用IDataErrorInfo实现input验证

此处下载源码 当form初始化显示&#xff0c;Register按钮应该启动和没有输入错误应该显示。如果用户点击注册按钮在特定的输入无效数据&#xff0c;form将显示输入错误和禁用的注册按钮。实现逻辑在标准的IDataErrorInfo接口。请查阅IDataErrorInfo接口&#xff08;System.Com…

机器学习(2)回归

0.前提 上一期&#xff0c;我们简单的介绍了一些有关机器学习的内容。学习机器学习的最终目的是为了服务我未来的毕设选择之一——智能小车&#xff0c;所以其实大家完全可以根据自己的需求来学习这门课&#xff0c;我做完另一辆小车后打算花点时间去进行一次徒步行&#xff0…

【ONNX】多个ONNX 模型合并为一个模型

ONNX 模型直接合并&#xff0c;输入和输出不一致也可以&#xff0c;各自输入输出各自的 示例代码 import onnxruntime# version : 1.16.0 import onnxdef log_model(model):model_1_outs {o.name for o in model.graph.output}model_1_ins {i.name for i in model.graph.in…

【Web】NISACTF 2022 个人复现

目录 ①easyssrf ②babyupload ③ level-up ④bingdundun~ 明天就新生赛了&#xff0c;练套题保持下手感吧 &#xff08;文章只选取了一部分&#xff09; ①easyssrf 输入/flag 输入file:///fl4g 访问/ha1x1ux1u.php ?filephp://filter/convert.base64-encode/resource/…

PyQt基础_012_对话框类控件QInputDialog

基本操作 import sys from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import *class InputdialogDemo(QWidget):def __init__(self, parentNone):super(InputdialogDemo, self).__init__(parent)layout QFormLayout()self.btn1 QPushButton(&qu…

YOLOv8改进 | 2023 | AKConv轻量级架构下的高效检测(可变核卷积)

一、本文介绍 本文给大家带来的改进内容是AKConv&#xff08;可变核卷积&#xff09;是一种创新的卷积神经网络操作&#xff0c;它旨在解决标准卷积操作中的固有缺陷&#xff08;采样形状是固定的&#xff09;&#xff0c;AKConv的核心思想在于它为卷积核提供了任意数量的参数…

Android超简单实现-即时更新Toast(可直接复制)

效果 实现 第一步、封装个工具类ToastUtil.class&#xff08;可直接复制拿走&#xff09; public class ToastUtil {private static Toast mToast null; // toast对象&#xff0c;用于判断是否第一次使用/*** 即时更新Toast* param content content* param message 消息内容…

FLASK博客系列4——再谈路由

最近好像拖更有点久了。抱歉抱歉~ 今天我们继续来聊聊路由&#xff08;其实就是我上次偷懒剩下一点没讲完&#xff09;。 通过上次的文章&#xff0c;我们基本了解了Flask中的路由&#xff0c;是不是比较简单呢&#xff1f;别急&#xff0c;今天来点猛料。 一、路由之HTTP方法绑…

在CentOS系统下的Tomcat8.5或9安装SSL证书

您可以在CentOS系统下的Tomcat服务器安装SSL证书&#xff0c;实现通过HTTPS安全访问Web服务。本文介绍如何CentOS系统下Tomcat 8.5或9安装SSL证书。 环境准备 操作系统&#xff1a;CentOS 7.6 64位 Web服务器&#xff1a;Tomcat 8.5或9 说明 Tomcat服务器需要提前安装JDK环…

C++基础 -25- 动态多态

静态多态在程序编译的时候&#xff0c;确定将要执行的状态。 动态多态在程序运行的时候&#xff0c;才能确定执行的状态。 下面举例实现动态多态 work函数接口通过传参不同做不同的工作 #include "iostream"using namespace std;class person {public:person(){}vi…

10个让UI设计更轻松的工具

UI设计软件对设计师来说非常重要。UI设计工具的使用是否直接影响到最终结果的质量&#xff0c;然后有人会问&#xff1a;UI界面设计使用什么软件&#xff1f;这里有一些UI设计师和那些对UI设计感兴趣的朋友列出了10个易于使用和免费的UI设计软件。 即时设计 即时设计是一款免…

《曾国藩传》:崇尚笨拙的人生哲学

哈喽啊&#xff0c;大家好&#xff0c;我是雷工&#xff01; 以前读书喜欢读小说&#xff0c;喜欢看《我从你的全世界路过》《云间有个小卖铺》这些轻松的小说&#xff0c;读起来很轻松。 随着年龄增长&#xff0c;阅历的增加开始喜欢读历史&#xff0c;读人物传记&#xff0c;…

【涂鸦T2-U】2、添加光感bh1750

文章目录 前言一、基础介绍二、电路图2.1 电路图12.2 电路图2——实际采用 三、代码四、编译五、刷机六、测试结果小结 前言 本章介绍如何在涂鸦T2-U开发板上添加光感bh1750驱动并实现定时读取数据。 一、基础介绍 BH1750( GY-302 )光照传感器 这篇文章有bh1750的基础介绍。…