TVM- End-to-End Optimization Stack for Deep Learning

news2024/10/5 15:27:48

TVM- End-to-End Optimization Stack for Deep Learning

引言

TensorFlow、MXNet、Caffe 和 PyTorch 等可扩展框架推动了深度学习当前的普及和实用性。然而,这些框架针对范围较窄的服务器级 GPU 进行了优化,将工作负载部署到其他平台(例如手机、嵌入式设备和专用加速器(例如 FPGA、ASIC))需要费力的手动工作。我们提出 TVM,这是一个端到端的优化堆栈,它公开图形级别和运算符级别的优化,为跨不同硬件后端的深度学习工作负载提供性能可移植性。我们讨论了 TVM 解决的深度学习特有的优化挑战:高级运算符融合、跨线程的低级内存重用、映射到任意硬件基元以及内存延迟隐藏。实验结果表明,TVM 可提供跨硬件后端的性能,可与用于低功耗 CPU 和服务器级 GPU 的最先进库相媲美。我们还通过以基于 FPGA 的通用深度学习加速器为目标,展示了 TVM 以新硬件加速器后端为目标的能力。编译器基础设施和 FPGA 加速器设计将开源。

1.简介

深度学习模型现在可以识别图像、处理自然语言并在具有挑战性的策略游戏中击败人类。现代硬件稳步提升的计算能力在深度学习目前在许多问题领域的普遍性和相关性中发挥了突出作用。许多最流行的深度学习框架,如 TensorFlow、MXNet、Caffe 和 PyTorch,通过将支持集中在一小类服务器级 GPU 设备上来利用现代硬件的力量——这种支持取决于高度工程化的使用和供应商特定的 GPU 库。然而,专业深度学习加速器的数量和多样性正在迅速增加。这些加速器带来了采用挑战,因为它们引入了现代编译器和框架无法处理的新硬件抽象。以目前的临时方式为各种硬件后端提供各种深度学习框架的支持是不可持续的。最终,目标是将深度学习工作负载轻松部署到各种硬件目标,包括嵌入式设备、GPU、FPGA 和 ASIC(例如 TPU),这些硬件在内存组织、计算等方面存在显着差异,如下所示图 1. 鉴于这些要求,开发优化框架以将深度学习程序的高级规范降低为适用于任何硬件后端的低级优化代码至关重要。

image-20230411230901505

当前的深度学习框架依赖于计算图中间表示来实现自动微分和动态内存管理等优化 [3, 7, 4]。然而,图级优化通常太高级而无法处理特定于硬件后端的运算符级转换。另一方面,深度学习框架所依赖的当前算子级库过于僵化和专业化,无法轻松跨硬件设备移植。为了解决这些弱点,我们需要一个编译器框架,它可以在图形和运算符级别上提供优化机会,以在硬件后端提供有竞争力的性能

基本优化挑战 用于深度学习的优化编译器需要公开高级和低级优化。我们总结了计算图级别和张量算子级别的四个基本挑战:

  1. 高级数据流重写:不同的硬件设备可能具有截然不同的内存层次结构,因此启用融合运算符和优化数据布局的策略对于优化内存访问至关重要。
  2. 跨线程内存重用:现代 GPU 和专用加速器具有可以跨计算核心共享的内存。传统的无共享嵌套并行模型不再是最优的。优化内核需要线程之间在加载的共享内存上进行协作。
  3. 张量计算内在函数:最新的硬件提供了超越向量运算的新指令,例如 TPU 中的 GEMM 运算符或 NVIDIA Volta 中的张量核心。因此,调度程序必须将计算分解为张量算术内在函数而不是标量或向量代码。
  4. 延迟隐藏:虽然具有同步多线程和自动管理缓存的传统架构隐式地隐藏了现代 CPU/GPU 中的延迟,但专门的加速器设计通常倾向于更精简的控制,并将大部分调度复杂性卸载到编译器堆栈。尽管如此,必须仔细执行调度以隐藏内存访问延迟。

image-20230411230914526

TVM:端到端优化堆栈 我们展示了 TVM(如图 2 所示),这是一种端到端优化编译器堆栈,可降低和微调深度学习工作负载以适应不同的硬件后端。 TVM 旨在分离算法描述、调度和硬件接口。该原则受到 Halide [21] 的计算/调度分离的启发,但通过将调度与目标硬件内在函数分离来扩展该概念。这种额外的分离可以支持新型专用加速器及其相应的新内在函数。

