TorchAcc:基于 TorchXLA 的分布式训练框架

演讲人:林伟,阿里云研究员,阿里云人工智能平台 PAI 技术负责人

本文旨在探讨阿里云 TorchAcc,这是一个基于 PyTorch/XLA 的大模型分布式训练框架。

过去十年 AI 领域的显著进步,关键在于训练技术的革新和模型规模的快速攀升。尽管大模型展现了堪比人类的理解力,但其训练却对算力提出了极高的要求。唯有配备充足的计算资源,方能在海量数据上有效训练大模型,确保其在有限时间内实现优质收敛。

图片

图片来源于 GTC 2024大会China AI Day 线上专场的演讲《TorchAcc:基于TorchXLA的分布式训练框架》

根据上图左侧图表显示,过去五年,大模型规模的增长态势尤为突出,平均每两年大小翻 15 倍;而对于 Transformer 为代表的语言模型以及多模态模型而言,其规模膨胀速度更加惊人,每隔两年以 750 倍剧增。对比之下,右侧图表揭示了一个明显的矛盾点:不论是单个 GPU 的计算能力抑或是 GPU 显存容量的发展速度,都无法跟上模型规模如此急剧的扩张步伐。这一现实状况直接催生了对分布式训练的迫切需求。分布式训练不再局限于以往单纯的数据并行模式,而是在此基础上,更加重视并采取模型并行策略,以弥补单个计算单元算力与存储提升速度相对于模型规模增长的滞后性。

在分布式训练实践中,开发人员普遍认同,构建模型并行的分布式训练系统相比数据并行更为复杂。数据并行从分布式角度来看,其逻辑相对直接和简洁,因为每个计算节点执行的任务本质上是对等且一致的。在这种情况下,只需在训练过程末尾插入 AllReduce 步骤,将各个工作节点(worker)独立计算出的梯度差异累加整合,然后求平均值,并将最终梯度结果广播至所有参与工作的节点,用以同步更新全局模型参数。

这类简单的分布式训练范式,确实呈现出类似单机计算的特点,主要涉及全局梯度同步的 AllReduce。然而步入大模型时代,由于模型规模过大,已无法容纳于单个 GPU 之内,我们就必须采用模型并行策略,其开发难度也就陡然上升了。

原因是,模型并行需要根据模型的规模和结构来决定如何恰当地“分割”模型,即将其分割为多个可以平衡计算负载的模块。在不同的分割策略下,模型在各个节点上算子的算法实现方式会发生变化,同时,不同分割方法还会引起节点间通信原语的差异,需要精心选择最优分割方案以及配套的通信原语。

在模型分割完成后,接下来的任务就是选用适合的通信原语,并精细地调度各个算子及其相关的通信操作,力求最大化计算与网络通信的重叠(overlap),以充分发挥底层计算资源的效率。正是由于存在多种可能的分割选项与调度决策,寻求最优模型并行策略的复杂性明显高于数据并行,对开发者的技巧和经验提出了更高的要求。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

本文将围绕四个核心方面展开。首个议题是如何在 TorchAcc 中实现多样化的并行策略,涵盖了常规的数据并行,以及当下备受关注的 FSDP(Fully Sharded Data Parallel,又称 ZeRO (Zero Redundancy Optimizer)) 。此外,还包括了模型并行的各种形态,诸如算子并行,即 Tensor Parallelism,以及流水线并行(Pipeline Parallelism)等。

TorchAcc 的一大亮点在于其能够自动探寻并有机整合各类并行策略,并为用户提供高度自动化的分布式策略配置方案;与此同时,为了满足高级开发者的定制化需求,TorchAcc 还提供了半自动化的控制接口,允许用户介入并调整自动探索并行策略的过程,从而在兼顾灵活性的同时,最大程度地提升训练效率和资源利用率。

