资源受限调度的编程模型的制作方法

文档序号:28298707发布日期:2021-12-31 23:29阅读:99来源:国知局
资源受限调度的编程模型的制作方法
资源受限调度的编程模型
1.相关申请的交叉引用
2.本技术要求2020年3月20日提交的第62/992872号美国临时专利申请的优先权,其通过引用并入本文。
技术领域
3.本文中的技术涉及图形处理单元(gpu),更具体地说,涉及图形处理单元要执行的调度工作。
4.

背景技术:
&

技术实现要素:

5.图1说明了有多少或大多数传统图形处理单元(gpu)能够处理不同种类的工作负载,例如图形工作负载和计算工作负载。图形工作负载和计算工作负载的调度方式存在差异。
6.图形工作负载通常包括与图像着色像素相关的图形数据处理中的一组逻辑阶段。在此上下文中,像素的着色通常产生影响图像的一个或更多个像素的外观的可视化(亮度、颜色、对比度或其他视觉特征)信息。
7.gpu通常使用图形管线来处理图形工作负载。图1的左侧显示了一个典型的图形管线,包括一系列着色器阶段10,它们使用管线片上存储器11进行通信。因此,图形管线将用于图形处理的逻辑阶段序列作为着色器阶段10a、10b、

、10n的管线序列提供,其中序列中的每个着色器阶段10执行各自的图形计算。在图1左侧所示的图形管线中,每个着色器阶段10对管线中前一阶段的结果进行操作,并提供处理后的结果供管线中后续阶段进一步处理。通过基于硬件的图形管线定义的跨硬件分布的图形处理已被证明是非常有效的,并且是大多数现代gpu设计的基础。
8.因此,此类图形编程模型是围绕固定功能图形管线的概念构建的,该图形管线以生产者

消费者的方式进行精细调度。在此上下文中,图形管线的每个阶段10能够以管线方式被单独调度。管线中的着色器阶段10是基于早期阶段生成的数据的存在和固定功能先进先出存储器(fifo)中为后期阶段提供输出空间的可用性而启动的。数据编组由一组定义良好的系统提供的基于硬件的触发器提供,以确保数据分组以有序的方式从一个着色器阶段流向下一个着色器阶段,并且在前一阶段生成数据之前,后续阶段不会开始处理数据。此外,数据编组通常由专用图形管线硬件阶段(例如,顶点着色器)使用的输入数据属性数及其生成的输出数据属性数的显式定义支持。
9.在这样的图形处理中,在许多(尽管不是所有)实施例中,在这样的图形管线中用于在阶段之间传递数据的存储器11包括可以快速有效地访问的片上本地存储器。这种使用片上本地存储器的管线调度方法已被证明对于处理图形工作负载非常有效,因为它避免了使用外部存储器带宽和相关联的存储器延迟来实现管线阶段之间有序、格式化的数据流。
10.以此类推,多批衣物计划在一种管线中进行洗涤和干燥。洗衣机清洗每一个新来的货物。洗衣机清洗完一批衣物后,洗衣工将衣物移到烘干机并启动烘干机,然后将一批新衣物移入洗衣机并启动洗衣机。这样,当洗衣机洗完新衣物时,烘干机就会烘干衣物。烘干
机完成后,洗衣工将干燥的衣物倒入篮子或箱子中进行熨烫和/或折叠,并熨烫/折叠这些衣物,同时洗衣机和烘干机在整个顺序过程中处理较早的衣物。在现代gpu图形管线中,“清洗工”是系统的一部分,软件开发人员可以简单地假设它将提供必要的数据编组,以确保有序的数据流和通过管线正确安排的处理。但应注意的是,现代图形管线不限于线性拓扑,管线体系架构的定义特征与在连续处理阶段之间编组数据的方式有关,而不是与任何特定拓扑有关。
11.在gpu设计的早期,这种固定功能的管线图形处理通常是gpu能够支持的唯一工作负载,所有其他功能都是在cpu上运行的软件中执行的。gpu编程的第一步是适度的:使一些着色器可编程并增强硬件以支持浮点运算。这为在gpu图形管线硬件上执行一些非图形科学应用打开了大门。例如,见du等人,“从cuda到opencl:迈向多平台gpu编程的高性能便携式解决方案”,并行计算。38(8):391

407(2012)。图形api(如opengl和directx)随后得到增强,以将一些通用计算函数表示为图形原语。此外,还开发了更多的通用计算api。例如,tarditi等人,“加速器:使用数据并行性编程通用gpu”,acm sigarch计算机架构新闻,34(5)(2006)。nvidia的cuda允许程序员忽略底层的图形概念,转而采用更常见的高性能计算概念,为微软的directcompute和苹果/khronos集团的opencl铺平了道路。参见du等人的《gpu硬件进一步升级》,在图形处理单元(“gpgpu”)上提供高性能通用计算。
12.因此,现代gpu计算处理涉及按照通用应用程序编程接口(api)进行操作,例如cuda、opencl和opencompute,用于通用数字计算。这些计算工作负载可以不是由可编程或固定功能图形管线定义的工作负载,例如,为处理与图像中的着色像素没有直接关系的数据定义一组逻辑计算阶段。一些示例计算操作可以包括但不限于与生成动画模型相关联的物理计算、分析来自科学或金融领域的大型数据集、深度学习操作、人工智能、张量计算和模式识别。
13.目前,与图形编程模型不同,gpu计算编程模型是围绕数据并行计算的概念而建立的,尤其是平面批量同步数据并行(bsp)。例如,见leslie g.valiant,《并行计算桥接模型》,acm通讯,第33卷,第8期,1990年8月。图1显示了右侧的示例gpu计算处理阶段12a、12b、