TVM 提供了两个优化层:

一个计算图优化层来解决第一个调度挑战

一个带有新调度原语的张量优化层来解决其余三个挑战

image-20230411231023597

通过组合这些优化层,TVM 可以从大多数深度学习框架中获取模型描述,执行联合高级和低级优化,并为后端(例如 Raspberry Pi、GPU 和 FPGA)生成特定于硬件的优化代码。基于专门的加速器。

我们的论文做出了以下贡献:

• 我们构建了一个端到端的编译优化堆栈,允许将高级框架(包括 Caffe、MXNet、PyTorch、Caffe2、CNTK)中指定的深度学习工作负载部署到不同的硬件后台端(包括 CPU、GPU 和基于 FPGA 的加速器)。

• 我们确定了为跨不同硬件后端的深度学习工作负载提供性能可移植性的主要优化挑战。我们引入了新颖的调度原语以利用跨线程内存重用、新颖的硬件内在函数和延迟隐藏。

• 我们在基于 FPGA 的通用加速器上评估 TVM,以提供有关如何最佳定位专用加速器的具体案例研究。我们的编译器生成的可部署代码与最先进的供应商特定库在性能上具有竞争力,并且可以针对新的专用加速器后端。

在本文的其余部分安排如下。我们在第 2 节中描述了图形优化,在第 3 节中描述了张量运算符调度。我们在这两节中包含了实验结果,以定量评估我们提出的优化。我们在第 4 节讨论运行时支持。我们的端到端评估在第 5 节。相关工作在第 6 节讨论。

2. 优化计算图

计算图: 计算图是深度学习框架中表示程序的常用方法[3、6、7、4]。图 3 显示了一个两层卷积神经网络的示例计算图表示。

image-20230411231846404

image-20230411231904456

在这种高级表示和低级编译器 IR(例如 LLVM)之间,中间数据项是大型多维张量。 TVM 利用计算图表示来应用高级优化:节点表示对静态已知维度的张量的操作,边表示张量操作之间的数据流数据依赖性。

计算图提供了计算任务的全局视图,但避免指定每个计算任务需要如何实现。可以在图上执行静态内存规划过程,以预分配内存来保存每个中间张量结果。这个分配阶段类似于传统编译器中的寄存器分配阶段。与 LLVM IR 类似,可以将计算图转换为功能等效的图以应用优化。例如,可以应用常量折叠传递来预先计算可以静态确定的图形部分,从而节省执行成本。图 4 概述了在 TVM 中实现的新颖图级优化:运算符融合和数据布局转换。

**运算符融合:**将多个运算符融合在一起是一种优化,可以大大减少执行时间,特别是在 GPU 和专用加速器中。这个想法是将多个运算符组合成一个内核,而不将中间结果保存回全局内存。具体来说,我们识别四类图运算符:单射(一对一映射)、缩减、复杂可融合(可以将逐元素映射融合到输出)和不透明(不能融合)。我们应用图 5 中列出的规则将计算图转换为融合版本。图 6 通过比较融合版本和非融合版本在三种工作负载中的性能,展示了这种优化的影响。

Data Layout Transformation 张量运算是计算图的基本运算符。计算中涉及的张量在不同的操作中可能有不同的布局要求。例如,深度学习加速器可能利用 4 × 4张量化操作,要求将数据平铺成 4×4 块以优化访问局部性。图 7 显示了如何转换矩阵的数据布局以适应对其数据进行 2 × 2 张量化操作。优化数据布局从指定每个运算符的首选数据布局开始,因为约束决定了它们在硬件中的实现。如果数据布局不匹配,我们将在生产者和消费者之间执行适当的布局转换

image-20230411232115189

图级优化的局限性 虽然高级数据流图优化可以极大地提高深度学习工作负载的效率,但它们的有效性取决于算子库提供的功能。目前,少数支持算子融合的深度学习框架需要算子库提供融合模式的实现。随着定期引入更多网络运营商,可能融合内核的数量会急剧增加。当以越来越多的硬件后端为目标时,这种方法不再可持续,因为所需的融合模式实现数量与需要支持的数据布局、数据类型和硬件加速器内在函数的数量一起增长。为后端特定运算符的巨大空间手工制作运算符内核是不可行的。为此,我们提出了一种代码生成方法,可以在下一节中生成张量运算符。