通过上述方式,TorchAcc 有效地助力算法开发者将精力集中于模型自身的结构设计、训练方法的优化,以及追求模型收敛性能的提升上,而非花费精力在分布式训练的具体实现细节。TorchAcc 将智能化地协助开发者探寻并实现最佳的分布式训练方案,从而显著提升计算资源利用效率和算法迭代效率。

其次,模型并行技术的必要性是因为大模型尺寸超出单个 GPU 显存容量的限制。显存容量对于模型训练至关重要,如何打破显存瓶颈,对于提升分布式训练的整体效率来说至关重要。因此,TorchAcc 提供了一种显存智能分配器,通过对显存资源的精细化调度与地址分配策略,最大限度地提高模型并行训练时的效率,确保模型能充分利用现有的显存地址空间。

再者,随着模型结构日益复杂,且规模不断增大,用户对计算资源的需求也在持续攀升,因此,进一步优化模型在训练过程中的计算密集度及减少访存开销也非常关键。

最后,考虑到当前数据中心基础设施的发展趋势,大模型训练对网络条件的要求日渐严苛。现代数据中心服务器间的互联带宽已达到 TB 级别,以满足大规模模型并行训练对高速数据交换的需求。然而,模型并行所带来的复杂通信模式与高频次的数据交互亦会对整体训练效率构成挑战。因此,如何有效利用网络带宽,减少通信过程在迭代计算中占据的时间比例,也就成了训练效率提升的另一重要因素。

在具体实现上,TorchAcc 通过一系列技术手段,成功地将用户在前端,无论是基于 PyTorch 还是 TensorFlow 构建的模型训练过程转化为统一的中间表示层(Model IR)的 graph。其中,对于 TensorFlow 而言,因其自身就是一种计算图模型,转化过程相对直接,而对于 PyTorch,我们采用了符号式追踪(symbolic tracing)以及 LazyTensor 等技术捕获计算图,进而转化为 IR Graph。

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

基于中间表示层(IR Graph)的构建,TorchAcc 实施了一系列多元化的优化策略,涵盖计算优化、存储优化、通信优化以及分布式策略优化,IR Graph 以各类组合并反复执行这些优化的 Pass 后,最终得到一个最优的执行 Plan。然后交由底层 Backend 执行,以实现模型训练性能的最大化提升。

通过这一整套方案,TorchAcc 在多个模型的分布式训练场景中表现出了显著的性能优势。部分模型的训练过程得以实现高达 3 倍的性能提速,充分证明了 TorchAcc 在解决分布式训练难题上的高效性和实用性。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

这张图片主要展示了 TorchAcc 的框架总体架构。TorchAcc 以 Pytorch/XLA 为基础,并 TorchAcc 依托于 OpenXLA,构建了一套大模型训练加速框架。TorchAcc 在处理使用不同前端构建的模型时,会灵活采用适宜的图捕获技术,如 Symbolic Trace 和 LazyTensor,进而生成两种不同层级的图表示:FX Graph 和 HLO Graph。其中,FX Graph 位于较高抽象层次,而 HLO Graph 则更为底层。

基于捕获到的模型计算图,TorchAcc 即可进一步展开了四类优化工作,即前文提及的计算优化、存储优化、通信优化以及分布式策略优化。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

在分布式策略优化层面,TorchAcc 支持业界广泛使用的各种并行策略,并能够灵活地结合这些策略对给定模型进行有效的并行化处理。具体而言,对于数据并行 DP(Data Parallelism)、流水并行 PP(Pipeline Parallelism)以及 FSDP(Fully Sharded Data Parallel, 也称为 ZeRO)这三种分布式策略,其实现和优化都是在 FX Graph 这一较高抽象层次上完成的。

选择在 FX Graph 层面对并行策略进行操作的原因在于,这一层级所包含的关于计算图结构和操作的信息已足够丰富,足以支撑开发人员设计出适应不同并行策略的优化方案。相较于在更低层的 HLO Graph 上直接进行优化,由于 FX Graph 具有更高的抽象性和概括性,在这一层面上进行优化的成本通常较低,更容易实施高效且针对性强的分布式策略调整。