、12m。每个并行计算处理阶段都可以通过启动多个并行执行线程来定义,以供gpu大规模并行处理体系架构执行。广泛的工作集合12一次启动一个,并通过主存储器或其他全局存储器13进行通信(见图2)。在一个实施例中,工作集合可以包括由软件、硬件或两者定义或支持的任何通用计算操作或程序。
14.在此类计算模型中,通常存在计算工作负载的执行层次结构。例如,在一个实施例中,“网格”可以包括一组线程块,每个线程块包括一组“线程束”(使用织物类比),而这些经线又包括单独执行的线程。线程块的组织可用于保证线程块中的所有线程将并发运行,这意味着它们可以共享数据(调度保证)、相互通信并协同工作。但是,可能无法保证网格中的所有线程块都将并发执行。相反,根据可用的机器资源,一个线程块可能会在另一个线程块启动之前开始执行并运行到完成。这意味着无法保证网格内线程块之间的并发执行。
15.一般来说,网格和线程块不会在常规图形工作负载中执行(尽管应注意,这些一般区别中的一些可能不一定适用于某些高级图形处理技术,例如us20190236827中描述的网格着色器,这些技术将计算编程模型引入图形管线,因为线程协同使用以直接在芯片上生成紧凑网格(小网格)以供光栅器使用)。此外,传统的图形处理通常没有在特定的选定线程
之间本地共享数据并保证计算工作负载所支持的线程块或网格模型的并发性的机制。
16.如图2所示,共享(例如,主或全局)存储器13通常用于在计算操作12之间传递数据。每个网格都可以从全局存储器13读取和写入。但是,除了提供每个网格可以访问的后台存储器之外,没有系统提供的数据编组或任何机制来将数据从一个网格传送到下一个网格。系统不定义或约束向网格输入的数据或从网格输出的数据。相反,计算应用程序本身定义了数据输入/输出、同步和网格之间数据编组的所有其他方面,例如,在后续网格处理开始之前,完成一个网格处理的结果(或至少那些后续处理依赖的特定结果)并将其存储在存储器中。
17.此类数据编组涉及数据分组如何从一个计算过程12a传递到另一个计算过程12b,并处理与有效负载从一个计算过程传递到另一个计算过程有关的各种问题。例如,数据编组可能涉及将数据从生产者传送到消费者,并确保消费者能够识别数据并具有一致的数据视图。数据编组还可能涉及保证某些功能和/或处理,例如高速缓存一致性和调度,以使消费者计算过程能够一致地访问和操作生产者计算过程已经产生的高速缓存数据。数据编组可能涉及也可能不涉及或要求移动或复制数据,具体取决于应用程序。在现代gpu计算api下,所有这些都留给应用程序处理。虽然这提供了很大的灵活性,但也存在一些缺点。
18.数据编组通常涉及提供从一个网格到下一个网格的某种同步。如果每个网格独立于所有其他网格,并且可以独立处理,则几乎不需要同步。这有点像拥挤收费公路上的汽车通过一排收费亭,每辆汽车都可以独立通过各自的收费亭,而不需要等待任何其他汽车。但一些计算工作负载(例如,某些图形算法、稀疏线性代数和生物信息学算法等)表现出“不规则并行性”,这意味着一些消费者网格的处理可能依赖于一个或更多个生产者网格的处理。
19.在计算api下,应用程序本身负责彼此之间的同步,以组织它们之间的数据流。例如,如果消费者网格希望查看和使用生产者网格生成的数据,则应用程序负责插入屏障、围栏或其他同步事件,以向消费者网格提供生产者网格生成的数据的一致视图。例如,参见2019年12月12日提交的美国专利申请16/712236号,标题为“协调计算机系统上的操作的高性能同步机制”,usp9223578、usp9164690和us20140282566,其中描述了网格12可以使用重权重同步原语(例如屏障或栅栏)彼此同步并通过全局存储器13通信数据的示例方式。这种栅栏/屏障同步技术例如可以提供同步,要求第一个计算网格在下一个计算操作访问数据之前完成向存储器写入数据。根据批量同步编程模型,同步是按每个网格批量完成的,这通常意味着生产者网格的所有线程必须在消费者网格开始处理之前完成执行并将其数据结果写入全局存储器。由此产生的gpu处理资源的次优利用率可能会导致大量时间浪费、性能降低、带宽减少和功率浪费,具体取决于计算工作负载。
20.由于这些不同的问题,对于某些类型的计算工作负载,当前计算编程模型显示的执行行为可能是低效的。过去曾尝试过几次改进,以允许应用程序表达这种“不规则”的并行工作负载。例如,nvidia使用开普勒体系架构引入了cuda嵌套并行性,针对hpc应用程序中的不规则并行性。参见例如usp8180998;和zhang等人,“通过gpu上的高级动态并行来驯服不规则应用程序”cf'18(2018年5月8日至10日,意大利伊斯基亚)。另一个示例项目着眼于基于队列的图形编程模型。intel的larrabee gpu架构(未作为产品发布)引入了ct,它具有“编织”并行的概念。例如,见morris等人,“kite:异构系统的编织并行”(计算机科学2012)。所有这些尝试都取得了相对有限的成功,并且不一定试图捕获图形管线系统提供的
用于计算工作负载的调度和数据编组的方面。
21.为了进一步解释,假设一个网格是消费者,另一个网格是生产者。生产者网格将向全局存储器写入数据,消费者网格将从全局存储器读取该数据。在当前的批量同步计算模型中,这两个网格必须串行运行——生产者网格在消费者网格启动之前完全完成。如果生产者网格很大(多个线程),则少数线程可能是散乱的,需要很长时间才能完成。这将导致计算资源的低效利用,因为消费者网格必须等到散乱线程完成后才能启动。当生产者网格的线程逐渐退出时,机器占用率下降,与两个网格中的所有线程都能够并发运行的情况相比,这导致了效率低下。本文所述技术的各个方面避免了此类低效,并实现了连续占用,例如在典型的图形管线中,利用连续同时的生产者/消费者来获取占用效益。
22.图3显示了常用gpu管线图形处理的额外扩展和收缩支持(工作放大和聚合)。许多gpu复制某些着色器阶段(例如,细分),以根据需要启用对同一阶段输入数据的系统调用的并行处理,以避免瓶颈并提高数据并行性,这对于某些类型的图形工作负载非常有用。这种系统调用的功能在过去对于gpu系统调度器支持的计算工作负载是不可用的。
23.因此,需要一种新的模型,允许以捕获图形管线的调度效率的方式调度计算工作负载。
附图说明
24.以下示例性非限制性说明性实施例的详细说明将结合其附图阅读:
25.图1示意性地显示了执行计算处理和图形处理的图形处理单元的分离个性。
26.图2显示了着色器阶段如何使用管线片上存储器彼此通信,以及计算进程如何使用全局存储器和同步机制彼此通信。
27.图3显示了图形处理的非限制性扩展和收缩支持示例,其具有对计算过程的此类支持的限制。
28.图4显示了示例非限制性调度模型。
29.图5显示了使用启动保证资源的非限制性调度模型示例。
30.图6显示了生产者和消费者之间资源受限的调度示例。
31.图7显示了使用启动阈值的示例图6的资源受限调度。
32.图8显示了包括生产和消费反馈的图6的资源受限调度示例。
33.图9显示了示例非限制调度器,该调度器在输入准备就绪且输出可用时启动。
34.图9a显示了图形执行启动的更详细视图。
35.图10显示了使用队列的资源受限调度。
36.图11显示了使用带有队列的数据编组的资源受限调度示例。
37.图12显示了使用内部和外部放置(put)和获取(get)的资源受限调度示例。
38.图13显示了非限制性资源释放的示例。
39.图14显示了如果生产者没有生成输出,则提供早期发布的情形下的非限制性资源发布示例。
40.图15显示了如果以后的消费者需要可见性的情形下的非限制性资源发布示例。
41.图16显示了包括更细粒度的资源使用情况跟踪的非限制性资源发布示例。
42.图17显示了在产生部分输出时的非限制性资源释放示例。
43.图18显示了生产者释放队列中未使用空间时的非限制性资源释放示例。
44.图19显示了消费者释放剩余空间的非限制性资源释放示例。
45.图20显示了用以允许新生产者更早地开始生产的非限制性资源释放的示例。
46.图21显示了用以允许在相关部分发布中进行可变大小的输出的非限制性资源发布示例。
47.图22显示了避免碎片的示例队列存储器分配方案。
48.图23显示了每个条目启动多个消费者的非限制性工作放大示例。
49.图24显示了用以在每次进入时启动多个不同的消费者的示例非限制性多桶(1个过多)启动方案。
50.图25显示了非限制性工作聚合的示例,其中一组工作项聚合为单个消费者启动。
51.图26显示了包括部分启动超时结果在内的非限制性工作聚合示例。
52.图27显示了连接分配示例,其中包括一组单独计算的字段,这些字段聚集到一个工作项中。
53.图28显示了使用聚合将工作排序到多个队列以重新收敛执行的示例。
54.图29显示了排序到多个队列以进行着色器专门化的示例性非限制性工作。
55.图30显示了排序到多个队列中以用于光线跟踪中的相干材质着色示例性非限制性工作。
56.图31显示了示例性非限制依赖性调度。
57.图32显示了示例性非限制性计算管线到图形。
58.图33显示了馈送图形以实现完全灵活的计算前端的示例性非限制性计算。
59.示例性非限制性实施例的详细描述
60.本技术扩充了计算编程模型,以提供图形管线的一些系统提供的数据编组特性,从而提高效率并减少开销。特别是,本文中的技术允许程序员/开发人员以目前可用的gpu计算api无法实现的方式创建计算内核管线。
61.示例性非限制性实施例提供了一个简单的调度模型,该模型基于抽象硬件资源的可用性的标量计数器(例如,信号量)。在一个实施例中,负责启动新工作的系统调度器通过递减相应的信号量来保留/获取资源(“空闲”空间或“就绪”工作项),并且用户代码(即,应用程序)释放资源(消费者:通过加回他们从中读取的资源池的“空闲”池,生产者:通过增加回至他们正在写入的资源的“就绪”池)。在这样的示例性非限制性安排中,保留/获取总是可以通过调度器递减信号量而保守地完成,并且资源释放总是由用户(应用程序)代码完成,因为它确定何时适合执行释放,并且总是通过递增信号量来完成。在这样的实施例中,资源释放因此可以以编程方式完成,并且系统调度器仅需要跟踪这样的计数器/信号量的状态以做出工作启动决策(如果用户或应用程序软件未能按照其预期的方式执行此操作,则可以提供回退规定,以便调度器可以递增信号量,从而释放资源)。资源的释放不必由用户的应用程序来完成,它可以由一些系统软件来实现,例如,可以确保完成所有正确的核算。在一个实施例中,计数器/信号量的语义由应用程序定义,该应用程序可以使用计数器/信号量来表示,例如,可用性存储器缓冲区中可用空间的不稳定性、网络中数据流引起的高速缓存的压力或待处理工作项的存在。从这个意义上讲,我们的方法是“资源受限”的调度模型。
62.这种方法的一个新方面是,应用程序可以灵活地组织网络中节点之间的自定义数据编组,使用有效的无锁算法,使用队列或应用程序选择的其他数据结构。通过解耦硬件计数器/信号量的管理来驱动对此类数据结构的管理的调度决策,我们实现了一个用于表达工作放大和聚合的框架,这是图形管线调度的有用方面,并有助于高效处理具有不同数量的数据并行性的工作负载(如计算工作负载)。提供了一个资源保留系统,该系统在启动时分配资源,以确保线程块可以运行到完成,从而避免死锁,并避免昂贵的上下文切换。资源保留系统相对简单,不需要以来自同一任务/节点的工作项的数量或并发执行的线程组的数量扩展或成长。例如,在一个实施例中,在任何特定任务的这种调度期间,调度器仅查看每个资源的两个计数器sf和sr(当管线中有多个资源/阶段时,将有或可能有多对计数器)