3. 优化张量操作

本节介绍 TVM 如何为各种硬件后端生成同一运算符的微调版本。

3. 1 张量表达语言

我们引入了一种数据流张量表达式语言来支持自动代码生成。与高级计算图形语言不同,张量运算的实现是不透明的,每个运算都用索引公式表达式语言描述,如图 8 所示。我们的张量表达式语言借鉴了 Halide [21]、Darkroom 等语言的线索[13] 和炸玉米饼 [18]。我们的张量表达式语言支持 C 等常见语言中常见的算术和数学运算。我们明确引入了交换归约运算符,以轻松安排跨多个线程的交换归约。我们进一步介绍了一种高阶扫描运算符,它可以结合基本的计算运算符来随着时间的推移形成循环计算。 TVM 计算操作还支持张量元组之间的缩减,从而可以轻松支持 argmax 等函数。这种表示可以描述高级数据流图中使用的所有张量操作,并涵盖深度学习中展示的常见模式。

image-20230411232341502

3.2 排程空间

给定张量表达式,为每个硬件后端创建高性能实现仍然具有挑战性。

例如,图 9 突出显示了应用于 CPU、GPU 和深度学习加速器设计的典型优化。每个优化的低级程序都是调度策略不同组合的结果,给内核编写者带来了很大的负担。我们采用从 **Halide [21] 的调度优化中解耦计算描述的原则。**调度是将计算描述降低到后端优化实现的特定规则。这个想法是对调度空间和用于遍历该空间的转换进行正式建模,从而提供生成低级代码的不同方法。 TVM 的调度空间如图 10 所示。为了快速探索调度空间,我们需要提供有效的调度原语来转换调度。图 11 显示了 TVM 中使用的一组通用调度原语。许多这些原语与高性能计算中的实践相呼应。我们采用了 Halide 中有用的调度原语并引入了新的调度原语来应对 GPU 和专用硬件加速器带来的挑战。我们将在接下来的三个小节中详细描述这些原语。

3.3 嵌套并行与协作

并行编程是提高深度学习工作负载中计算密集型内核效率的关键。现代 GPU 提供大规模并行性,要求我们将并行编程模型烘焙到调度转换中。大多数现有解决方案都采用称为嵌套并行程序的并行编程模型,这是一种 fork-join 并行形式。具体来说,我们可以使用并行调度原语来并行化数据并行任务。每个并行任务可以进一步递归细分为子任务,以利用目标架构上的多级线程层次结构(例如,GPU 中的线程组)

image-20230411232527236

image-20230411232550597

image-20230411232600108

image-20230411232607236

我们称此模型为无共享嵌套并行,因为一个工作线程无法在同一并行计算阶段查看其同级线程的数据。兄弟线程之间的交互发生在连接阶段,当子任务完成并且下一阶段可以使用前一阶段产生的数据时。这种编程模型不允许线程相互协作以在同一并行阶段执行集体任务。图 12 提供了一个矩阵乘法示例来说明此限制。 GPU 上的矩阵乘法需要将工作划分为分布在线程组中的块,然后分配给每个单独的线程。在无共享嵌套并行性下,每个线程都必须在缩减阶段独立获取其计算块所需的数据。 1 无共享方法的更好替代方法是跨线程协作获取数据。这种模式在使用 CUDA、OpenCL 和 Metal 等语言的 GPU 编程中广为人知,但尚未在调度原语中实现。我们将内存范围的概念引入到调度空间中,以便可以将一个阶段标记为共享。如果没有内存范围,自动范围推断会将相关阶段标记为线程本地,如图 12 所示。共享任务需要计算组中所有工作线程的依赖关系。我们可以通过使用线程绑定原语在同一组线程中分配加载任务来高效地安排输入数据加载。值得注意的是,我们被迫使用相同的线程来处理除法共享工作负载,因此线程可以在加载和计算阶段持续存在。这种改进的实现需要额外的编译器支持。具体来说,边界推理算法需要能够通过将所有协作线程的任务合并在一起来推断共享任务的边界。此外,需要正确插入内存同步屏障,以保证共享加载的数据对消费者可见。图 14 比较了使用无共享嵌套并行生成与协作生成的 GPU 内核的性能。我们还将 TVM 与 Halide 进行了比较,后者采用无共享嵌套并行方法。我们发现采用这些新的调度原语对于在 GPU 上获得最佳性能至关重要。最后,除了对 GPU 有用之外,内存作用域还允许我们标记特殊的内存缓冲区,并在针对专门的深度学习加速器时创建特殊的降低规则.