以流水并行作为例子,系统能够自动检测 FX Graph 层级上的不同阶段,并确定合适的分割点,从而有效地将模型分割为多个连续执行的阶段,实现流水线并行化。在此过程中,我们可以利用 FX Graph 提供的详细计算结构信息来进行智能分割。

至于 Tensor Parallelism (张量并行)和 Sequence Parallelism (序列并行)这两种更为复杂的并行策略,它们要求更为细致精确的信息以便进行决策。为了实现这一点,系统需要对前向传播和反向传播的整个计算图的执行计划来进行分析。这时的工作主要在 HLO 这一低级别表示层面上进行。

通过利用 PyTorch/XLA 提供的 mark sharding 接口,系统能够在模型参数上添加相应的拆分标记,然后将这些拆分信息传递给 OpenXLA 的 SPMD 优化 Pass,进而触发计算图的拆分、优化、推导和重写过程,最终实现自动的 Tensor Parallelism 和 Sequence Parallelism 功能。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

在算子优化层面,TorchAcc 引入 FlashAttention 技术来提升 Attention 模块的执行效率。首先,通过 XLA 的 custom call 功能,将 FlashAttention 的实现无缝地融入到了 OpenXLA 编译器和运行时框架中。这意味着 FlashAttention 可以直接在 XLA 内核层级被执行,从而充分利用硬件加速能力。

在整合过程中,要处理好在 PyTorch 与 XLA 之间 Tensor 数据的传递问题,确保在两个系统间转换时的数据一致性与性能优化,同时,还要妥善处理 FlashAttention内部参数传递等细节问题,保证在并行计算和优化的过程中,这些关键参数能够正确且高效地应用到计算中,进一步提升模型在执行注意力机制部分的运算速度和资源利用率。

为了用户能便捷地使用 FlashAttention 优化功能,我们提供了两种接口,用户也可以直接通过 Python 接口调用预先写好的 FlashAttention 算子,第三种方法是用户可以使用我们在 OpenXLA 上写好的 Pattern Match Pass,该 Pass 能够自动识别计算图中的 Attention Block,并将这部分计算结构提取出来,替换为FlashAttention 的 custom call。这样设计的优势在于,既能充分利用 XLA 原本就十分出色的 Kernel fusion 等算子优化功能,又能结合 FlashAttention 带来的先进计算优化技术。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

在 Llama 2-7B 模型的性能测试中,我们能够明显观察到上述计算优化带来的效果。通过利用 XLA 自身的优化技术,尤其是 kernel fusion,我们将大量的访存密集型算子做了有效合并,从而大幅减少其数量,在叠加 FlashAttention 后,优化性能进一步提升。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

在通信优化层面,我们主要完成了三项核心任务以提升分布式训练效率:首先,我们合并了一些零散的 collective 通讯算子,通过减少算子数量来降低通讯开销和调度复杂度,其次,我们将合并的 collective 通讯算子移至独立的 CUDA Stream 上执行,这样一来,就能够异步实现计算与通讯的重叠执行。最后,我们充分利用了 OpenXLA 的 Latency Hiding Scheduler 功能,对通讯算子的调度进行了精细优化,使其尽早启动和执行,从而增强通讯与计算之间的重叠效果。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

通过在 Llama2 -7B 模型上进行的端到端多机性能测试,我们发现,应用了通讯优化策略后,在 128 张 GPU 卡上进行分布式训练,优化后的加速比从原来的 88 提升到了 116,通过 timeline 图我们也可以直观地看到,优化后的通讯算子更加有序,并且能够更好地和计算重叠执行。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

本文最后一个章节绍 TorchAcc 的显存优化功能,该功能通过优化计算图中算子的执行顺序以及 Tensor 在显存中的地址分配,来降低显存开销。