这对可以构造的执行节点图的类型有一些影响。在示例实施例中,信号量本身不被视为调度器的一部分,而是由硬件平台提供以支持调度器。因此,调度器和应用程序可以通过某些的方式各自操纵信号量,并且调度器可以监视应用程序对信号量的操纵。
63.另一个方面涉及调整图形管线的扩展/收缩支持,使这些概念对计算工作负载有用——提供动态并行性。
64.以前尝试调度表现出不规则并行性的计算工作负载时,通常没有尝试将背压的概念直接纳入生产者

消费者数据流,这有助于高效调度,并允许数据流保持在芯片上。背压——消费者减慢生产者速度的能力,以防止消费者被生产者正在产生的数据流淹没——允许通过固定大小的缓冲区高效地传输大量数据。由于物理存储器是一种有限的资源,并且与应用程序的许多其他部分共享,因此,能够分配其大小小于通过缓冲区传输的工作项的潜在数量的固定大小的缓冲区,是非常理想的。图形管线通常支持这种背压调度,例如,jonathan ragan kelley,《让许多内核保持忙碌:调度图形管线,超越可编程着色ii》(2010年7月29日,周四,siggraph);kubisch,gpu驱动的渲染(nvidia,gtc硅谷,2016年4月4日);节点美国,“流中的背压”,https://nodejs.org/en/docs/guides/backpressuring

in