image-20230411232652027

3.4 张量化:泛化硬件接口

深度学习工作负载具有很高的算术强度,通常可以分解为张量运算符,如矩阵-矩阵乘法或一维卷积。这些自然分解导致了最近的趋势,即添加超越向量指令的张量计算原语。新兴的计算内在函数非常多样化,包括矩阵-矩阵乘法 [17]、矩阵-向量积 [1] 和一维卷积 [9] 等示例。这些新原语为张量运算符调度带来了新挑战:调度必须使用这些原语才能从加速中获益。我们将其称为张量化问题,类似于 SIMD 架构的矢量化问题。张量化与矢量化有很大不同。张量计算原语的输入是多维的,具有固定或可变长度,并规定了不同的数据布局。更重要的是,我们不能求助于一组固定的原语,因为新的深度学习加速器正在出现,它们都有自己的张量指令。因此,我们需要一种经得起未来考验的解决方案来支持新一代的专用加速器。为了解决这一挑战,我们将硬件接口与时间表分开。具体来说,我们引入了张量内在声明机制。我们可以使用张量表达式语言来声明每个新硬件内在的行为,以及与之相关的降低规则。此外,我们引入了张量化调度原语,以用相应的张量内在函数替换计算单元。编译器将计算模式与硬件声明相匹配,并将其降低为相应的硬件内在函数。图 15 显示了一个张量化示例。张量表达式语言既描述了用户的预期计算描述,也描述了硬件公开的抽象。张量化将时间表与特定硬件原语分离,使 TVM 易于扩展以支持新硬件架构。生成的张紧调度代码与高性能计算中的常见做法一致:将复杂的操作分解为重复的微内核调用序列。因此,我们还可以使用 tensorize 原语来利用手工制作的汇编微内核,这在某些平台上可能是有益的。例如,在 AMD Vega GPU 上将半精度 GEMM 张量为 4 × 4 手工制作的微内核可以产生超过最佳非张量版本 1.5 倍以上的加速.

image-20230411232748479

image-20230411232756156

3.5 编译器支持延迟隐藏

延迟隐藏是指将内存操作与计算重叠以最大化内存和计算利用率的过程。它需要不同的策略,具体取决于目标硬件后端。在 CPU 上,内存延迟隐藏是通过同步多线程 [11] 或硬件预取技术 [16、8] 隐式实现的。 GPU 依靠许多线程扭曲的快速上下文切换来最大化功能单元的利用率 [25]。另一方面,专门的深度学习加速器通常支持更精简的控制,并将此问题卸载到编译器堆栈。图 16 演示了编译器如何显式处理流水线深度学习加速器的数据依赖性,该加速器遵循解耦访问/执行理念 [22、17]。我们假设硬件流水线由可以同时执行的内存和计算阶段组成,其中每个阶段都由独立的指令流控制。显式同步指令用于在流水线阶段之间发送信号以指示给定任务何时完成,以便下一个相关阶段可以开始使用或覆盖数据。这种类型的显式依赖跟踪是在具有 FIFO 队列的硬件中实现的,并为程序员和编译器提供了对硬件任务执行方式的直接控制。通过使用硬件公开的低级同步原语,我们可以有效地隐藏延迟。在如此低的层次上对具有显式同步操作的硬件进行编程是一项艰巨而艰苦的任务。为了减轻程序员的负担,我们提供了一个虚拟线程调度原语,让程序员指定一个高级数据并行程序,TVM 自动将其降低为低级显式数据依赖程序。降低过程如图 17 所示。该算法从高级并行程序开始,然后插入必要的同步指令以保证每个线程内操作的正确执行顺序。最后,所有虚拟线程的操作交织到一个线程中。