如图举例说明,假设有一个包含四个算子 V0、V1、V2、V3 的计算图,如果不控制算子执行顺序,如左图所示按照 V0-V1-V2-V3 的顺序执行,若每个 Tensor 按照默认方式进行显存地址申请,则可能出现如 B 图左半部分所示的情况,即显存容量不足以容纳所有 Tensor,导致 out of memory 错误。

然而,如果我们能够预判并精细管理内存分配,即在分配地址时预知后续执行的算子序列,即可如 B 图右半部分所示进行更优的显存布局,使得整体计算可在有限显存内顺利完成。更进一步,通过精确控制执行顺序,比如按照 V0-V2-V1-V3 的方式执行,可以进一步压缩显存需求至原始需求的 70% 左右。

这一理念是基于 XLA 中间表示层已有的 scheduler 和 buffer 管理机制,我们在此基础上提出了更先进的显存优化方法。目前业界存在多种优化显存分配的方法,如启发式算法、约束求解等,但这些方法往往难以兼顾时效性和高效性,在实际生产环境的集群中应用时可能存在局限性。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

在训练场景中实现有效且高效的显存优化是一项极具挑战的任务,原因主要包括以下几个方面:

  1. NP-Hard 问题本质:由于模型的规模、算子的种类繁多,以及算子间显存分配的复杂性,显存优化问题成为一个典型的 NP-hard 问题,即找到全局最优解在计算上通常是不可行的。

  2. 算子执行灵活性:训练过程中,前向传播、反向传播和权重更新等操作具有很高的灵活性,特别是在权重更新方面,梯度产生后随时可以被用于权重更新,但不同的执行时机会影响显存的申请和释放,增加了优化难度。

  3. 显存复用复杂性:在训练过程中,前向和反向传播可以通过复用显存减少重新计算,但 Tensor 生命周期的多样性和尺寸的变化使得显存复用变得极为复杂,这对启发式算法等传统优化手段构成了严峻挑战。

为了解决上述难题,我们采取了一种分治策略:

  1. Memory-aware Weight Update Scheduler:引入了显存感知的权重更新调度器,它会根据梯度产生的时机、使用的优化器类型以及当前显存资源状况,选择合适的权重更新时间点,避免即时更新加重显存压力,特别是对于复杂的优化器如 Adam,需考虑动量和其他变量的存储。

  2. Graph 分割与局部优化:将大计算图根据关键节点 (memory insensitive operator) 分割成多个内存无关性的子图,子图间执行顺序固定,而子图内部的执行顺序则可以多样化。通过这种方式,可以将复杂的全局线性规划问题分解成多个局部问题,在子图范围内采用高效的优化方法,如线性规划求解最优执行顺序。

通过上述分治策略,最终我们能够聚合这些子图的求解结果,这也就是我们提出的 ROAM (Reorder Operators and Arrange Tensors Address to Reduce Memory Usage) 这一内存优化探索方式。

上述方法可以成功实现对显存优化问题的高效处理。实验结果显示,与原生 PyTorch、启发式算法以及 Facebook 近期基于整数线性规划的优化方法等 baseline 相比,ROAM 分别节省了约 16%、13% 和 27% 的显存开销,且在优化时长和可扩展性方面表现出色,证实了这种方法的有效性。

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

图片

图片来源于 GTC 2024 大会 China AI Day 线上专场的演讲《TorchAcc:基于 TorchXLA 的分布式训练框架》

从另一个维度衡量效果,我们考察了算法求解的时间开销。实验证明,在常见的深度学习场景中,我们的优化算法能够在短短几分钟内得出优化结果。从右图所示对比中可以看出,相较于 Facebook 最近提出的 MODeL(一种基于线性规划的优化方法),我们的方法在求解时间上实现了显著的缩减。原因在于,MODeL 在处理大规模图时并未对其进行有效分割,而我们的方法通过引入 memory-aware weight update scheduler 和子图划分策略,有效地降低了优化问题的空间复杂度,从而提高了求解效率。