streams/.另一种选择是在生产者和消费者之间分配足够大的缓冲区,以适应“最坏情况”,但机器的并行性只允许这种“最坏情况”分配的一小部分在同一时间处于活动状态——其余最坏情况下的存储器分配将被浪费或闲置。背压调度允许高效地使用固定大小的存储器分配,最大限度地减少浪费。
65.示例非限制性方法还可以显式地避免任何正在进行的工作需要进行上下文切换。例如,在现代gpu中,需要为上下文切换保存和恢复的状态量可能太大,无法在高性能环境中使用。通过使调度器直接了解芯片中施加的资源约束,例如可用的片上缓冲量,我们的方法使整个系统能够在工作负载中流动,同时最小化传输瞬态数据所需的外部存储器带宽。
66.随着我们将处理器(如gpu)处理的应用范围扩展到机器学习和光线跟踪等新领域,目前的方法将有助于推进用于有效处理此类工作负载的核心编程模型,包括那些表现出不规则并行性的工作负载。特别是,我们模型的示例功能允许在调度复杂的计算工作负载时减少和/或完全消除外部存储器流量,从而提供了一条将性能扩展到外部存储器带宽限制之外的途径。
67.另一方面包括支持该技术的api(应用程序编程接口)。这种api可以存在于许多不同的硬件平台上,开发人员可以使用这种api开发应用程序。
68.另一个方面包括有利地使用存储器高速缓存,以更快的存储器捕获计算级之间的
数据流,从而使用片上存储器在图形管线级之间进行数据通信。这允许实现通过利用片上存储器进行优化,从而通过管线化计算数据流、增加带宽和计算能力来避免昂贵的全局(例如,帧缓冲区)片外存储器操作。
69.还有一个方面涉及数据编组,它以更精细的粒度安排工作。我们不需要同时启动网格中的所有线程或其他线程集合并等待它们全部完成,而是可以以管线方式启动选定的线程或线程组,从而通过不批量同步实现细粒度同步。这意味着我们可以同时运行生产者和消费者——就像在管线模型中一样。
70.示例性非限制性调度模型
71.图4示意性地显示了一个示例性非限制性调度模型100,显示了调度的基本单元。在所示的调度模型100中,向线程块104提供输入有效负载102,线程块104生成输出有效负载106。调度模型100的特征之一是系统定义的编程模型,该模型为线程块104的输入有效载荷和来自线程块的输出有效载荷提供显式的定义。在一个实施例中,有效载荷102、106的显式定义定义了数据分组的大小。
72.例如,该定义可以符合一般程序模型,该模型包括一个显式的系统声明,即线程块104将消耗n字节的输入有效负载102,并产生m字节的输出有效负载106。线程块104(而不是一个实施例中的系统)将关注输入和输出数据有效载荷102、104的含义、解释和/或结构,以及对于线程块的每次调用,线程块实际从其输入有效载荷102读取并写入其输出有效载荷106的数据量。另一方面,一个实施例中的系统确实知道,根据调度模型,输入有效载荷102用于线程块104的输入和该(对系统不透明)输入数据有效载荷的大小,并且类似地,输出有效载荷106用于该线程块的输出和该(对系统不透明)输出数据有效载荷的大小。对于每个线程块104及其各自的输入有效载荷102和输出有效载荷106,存在输入和输出相关性的这种显式声明。
73.图5显示了在一个实施例中使用此类定义,以使系统能够在非常有效的管线中调度线程块104。定义良好的输入和输出允许在对象范围内进行同步,并免除了同步无关工作的需要。这种定义良好的输入和输出声明增加了占用率,并使资源能够在内核启动之前以保守的方式获取(如“获取”箭头所示),并在执行完成之前以编程方式释放(如“释放”箭头所示)。图5显示,在一个实施例中,调度器在线程块启动之前执行资源获取(即,将sf信号量减去线程块预先声明的某个已知量,以反映输出有效负载的大小,从而为线程块保留所需的资源量),而线程块(申请)它本身可以在资源使用完毕之后和执行完毕之前以编程方式释放资源,调度器会记录并跟踪适当的释放量,该释放量可以通过编程方式计算——以提供无死锁的保证,避免上下文切换的需要,并实现额外的灵活性。在一个实施例中,释放可以在程序流中的任何位置执行,并且可以在给定线程块的不同时间增量执行,以使线程块在不再需要那么多资源时能够减少其资源保留。在这样的实施例中,就像一群吃完饭离开餐桌的食客,由线程块决定何时释放资源;基于系统的调度程序将通过保留释放量来识别和利用释放。获取了多少资源和释放了多少资源的聚合操作应一致,以确保每次获取的资源最终被释放,避免了累积错误和相关的死锁以及前进进程的停止。然而,在一个实施例中,何时以及如何获取和释放资源可由线程块以编程方式确定,但由基于系统的调度器监视、强制和利用。
74.图5举例显示了一些抽象输入资源107和输出资源108(例如,片上存储器、系统用
于实现线程块104的数据编组的主存储器分配、硬件资源、计算资源、网络带宽等)。在一些实施例中,这样的输入和输出资源107、108分配的大小/范围是固定的,即,在存储器分配的情况下是有限数量的字节。因为系统知道每个单独线程块104的输入和输出102、106的大小,所以它可以计算可以并行运行的线程块的数量,并基于线程块是生产者还是消费者来安排线程块的启动。例如,如果两个线程块104a、104b参与某种生产者/消费者关系,则系统可以调度生产者并知道生产者将使用哪个输出资源108,然后可以调度来自该输出资源的消费者线程块。通过知道输入有效载荷120和输出有效载荷106的大小,系统可以使用最优资源约束调度来正确地调度管线。例如,见somasundaram等人,《使用最优资源约束(orc)调度的网格计算中的节点分配》,ijcsns国际计算机科学与网络安全杂志,第8卷第6期(2008年6月)。
75.尽管与输入和输出资源107、108的量相关的最常见参数将是存储器大小,但实施例不限于此。输入和输出资源107、108可以是例如网络带宽、通信总线带宽、计算周期、硬件分配、执行优先级、对诸如传感器或显示器之类的输入或输出设备的访问,或者任何其他共享系统资源。作为示例,如果多个线程块104需要共享有限的网络带宽,则类似的资源约束调度可以应用于作为资源约束的网络带宽。每个线程集合或块104将声明其每次调用使用的带宽,从而使系统能够在带宽约束内进行调度。在这种情况下,将使用值或其他度量来指定所需的网络带宽量(即,调用时线程块将占用多少网络带宽资源)。gpu可以支持的并行量将是支持线程块104所需的资源大小的函数。一旦系统知道所需资源的大小/数量,系统就可以最佳地调度计算管线,包括哪些线程块104将并发运行,并在生产者和消费者启动之前分配输入/输出资源。在一个实施例中,在启动线程块之前,系统使其能够获取输出资源108中的空间并相应地跟踪利用率。在一个示例性非限制性实施例中,为了速度和效率,通过可编程硬件电路(例如,硬件计数器和实现空闲和释放计数器/信号量对的集合或池的相关逻辑门)执行这样的系统操作。这种可编程硬件电路可以通过系统软件和系统上运行的应用程序,使用传统的api调用进行编程。
76.图6显示了资源受限调度的示例视图,使用信号量s
f
和s
r
指示受限资源的空闲和就绪容量。s
f
可指示资源的总容量,s
r
可指示已分配或以其他方式要求的资源容量。知道这些值是什么,系统可以计算或以其他方式确定要启动多少(和哪些)线程块,以避免资源过载,并在一些实施例中优化资源利用率。
77.生产者110和消费者112通过资源114连接。资源114可以是软件构造,例如队列。每个内核定义一个固定大小的输入和输出——同步在对象范围内。更详细地说,每个内核在技术上定义了最大输入或输出;生产者可以选择生产达到这个数量的产品。消费者可能(在合并启动案例中)在启动时使用的工作项少于最大数量。系统可以假设生产者110和消费者112将始终运行到完成,并且系统的硬件和/或软件调度机制可以监视系统操作,并基于监视、维护、更新和查看s
f
和s
r
来确定要启动多少线程块。在一个实施例中,系统使用信号量来保证当并发执行的过程需要这些资源时,输入/输出资源将可用,从而确保过程可以运行到完成,而不是过程死锁,必须被抢占或暂停,并且需要等待资源可用,或者需要昂贵的上下文切换和相关联的开销。
78.在一种实现中,基于系统的硬件和/或软件调度代理、机制或过程在后台独立于消费者和生产者运行。调度代理、机制或进程查看信号量s
f
和s
r
,以确定系统在给定的时间步
长可以启动多少生产者和消费者。例如,调度代理、机制或进程可以仅在有可用空间时确定启动生产者,并根据可用空间信号量s
f
的值确定可以启动多少生产者。调度代理、机制或进程类似地基于就绪信号量s
r
确定受约束资源的有效消费者条目的数量。每当启动新线程块时,调度代理、机制或进程都会更新信号量,并且每当线程块在完成和终止之前更新信号量时,调度代理、机制或进程也会监视状态更改。
79.在许多实现中,多个生产者和消费者将并发运行。在这种情况下,由于输入/输出资源是在生产者和消费者启动时获得的,因此输入/输出资源不会被清晰地划分为占用空间和非占用空间。而是,在中间空间中会有一小部分资源。例如,刚开始运行的生产者尚未产生有效的输出,因此就绪信号量s
r
不会将生产者的输出注册为有效数据,并且信号量的值将小于生产者在输出有效数据时最终需要的存储器空间量。在一个实施例中,只有当生产者输出的数据变得有效时,生产者才会通过增加就绪信号量s
r
来说明该实际数据使用情况。示例性实施例使用两个信号量s
f
和s
r
来说明在启动时与并发处理和资源获取相连接的这些事件的时间序列。例如,在启动线程块之前,系统在i/o资源114中保留线程块将需要的输出空间,从而保证该空间在需要时可用,并避免稍后在执行期间获取资源可能导致的死锁情况。它通过借用空闲信号量s
f
来跟踪此资源保留。一旦线程块启动并开始填充或以其他方式使用资源,正在使用的资源量将移动到就绪状态