image-20230411232844775

降低虚拟线程的正确性 虚拟线程的不正确交错会导致死锁。从概念上讲,在降低之前,每个线程都有自己的私有虚拟指令流。降低过程将这些虚拟指令流的指令映射到有限的物理指令流中。此过程需要正确性保证,因为物理指令流之间的物理消息队列需要在线程之间共享以传递依赖关系。只要按照图 18 中显示的规则保留顺序,我们就可以证明降低是正确的。以下定理给出了该降低过程正确性的充分必要条件

8. 结论

我们的工作提供了一个端到端的堆栈来解决跨各种硬件后端的基本优化挑战。我们希望我们的工作能够鼓励对编程语言、编译的更多研究,并为深度学习系统的硬件协同设计技术开辟新的机会。我们计划开源我们的编译器堆栈和 VITA 设计,以鼓励朝这个方向进一步研究。

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

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

相关文章

SpringBoot中@EnableAsync和@Async注解的使用

目录 1.EnableAsync 注解1.1 配置类使用示例1.2 复制请求上下文 2.用法1:Async 注解2.1 测试Controller2.2 测试Service2.3 测试ServiceImpl2.4.测试 4.用法2:直接使用 taskExecutor 做异步4.1 重新实现:测试ServiceImpl4.2 测试 5.Async异步…

保护模式段描述符

目前为止,内存还是分段模式,所以想要保护内存,就需要保存段。由于CPU的扩展导致了32位的段基地址和段内偏移,所以16位的段寄存器就无法放下这些信息。现在就需要把这些信息放到内存中,这些信息被封装成特定的段描述符。…

Vue进阶-Vue cli项目搭建、项目基本操作、axios的使用、路由、Vuex

Vue进阶 Vue cli 一、Vue cli 概述 CLI 全称是 Commond Line Interface,翻译为命令行界面,俗称脚手架。VueCLI是一个官方发布vue.js项目脚手架。用VueCLI 可快速搭建Vue开发环境以及对应webpack配置。 二、环境搭建 1、下载 node.js 下载地址&…

4 redis高可用

所谓的高可用,也叫HA(High Availability),是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。如果在实际生产中,如果redis只部署一个节点,…

Compiler- 自增运算

我们来看一下C语言中的前自增(i)和后自增(i) 这个经典案例。大家在学习C的时候肯定学过前自增是先自增,然后将结果用于计算;后自增是先参与计算,再增加。 好,看一下这段代码的结果: #include …

PE文件反编译为python脚本流程

1、查壳 DetectltEasy、PeiD查壳 2、脱壳 常见打包工具PyInstaller,脱壳方法 (1)用pyinstxtractor.py脱壳,用”python pyinstxtractor.py 1.exe“命令,生成“.exe文件名_extracted” (2)用…

Python+Qt人脸识别门禁管理系统

程序示例精选 PythonQt人脸识别门禁管理系统 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<PythonQt人脸识别门禁管理系统>>编写代码&#xff0c;代码整洁&#xff0c;规则&am…

1 ROS2介绍与安装

1 ROS2介绍与安装 1.1 Ubuntu配置与ROS2安装1.1.1 Ubuntu22.04安装1.1.2 下载安装ROS21.1.3 配置ROS2环境并测试 1.2 使用VSCode搭建ROS2开发环境1.2.1 安装并配置VSCode1.2.2 创建ROS2工程的方法1.2.3 使用VSCode创建ROS2的C/C项目1.2.4 使用VSCode创建ROS2的Python项目 1.3 R…

Samba配置回收站功能

部门确实需要给Samba配置回收站&#xff0c;查阅了下回收站的资料&#xff0c;配置也挺简单的。 配置说明&#xff1a; 在Samba配置回收站功能中各参数作用如下。 (1) vfs object recycle&#xff1a;载入Samba用于回收站功能的模块recycle.so。 (2) recycle:repository /Pr…

企业如何保护外发文件的数据安全?

随着数字化转变&#xff0c;企业的业务文件大多通过电子形式在内外部流转。这增加了外发文件数据泄露或被篡改的风险&#xff0c;如何保护外发文件安全已成为企业不容忽视的课题。 企业外发文件&#xff0c;特别是电子文件&#xff0c;存在一定的数据安全风险&#xff1a; 文件…