综上所述,TorchAcc 在显存优化、计算优化、通信优化以及并行策略优化等方面均取得显著成效,全方位提升了分布式训练的效率与性能。


以上内容来源于 GTC 2024 大会 China AI Day 线上中文演讲专场。扫描图片二维码或登录大会官网,即可观看演讲视频,并可下载讲义。

图片

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【XXL-JOB】执行器架构设计和源码解析

简介 XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。 XXL-JOB分为B/S架构,调用中心是XXL-JOB服务端,执行器是客户端。 调度中心核…

【1】网络协议基础概念

【1】网络协议基础知识 1、互联网2、为什么要学习网络协议3、学习中需要搭建的环境4、客户端-服务器5、Java 的跨平台原理6、C/C的跨平台原理7、一个简单的SpringBoot项目(1) pom.xml(2) application.yml(3) NetworkStudyApp.java(4) SwaggerConfig.java(5) HelloWorldControll…

CXL系统架构

CXL系统架构 CXL支持三种设备类型,如下图。Type 1支持CXL.cache和CXL.io;Type 2支持CXL.cache,CXL.mem和CXL.io;Type 3支持CXL.mem和CXL.io。无论哪种类型,CXL.io都是不可缺少的,因为设备的发现&#xff0…

Deconstructing Denoising Diffusion Models for Self-Supervised Learning解读(超详细)

论文题目:Deconstructing Denoising Diffusion Models for Self-Supervised Learning 原文链接:https://arxiv.org/html/2401.14404v1 本文是对何凯明老师的新作进行的详细解读,其中穿插了一些思考,将从以下四个方面对这篇工作进…

3723. 字符串查询:做题笔记

目录 思路 代码 注意点 3723. 字符串查询 思路 这道题感觉和常见的前缀和问题不太一样,前缀和的另一种应用:可以统计次数。 这道题我们想判断一个单词的其中一段子序列A是否可以通过重新排列得到另一段子序列B。 我看到这道题的时候想着可能要判…

Gitlab 实现仓库完全迁移,包括所有提交记录、分支、标签

1 方案一&#xff1a;命令 cd <项目目录> git fetch --all git fetch --tags git remote rename origin old-origin #可以不保留 git remote add origin http://***(项目的新仓库地址) #git remote set-url origin <项目的新仓库地址> git push origin --all git…

Qt 多线程QThread的四种形式

重点&#xff1a; 1.互斥量&#xff1a;QMutex配套使用&#xff0c;lock(),unlock(),如果一个线程准备读取另一个线程数据时候采用tryLock()去锁定互斥量&#xff0c;保证数据完整性。 QMutexLocker简化版的QMutex,在范围区域内使用。 QMutex mutex QMutexLocker locker(&…

达梦数据库新手上路排坑

数据库安装 这个没啥说的&#xff0c;按照官网教程操作&#xff0c;我使用的是docker进行安装 下载文件docker文件 官方下载地址- load -i dm8****.tar (注意修改为当前下载的文件)达梦官方文档注意修改为当前版本 docker run -d -p 5236:5236 --name dm8 --privilegedtrue -…

程序员口才提升技巧:从技术到沟通的进阶之路

程序员口才提升技巧&#xff1a;从技术到沟通的进阶之路 在数字化时代&#xff0c;程序员作为推动技术发展的关键角色&#xff0c;其专业能力的重要性不言而喻。然而&#xff0c;除了编程技能外&#xff0c;良好的口才同样是程序员职业生涯中不可或缺的一部分。本文将探讨程序…

学透Spring Boot — [二] Spring 和 Spring Boot的比较

欢迎关注我们的专栏 学透 Spring Boot 一、创建一个简单Web应用 本篇文章&#xff0c;我们将会比较 Spring 框架和 Spring Boot 的区别。 什么是 Spring? 也许你在项目中已经可以很熟练的使用 Spring 了&#xff0c;但是当被问到这个问题时&#xff0c;会不会犹豫一下&#…