通过更新信号量s
r
进行跟踪。在启动前分配资源不仅可以避免潜在的死锁,它还有一个好处,就是能够使用不需要处理资源不可用的可能性的算法。这种对数据编组算法分配阶段的潜在大简化是一个好处。
80.类似的分析适用于资源分配的消费者端。在系统启动消费者线程块之前,它会检查就绪信号量s
r
,以确保其非零。当系统启动消费者线程块时,它通过减少就绪信号量s
r
来跟踪消费者将来将消耗资源但尚未消耗资源。只有在消费者消耗保留资源后,系统状态才能将资源转换为可用空间,释放与工作项相关联的资源(在示例实施例中,这由消费者本身完成),并增加空闲空间信号量s
f
。在保留之后和释放之前,工作项处于未确定状态。
81.在一个实施例中,调度代理、机制或过程可以很简单,因为如上所述,它使用两个信号量跟踪资源。在示例实现中,信号量s
f
、s
r
可以通过诸如寄存器和/或计数器之类的硬件电路的集合来实现,这些硬件电路维护、更新和使用成本低廉。在每个周期或以其他周期性或非周期性频率,系统通过简单且廉价的基于硬件的更新(例如,增量和减量,向寄存器/计数器添加或减去整数值,等等)来更新寄存器/计数器,以考虑资源消耗。此外,这种实现可以直接映射到旧的gpu调度实现(由于当前调度技术的简单性,不需要旧的调度器的所有复杂性),因此不需要额外的硬件设计。在其他实现中,可以提供专用于当前调度器的附加简化硬件电路,从而降低芯片面积和功率需求。由于当前调度器可以围绕跟踪资源利用率的简单计数器以抽象的方式实现,因此可以实现简单的调度器。信号量值可以通过硬件或软件机制进行更新。如果使用软件指令,则指令可以体现在系统代码或应用程序代码中。在一个实施例中,请求和释放资源保留的信号由生产者和消费者线程块本身提供。此外,s
f
和s
r
的增加/减少可通过编程方式执行,以响应来自生产者和消费者的请求和释放信号,从而利用早期资源释放。尽管s
f
和s
r
可以被视为调度器的一部分,调度器最终控制这些值,但更高级别的系统组件还可以确保行为不良的生产者和消费者不能占用资源或停止前进。
82.提供了足够多的此类寄存器,以支持并发处理所需的最大任务数,即表示支持可能并发启动的所有消费者和生产者所需的状态数。在一个实施例中,这意味着为每个并发
执行的线程块或应用程序提供一对信号量。调度器可以以流线型方式启动任意数量的并发消费者和生产者节点,并且调度器跟踪资源保留和使用所需保持的状态量不取决于来自同一任务/节点的并发执行线程组的数量。而是,在一个实施例中,调度器为给定节点保持的状态量不随之增长,因此不依赖于等待执行的排队工作项或任务(例如,线程)的数量。调度器维护的状态数将随着要分配的资源数量和系统中活动的着色器程序(任务)数量的增加而增加,这在管线调度模型中取决于管线中的阶段数量,因为一个阶段的输出资源通常包括管线中后续阶段的输入资源。在一个实施例中,调度器为每个“资源”(例如,存储器、高速缓存等)维护一个寄存器以表示s
f
,为每个“任务”维护一个寄存器以表示s
r