第十二章 外观模式

文章目录 前言一、外观模式基本介绍完整代码DVD类爆米花类投影仪类屏幕类立体声类灯光类家庭影院类进行聚合Client测试类 二、 外观模式在MyBatis框架应用的源码分析三、外观模式的注意事项和细节 前言 一、外观模式基本介绍 完整代码 DVD类 package tanchishell.SJMS.faca…

Layui 2.8.0 正式发布,朴实归来

Layui 是一套开源的 Web UI 组件库&#xff0c;采用自身轻量级模块化规范&#xff0c;遵循原生态的 HTML/CSS/JavaScript 开发模式&#xff0c;极易上手&#xff0c;拿来即用。其风格简约轻盈&#xff0c;而内在雅致丰盈&#xff0c;甚至包括文档在内的每一处细节都经过精心雕琢…

On the Efficacy of Knowledge Distillation 解析

paper&#xff1a;On the Efficacy of Knowledge Distillation 本文的题目是《论知识蒸馏的有效性》&#xff0c;主要是对教师模型并不是越大越好这一现象进行研究&#xff0c;并提出了缓解方法&#xff1a;early stop。 Bigger models are not better teachers 知识蒸馏背…

S32k3系列开发学习(FlexCAN)

前言 由于之前没有接触过CAN总线模块&#xff0c;对这一块的知识仍比较陌生&#xff0c;于是乎想简单梳理一下CAN总线的工作流程&#xff0c;加深理解。 一、CAN是什么&#xff1f; 参考&#xff1a;https://zhuanlan.zhihu.com/p/346696648 二、CAN框架 各模块功能如下&am…

NLP基础:标注器Label Studio的入门使用

目录 一、环境准备 二、操作 文章来源&#xff1a; 简介&#xff1a; Label Studio是一个开源的数据标注工具&#xff0c;它可以用于各种机器学习和深度学习项目。它的主要目的是帮助数据科学家和机器学习工程师快速、高效地标注数据&#xff0c;以构建和训练准确的机器学…

Linux入门---开发的Linux命令手册

Linux 基础知识 基础 启动过程&#xff1a; 内核的引导。运行 init。系统初始化。建立终端 。用户登录系统。 命令介绍 磁盘 #文件#a&#xff1a;相当於 -pdr 的意思&#xff0c;至於 pdr 请参考下列说明&#xff1b;(常用)&#xff1b;f&#xff1a;为强制(force)进行&…

Ubuntu20.04使用多卡训练HyperNetwork模型和LoRA模型全流程及疑难问题解决方案

目录 一. LoRA模型多卡训练1.1 安装xformer等库1.2 设置路径1.3 多卡训练 二. LoRA模型多卡训练疑难报错解决方案多卡训练报错 软硬件配置&#xff1a; CPU: AMD 5800 8core 16Thread GPU: NVIDIA RTX 3090 *1 NVIDIA TITAN RTX *1 OS: Ubuntu20.04 一. LoRA模型多卡训练 1.1 …

JavaScript概述四(DOM文档对象模型)

1.DOM(Document Object Model) 会把网页里面的元素当成对象去操作,包含对象的属性,属性值,方便我们去 操作网页。 整个页面最终会形成一个对象 :document ,页面里面的所有的元素(如 标签 ) 最终都会转换成 js 里面的对象。 1.1 获取页面的元素&#xff08;通过选择器&#xff0…

S32K3系列单片机学习

前言 定时器中断&#xff0c;每个平台的实现方式均有差异&#xff0c;从51单片机通过寄存器配置实现定时器周期计数&#xff0c;再到使用HAL库配置STM32的定时器&#xff0c;他们的实现原理都是大同小异的&#xff0c;只不过不同的平台使用的底层库不同&#xff0c;导致实现的…

公司25k招了一个测试员不会自动化,试用期没过就赶走了...

最近翻了一些网站的招聘信息&#xff0c;把一线大厂和大型互联网公司看了个遍&#xff0c;发现市场还是挺火热的&#xff0c;虽说铜三铁四&#xff0c;但是软件测试岗位并没有削减多少&#xff0c;建议大家有空还是多关注和多投简历&#xff0c;不要闭门造车&#xff0c;错过好…