2024-3-28 市场情绪强修复

这一轮退潮负反馈都修复了&#xff0c; 艾艾精工 博信股份 安奈尔 永悦科技 大理药业 &#xff0c;高新发展 也补跌了&#xff0c;收尸队也干活了&#xff0c;情绪不修复不接力得最好写照。这轮周期 宁科生物 已经7板&#xff0c;已经追平了 博信股份7板&#xff0c;看明天溢…

永磁同步电机速度环滑膜控制(SMC)

文章目录 1、前言2、滑膜控制基本原理2.1 滑膜控制的定义2.2 趋近率 3、滑膜控制器的设计与参数4、二阶滑膜速度控制器的设计5、二阶速度环滑膜控制仿真5.1 模型总览5.2 电机及系统参数5.3 滑膜控制模块5.4 控制效果对比 参考 写在前面&#xff1a;本人能力、时间、技术有限&am…

广场舞团系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目持续更新中..... 2024年计算机毕业论文&#xff08;设计&#xff09;学生选题参考合集推荐收藏&#xff08;包含Springboot、jsp、ssmvue等技术项目合集&#xff09; 目录 1. 系…

明天线上见!DPU构建高性能云算力底座——DPU技术开放日最新议程公布!

算力&#xff0c;是数字经济时代的新质生产力。随着人工智能、智算中心建设等需求不断拓展&#xff0c;DPU在各行各业数据中心的应用逐步深入。异构算力代表DPU在新质生产力建设中&#xff0c;能否给出别开生面的答案&#xff0c;应战算力难题&#xff1f;DPU技术在不同行业中的…

详细解析记忆泊车的顶层技术原理

详细解析记忆泊车的顶层技术原理 附赠自动驾驶学习资料和量产经验&#xff1a;链接 相对于记忆行车而言&#xff0c;记忆泊车 MPA&#xff08;Memory Parking Assist&#xff09;可以看成是停车场区域内的一个自动驾驶功能&#xff0c;可帮助用户按记忆的路线自动巡航并泊入车…

Vue2 与 Vue3的面试题

1.Promise有几种状态 pending(进行中) fulfilled(已成功) rejected(已失败) data(){return{}},create(){const result this.ganaretor()result.next.value.then((res)>{console.log(res);}) // 解决一直.then()方法问题},methods:{* ganaretor(){yield axios.get(httpts)…

vulnhub靶场之driftingblues-3

一.环境搭建 1.靶场描述 get flags difficulty: easy about vm: tested and exported from virtualbox. dhcp and nested vtx/amdv enabled. you can contact me by email for troubleshooting or questions. This works better with VirtualBox rather than VMware 2.靶场…

【前端面试3+1】01闭包、跨域、路由模式

一、对闭包的理解 定义&#xff1a; 闭包是指在一个函数内部定义的函数&#xff0c;并且该内部函数可以访问外部函数的变量。闭包使得函数内部的变量在函数执行完后仍然可以被访问和操作。 特点&#xff1a; 闭包可以访问外部函数的变量&#xff0c;即使外部函数已经执行完毕。…

天锐绿盾|公司如何防止员工拷贝电脑资料?

#天锐绿盾# 天锐绿盾是一款针对企业数据安全设计的终端安全管理软件&#xff0c;用来防止员工拷贝电脑资料的具体措施包括&#xff1a; www.drhchina.com PC地址&#xff1a; https://isite.baidu.com/site/wjz012xr/2eae091d-1b97-4276-90bc-6757c5dfedee 1. **文件透明加密…

10.对象的使用,遍历

什么是对象 其实就是map那种键值对的存储形式&#xff0c;别的语言也有&#xff0c;老规矩&#xff0c;和别的语言差不多的就在给pink老师打一波广告。 常见的对象操作&#xff0c;其实没啥直接上代码吧 <!DOCTYPE html> <html> <head><meta charset&…
最新文章