83.然而,本文中的技术并不限于创建管线,而是可以支持更复杂的关系和依赖关系。在这种情况下,调度器需要维护的状态量将取决于其负责调度并发执行的节点图的拓扑结构。如上所述,因为在某些实现方式中,调度器被设计为简单而高效,基于上述每个资源的两个值s
f
、s
r
,可能不支持任意复杂度/拓扑图,并且在需要这种灵活性的情况下,可以调用另一种调度算法/安排,代价是增加调度复杂度。
84.此外,在分配的资源是存储器的情况下,调度程序不限于任何存储器类型,而是可以与许多不同的存储器结构一起使用,例如线性存储器缓冲区、堆栈、关联高速缓存等。
85.因为为了保持调度器的简单,示例实施例调度器维护的状态数量是固定大小的,所以导致了一种我们称之为背压的现象。首先,在示例性实施例中,固定大小的约束可能意味着每个资源只使用一对信号器s
f
、s
r
来控制资源的分配和释放,从而控制系统并发。在一个实施例中,每个资源只有两个值,所以调度器维护的状态量不会随着调度器被要求调度的执行节点拓扑图的性质而增长。在示例实施中,每个资源有一对这样的信号器,而在具有多个节点和连接它们的队列的图中,每个队列将有一对信号器。在系统执行开始时初始化这些值s
f
、s
r
时,它们被初始化为管线或图拓扑的那些特定阶段可用的资源量。然后,随着工作的安排,信号量s
f
、s
r
发生变化(增加和减少)。
86.当资源用完时,需要该资源的特定节点将停止被调度,而其他一些节点将开始执行,因为它有可用的资源和输入。
87.在一个实施例中,当启动工作时,空闲和就绪信号量s
f
、s
r
与该工作日志一起原子地被修改。例如,启动工作将原子地减少空闲信号量s
f
,从而防止其他工作在错误地认为存在可用的附加资源的情况下启动,而实际上这些资源已经被预留了。这有点像领班在入座前一直跟踪已经在酒吧喝酒的餐厅顾客人数,以确保分配给他们而不是新来的顾客。这避免了比赛的可能性。同样,调度员原子地减少了释放信号量s
f
,以便将资源分配给生产者。此外,在一些实施例中,应用程序可以在完成工作之前使用信号量释放资源。因此,在示例性非限制性实施例中,资源保留由调度器完成,但资源释放由应用程序/用户代码完成。
88.图7和图8显示了资源获取事件和资源释放事件之间一对一映射的示例。时间从左到右。假设资源是存储器中的缓冲区。如图7所示,在获取时,s
f
从(
“‑‑
sf”)中减去由生产者线程块的显式定义声明的数量,以指示该部分或数量的存储器正在保留以供使用。图8显示生产者线程块将在其生命周期的某个点上增加s
r
,以指示生产者线程块现在实际使用资源来存储有效的存储器数据。由于消费者线程块所需的数据现在有效,调度程序可以启动消费者线程块。调度程序还将在启动消费者时减少s
r

完成两个信号量和生产者/消费者任务之间的对称。在执行期间的某个点,一旦消费者消耗资源,它可以向系统指示它不再需要该
资源。因此,系统通过添加s
f
值将资源转换为可用空间,以响应应用程序/消费者代码执行的释放事件,这意味着调度器现在可以启动其他生产者。如图13所示,在先前启动的生产者释放为其保留的存储器之前,调度程序不会启动另一个生产者;因此,只有在调度程序增加s
f
值以指示现在可用于重新获取的资源量之后,才能进行下一次获取。因此,系统中的并发受到资源利用生命周期的限制。
89.图9是一个示例调度程序视图的流程图,该视图根据输入是否准备就绪以及输出是否可用,启动线程块,如s
r
>0和s
f
>0所示。在一个实施例中,这种功能可以在硬件中实现。可以提供多组硬件电路寄存器,每个寄存器组对应于一个任务。如图9右侧所示,每个寄存器集(可称为“任务描述符”)包含例如:
90.对任务的引用(例如,可用于调用任务实例的名称或地址);
91.s
r
信号量(与任务唯一关联);
92.对至少一个s
f
信号量(*s
f
)的引用(一些实施例允许多个无任务信号量引用来引用一个公共的无任务信号量s
f
;其他实施例也可以使用任务标识符而不是引用值来存储该无任务信号量)。
93.多个任务可以引用同一个信号量,这就是如何实现任务之间的隐式依赖关系。
94.每个时间单位或时钟,调度程序都会查看这些信号量,并将其与零进行比较。如果某个任务的信号量都大于零,则调度程序确定它可以启动该任务并执行此操作,从而在启动时减少该任务的s
f
信号量。当任务使用资源完成时,任务本身(在一个实施例中)通过使用识别要增加哪些信号量的机制使硬件增加s
f
和s
r
信号量。对于就绪信号量,任务可以向与任务关联的任务条目发送增量信号。对于自由信号量,软件将向自由信号量表发送一个带有索引的增量信号。软件可以按名称引用信号量,并按名称命令它们递增。如将理解的,“增量”和“减量”信号或命令实际上可以命令增加(加法)或减少(减法)一个范围内的任何整数。
95.在软件api级别,可由开发人员或应用程序程序员定义或声明并由系统软件提供服务的对象包括:
96.至少一个队列对象,以及
97.引用要运行的代码和要使用的队列对象的任务对象。
98.然后,系统将队列映射到信号量,将任务映射到硬件任务描述符。
99.应用程序将首先指定图形,即要执行的任务节点的拓扑及其依赖关系。为响应此类规范,系统软件将创建队列并初始化硬件和系统。当应用程序触发图形的执行时,gpu系统软件调用图形的根任务,并开始以与cpu分离的自主方式执行图形。参见图9a。当所有队列都被清空且图形执行处于空闲状态时,系统可以确定图形已完成。另一种方法是使用系统解释的令牌来显式地指示工作结束。图9a显示了释放资源的三种可选方式:选项1)通过管线传播工作结束令牌以指定已完成的工作;选项2)等待队列变为空,并且任何活动执行的节点停止运行;选项3)任务中的用户代码确定何时完成并发出信号(示例实施例中最常见的场景)。
100.图10显示了使用队列的资源受限调度示例。如上所述,资源可以是任何类型的存储器结构,但通常使用队列将数据从生产者传递到消费者是有利的,这类似于图形管线中经常使用的模型。在一些实施例中,信号量和测试逻辑在硬件电路中实现,而队列是软件构
造并由系统软件管理。
101.图11显示了生产者如何在消费者从同一队列读取数据的同时将数据写入队列。因此,队列可以包括生产部分、准备部分和消费部分。生产部分包含生产者当前正在写入的有效数据,就绪部分包含生产者先前写入队列且消费者尚未开始消费的有效数据,并且消费部分可以包含生产者先前写入的、消费者当前正在读取的有效数据。
102.图12显示了示例调度器如何使用队列实现数据编组。在此示例中,s
f
信号量跟踪队列中有多少空闲时隙,s
r
信号量跟踪队列中有多少就绪时隙(即,包含消费者尚未释放的有效数据的时隙)。调度器使用这两个信号量来确定是否(何时)启动新的生产者和消费者。图12的底部显示了四个指针:外放置(outer put)、内放置(inner put)、内获取(inner get)和外获取(outer get)。系统软件可以使用这四个指针来管理队列。虽然顶部的信号量和底部的指针似乎在跟踪同一事物,但在一些实施例中,通过调度硬件电路来维护和更新信号量,而底部的指针则通过应用软件来维护和更新。以这种方式将信号量调度功能与队列管理功能解耦提供了某些优势,包括支持各种用例。例如,查看图23,两个不同生产者的s
r
信号量可以增加不同的数量。在一个实施例中,调度器仅查看这些信号量来决定要启动多少消费者。因此,在本例中,生产者和消费者之间没有一对一的对应关系,结果是工作放大,即每个条目启动多个消费者(同一任务的多个实例,或不同任务,或两者)。这可用于处理较大的输出大小。每个条目可以启动任意数量的消费者,所有消费者在其输入端“看到”相同的有效负载。消费者相互协调,以推进管理队列本身的队列指针。此类数据并行操作可用于使不同消费者能够对输入数据执行不同的工作,或对输入数据的不同部分执行相同的工作,或更一般地,对输入数据的相同或不同部分执行相同或不同的工作。通过将队列状态的管理与信号量提供的新工作调度功能解耦,可以启用此功能。
103.图13和14显示,在一些实施例中,如果生产者根本不需要资源,则生产者可以提前释放资源。例如,假设启动了一个生产者,该生产者有条件地生成一个输出。在启动之前,制作者将声明如果生成输出,它将需要的资源量,调度程序通过从s
f
中减去生产者声明它需要的资源量来处理这个问题,从而保证生产者可以运行到完成,并防止另一生产者争夺该资源。但是,如果生产者在执行期间和完成之前确定它不再需要部分或全部该资源(例如,因为它确定它将生成较少或没有输出),生产者可以通过释放其对资源的保留来向系统调度器指示这一点。资源生命周期现在短得多,系统的并发性可以随着释放资源和启动下一个生产者之间的延迟的减少而增加。与图13中的位置相比,图14将第二个生产者移动到了图的左侧,这表明了这一点。具体地,第一生产者将相应地增加s
f
,这使得调度器能够立即允许第二生产者获取资源,以便调度器将再次减少s
f
以记录对该第二生产者的保留。
104.此外,发布可由生产者、消费者或甚至管线中的后续阶段发布。图15显示了三个阶段的管线,最终释放操作由生产者的孙子执行(即,管线序列中生产者之后的下一个连续阶段在管线下游的管线阶段)。因此,只要调度器强制执行一致的获取和释放(即,资源保留量始终由相应的资源释放量抵消),就可以跨多个管线阶段维护资源保留。
105.图16

20显示了另一个示例,其中生产者逐步地释放资源。在图16中,生产者获得n个单位的资源。在产生如图17所示的部分输出时(例如,系统在存储器资源和生产者中保留了n个时隙,但生产者仅输出了图17交叉阴影部分所示的少量时隙值的数据,如m

n时隙值),生产者可以释放未使用的空间m(图18)可供其他生产者使用(图20)。当生产者释放空
间m<n(图19)时,这意味着它已经在资源中存储了一些有效数据,但不需要调度程序可以保留给另一生产者的m个时隙,后者现在可以启动以使用释放的空间m(图20)。在本例中,生产者知道为其保留了n个时隙,并且只使用了m个时隙(就像一家餐厅就餐者为8人保留了一张桌子,但只有6人出现)。在部分释放的情况下,消费者被告知它只能读取n个有效时隙(图19),因此如图20所示,消费者读取并随后释放输出的一部分(s
f
+=n

m),分配的另一部分m由另一生产者获取。因此,信号量s
f
、s
r
可用于指示分数资源使用情况,生产者可返回未使用的分数。来自无关生产者/消费者的另一个资源发布可以及时到来,因此调度程序可以启动另一个生产者。此机制允许处理最大大小为n的变量输出。
106.图21显示调度程序正在跟踪的资源的使用情况可以是可变大小的。
107.示例性高速缓存占用管理
108.在一个示例实施例中,系统调度器获取和释放的资源可以包括高速缓存,信号量s
f
、s
r
跟踪用于生产者和消费者之间数据流的高速缓存线数量。由于许多高速缓存设计为自由地将缓存线与存储器位置关联,因此存储器位置通常不连续,并且高速缓存中的数据碎片相当不可预测。然而,所公开的实施例调度器可以使用单个值s
f
跟踪和管理高速缓存(和主存储器)的使用,因为该值仅指示线程块仍然可以获取多少高速缓存线。
109.如图22所示,非限制性实施例调度器跟踪的是高速缓存工作集,即正在使用的交叉阴影缓存线。硬件高速缓存是优秀的分配器,这意味着高速缓存线可以任意分段。当生产者通过完全关联高速缓存写入外部存储器时,会分配高速缓存线(类似地,如果生产者未写入部分外部存储器,则不会分配高速缓存线)。在实际实现中,高速缓存通常设置为集合关联的,因此它们在这方面没有完美的行为,但仍然很接近。系统调度器试图做的是按照高速缓存线可以廉价动态分配的近似方式进行调度。
110.所公开的实施例调度器可以忽略高速缓存内的高速缓存片段固定大小的块主存分配的方式,因为调度决策不是基于实际或最坏情况的使用,而是基于已保留/获取和释放的高速缓存线的总数,而不考虑这些高速缓存线是如何分配的可能映射到物理存储器地址。这意味着将高速缓存线分配给固定大小的主存储器块(软件开发的一个优势是使用固定大小的存储器块分配)可以由高速缓存管理,并且系统调度器可以仅基于用于物理存储器分配的高速缓存线的总数来跟踪物理存储器使用情况,而不必关心分配了或没有分配哪些特定的固定大小的存储器块。例如,生产者只能部分写入固定块存储器分配的每个块,消费者只能部分读取。但是调度不需要跟踪哪些块被读取或写入,因为高速缓存会自动跟踪这些块。使用关联的高速缓存跟踪存储器映射可以避免调度器需要使用链表或其他任何机制,而其他一些调度器可能需要使用这些机制通过物理外部存储器跟踪存储器更新的碎片。示例调度器可以通过使用现有高速缓存分配机制来管理碎片而忽略碎片,同时仍然允许gpu上的数据流留在片上缓存中/通过片上高速缓存。通过以固定大小的块管理高速缓存占用空间,系统通过利用高速缓存捕获数据流的能力来获得片上数据流的好处,相对于更大的存储器分配而言,数据流是稀疏的,因为高速缓存捕获的是实际读取和写入的内容。如果调度器可以将其工作集管理为有限大小,则高速缓存提供了调度器可以利用的有限工作大小。示例非限制性调度模型使这一点得以实现。示例性实施例可以在可使缓存更高效的附加调度约束上分层。
111.此外,当消费者完成从高速缓存线仅读取(而不是更新)数据时,可能需要销毁(使
缓存线失效而不向外推到外部存储器),以便释放高速缓存线并将其返回计划程序,而无需将高速缓存线写回外部存储器。这意味着生产者产生的数据可以通过片上缓存传递给消费者,而无需将其写入外部存储器,从而节省存储器周期和存储器带宽,减少延迟。同时,调度器通过高速缓存线的资源约束调度来主动管理高速缓存,提高了高速缓存效率。
112.概括功能,如工作放大和工作扩展
113.图23和下图显示了可构造为使用所公开的调度器实施例的示例非限制性执行节点图拓扑。上面结合图12描述了图23。
114.图24通过使用阴影显示,启动的消费者不必是相同的程序。同一消费者的多个实例或多个不同消费者(即,同一任务或不同任务的不同实例)可以同时从同一队列启动。
115.图25显示了工作聚合的示例,这是一种与工作扩展互补的机制。在这里,生产者将一些工作放入一个特殊队列中,该队列不会立即启动消费者。而是,队列是使用聚合窗口编程的,该窗口允许它对生产者在特定时间窗口内和/或基于生产者产生的特定数量的元素产生的工作进行分组和处理。在一个示例中,队列将尝试在启动消费者任务实例之前收集最多用户指定数量的工作项。队列还将向启动的消费者报告它能够收集多少项(最小值=1,最大值=用户指定的值)。为了使用此功能,将对图9进行修改,以与此编程值进行比较,并且s
r
将增加此编程值。该用例非常有用,例如,可以使用硬件支持来确保线程块中的所有线程在线程块启动时都有要处理的数据。
116.图26显示了超时时发生为了恢复一致性的情况。如果图25中的聚合窗口没有完全填充,则会发生超时并部分启动以减少延迟。在这种部分启动中,s
r
信号量减少实际已启动的元素数量而不是用户定义的可编程值。超时设置了队列在启动消费者之前等待的时间上限。
117.图27显示了一个连接操作示例。不同的生产者502提供不同的作品,这些作品试图填充具有对应于不同生产者的不同字段的公共结构。在所示的示例中,三个生产者中的每一个将字段写入输出数据记录508(1)、508(n)中的每一个,其中每个输出数据记录包含分别对应于三个生产者502(a)、502(b)、502(c)的三个字段510(a)、150(b)、510(c)。即,数据生产者502(a)写入每个输出记录508的字段510(a),数据生产者502(b)写入字段502(b),并且数据生产者502(c)写入字段502(c)。因此,每个生产者502在输出缓冲器中具有其自己的独立空间概念。每个生产者502具有其各自的自由信号量s
f
,其允许生产者填写其自己的字段510,只要有空间。因此,有三个冗余空闲信号量s
f
a、s
f
b和s
f
c,它们都引用相同的输出记录,但每个都跟踪这些输出记录中不同的字段510集。因此,在该示例性实施例中,这些信号量一对一映射到资源占用,但是其他布置是可能的。
118.在所示示例中,填充其各自的最后记录字段的最后生产者506负责推进就绪信号量s
r
,以指示输出记录508可由消费者读取。由于调度非常简单,因此可以使用相对少量的调度硬件来管理相对复杂的输出数据结构。
119.图28显示了使用聚合将工作排序到多个队列以重新收敛执行。这里,生产者输出到三个不同的队列,这些队列将数据编组到三个不同的消费者。本文描述的调度机制适用于三个队列中的每一个。
120.图29显示了将工作排序到多个队列以进行着色器专门化的示例,图30显示了工作排序以减少实时光线跟踪中的收敛。光线可以产生随机收敛的功。通过在这些拓扑中排列
多个队列以及合并(如队列尾部的元素变大所示),可以增加处理的一致性以减少随机收敛。这种聚合可用于将工作分为一致的批。这种安排可以在系统出现分歧时使用。
121.图31显示了一个基本的依赖关系调度模型,该模型概括了上述内容,根据对已完成任务的依赖关系选择要启动的多个任务。
122.图32和33显示了如何使用这些通用概念来实现图形任务和为图形光栅器提供数据的图形管线的示例。
123.本文引用的所有专利和出版物均以引用方式并入,用于所有目的,如同明确规定一样。
当前第1页1 2 
网友询问留言 已有0条留言
  • 还没有人留言评论。精彩留言会获得点赞!
1