网站关键词设定,安徽工程建设信息网站王开林,义安区住房和城乡建设局建网站,wordpress模板程序原文#xff1a;towardsdatascience.com/gpu-accelerated-polars-intuitively-and-exhaustively-explained-e823a82f92a8 我最近参加了由 Cuda 和 Polars 团队举办的一个秘密演示。他们让我通过金属探测器#xff0c;给我头上套了一个袋子#xff0c;然后开车把我带到法国乡…原文towardsdatascience.com/gpu-accelerated-polars-intuitively-and-exhaustively-explained-e823a82f92a8我最近参加了由 Cuda 和 Polars 团队举办的一个秘密演示。他们让我通过金属探测器给我头上套了一个袋子然后开车把我带到法国乡村森林中的一间小屋。他们拿走了我的手机、钱包和护照以确保我不会在最终展示他们一直在研究的东西之前泄露任何信息。或者感觉就是这样。实际上那是一个 Zoom 会议他们礼貌地要求我在指定的时间之前不要说话但作为一个技术作家这种神秘感让我感觉有点像詹姆斯·邦德。在这篇文章中我们将讨论那次会议的内容Polars 中一个新的执行引擎它使 GPU 加速计算成为可能允许对 100GB 的数据进行交互式操作。我们将讨论在 Polars 中数据框是什么GPU 加速如何与 Polars 数据框协同工作以及使用新的 CUDA 驱动的执行引擎可以期待的性能提升有多大。Who is this useful for?Anyone who works with data and wants to work faster.How advanced is this post?This post contains simple but cutting-edge data engineering concepts. It’s relevant to readers of all levels.Pre-requisites:NoneNote:At the time of writing I am not affiliated with or endorsed by Polars or Nvidia in any way.Polars In a Nutshell在 Polars 中你可以创建和操作数据框它们就像超级强大的电子表格。在这里我创建了一个简单的 dataframe包含一些人的年龄和他们居住的城市。 Creating a simple dataframe in polars importpolarsaspl dfpl.DataFrame({name:[Alice,Bob,Charlie,Jill,William],age:[25,30,35,22,40],city:[New York,Los Angeles,Chicago,New York,Chicago]})print(df)shape:(5,3)┌─────────┬─────┬─────────────┐ │ name ┆ age ┆ city │ │---┆---┆---│ │str┆ i64 ┆str│ ╞═════════╪═════╪═════════════╡ │ Alice ┆25┆ New York │ │ Bob ┆30┆ Los Angeles │ │ Charlie ┆35┆ Chicago │ │ Jill ┆22┆ New York │ │ William ┆40┆ Chicago │ └─────────┴─────┴─────────────┘使用这个 dataframe你可以进行诸如按年龄筛选等操作。 Filtering the previously defined dataframe to only show rows that have an age of over 28 df_filtereddf.filter(pl.col(age)28)print(df_filtered)shape:(3,3)┌─────────┬─────┬─────────────┐ │ name ┆ age ┆ city │ │---┆---┆---│ │str┆ i64 ┆str│ ╞═════════╪═════╪═════════════╡ │ Bob ┆30┆ Los Angeles │ │ Charlie ┆35┆ Chicago │ │ William ┆40┆ Chicago │ └─────────┴─────┴─────────────┘你可以进行数学运算 Creating a new column called age_doubled which is double the age column. dfdf.with_columns([(pl.col(age)*2).alias(age_doubled)])print(df)shape:(5,4)┌─────────┬─────┬─────────────┬─────────────┐ │ name ┆ age ┆ city ┆ age_doubled │ │---┆---┆---┆---│ │str┆ i64 ┆str┆ i64 │ ╞═════════╪═════╪═════════════╪═════════════╡ │ Alice ┆25┆ New York ┆50│ │ Bob ┆30┆ Los Angeles ┆60│ │ Charlie ┆35┆ Chicago ┆70│ │ Jill ┆22┆ New York ┆44│ │ William ┆40┆ Chicago ┆80│ └─────────┴─────┴─────────────┴─────────────┘你可以执行聚合函数比如计算一个城市中平均年龄。 Calculating the average age by city df_aggregateddf.group_by(city).agg(pl.col(age).mean())print(df_aggregated)shape:(3,2)┌─────────────┬──────┐ │ city ┆ age │ │---┆---│ │str┆ f64 │ ╞═════════════╪══════╡ │ Chicago ┆37.5│ │ New York ┆23.5│ │ Los Angeles ┆30.0│ └─────────────┴──────┘大多数阅读这篇文章的人可能都熟悉 Pandas这是 Python 中更受欢迎的 dataframe 库。我认为在我们探讨 GPU 加速的 Polars 之前探索一个区分 Polars 和 Pandas 的重要特性可能是有用的。Polars LazyFramesPolars 有两种基本的执行模式“eager” 和 “lazy”。一个 eager 的 dataframe 会在被调用时立即进行计算正好按照它们被调用的方式。如果你给一个列中的每个值加2然后再给那个列中的每个值加3每个操作都会像使用 eager dataframe 时预期的那样执行。每个值都会加上2然后每个这些值都会在你说这些操作应该发生的确切时刻加上3。importpolarsaspl# Create a DataFrame with a list of numbersdfpl.DataFrame({numbers:[1,2,3,4,5]})# Add 2 to every number and overwrite the original numbers columndfdf.with_columns(pl.col(numbers)2)# Add 3 to the updated numbers columndfdf.with_columns(pl.col(numbers)3)print(df)shape:(5,1)┌─────────┐ │ numbers │ │---│ │ i64 │ ╞═════════╡ │6│ │7│ │8│ │9│ │10│ └─────────┘如果我们用.lazy()函数初始化我们的 dataframe我们会得到一个非常不同的输出。importpolarsaspl# Create a lazy DataFrame with a list of numbersdfpl.DataFrame({numbers:[1,2,3,4,5]}).lazy()# -------------------------- Lazy Initialization# Add 2 to every number and overwrite the original numbers columndfdf.with_columns(pl.col(numbers)2)# Add 3 to the updated numbers columndfdf.with_columns(pl.col(numbers)3)print(df)naive plan:(run LazyFrame.explain(optimizedTrue)to see the optimized plan)WITH_COLUMNS:[[(col(numbers))(3)]]WITH_COLUMNS:[[(col(numbers))(2)]]DF[numbers];PROJECT*/1COLUMNS;SELECTION:None我们得到的不是一个 dataframe而是一个类似 SQL 的表达式它概述了为了得到我们想要的 dataframe 需要执行的操作。我们可以调用.collect()来实际运行这些计算并获取我们的 dataframe。print(df.collect())shape:(5,1)┌─────────┐ │ numbers │ │---│ │ i64 │ ╞═════════╡ │6│ │7│ │8│ │9│ │10│ └─────────┘初看这可能似乎没有太大用处我们是在代码的哪个部分进行所有计算又有什么关系呢实际上没有人关心。这个系统的优势不在于计算发生的时间而在于发生的是什么样的计算。在执行懒态 dataframe 之前Polars 会查看累积的操作并找出任何可能加快执行速度的捷径。这个过程通常被称为“查询优化”。例如如果我们创建一个懒态 dataframe 然后在数据上运行一些操作我们会得到一些 SQL 表达式# Create a DataFrame with a list of numbersdfpl.DataFrame({col_0:[1,2,3,4,5],col_1:[8,7,6,5,4],col_2:[-1,-2,-3,-4,-5]}).lazy()#doing some random operationsdfdf.filter(pl.col(col_0)0)dfdf.with_columns((pl.col(col_1)*2).alias(col_1_double))dfdf.group_by(col_2).agg(pl.sum(col_1_double))print(df)naive plan:(run LazyFrame.explain(optimizedTrue)to see the optimized plan)AGGREGATE[col(col_1_double).sum()]BY[col(col_2)]FROM WITH_COLUMNS:[[(col(col_1))*(2)].alias(col_1_double)]FILTER[(col(col_0))(0)]FROM DF[col_0,col_1,col_2];PROJECT*/3COLUMNS;SELECTION:None但如果我们对那个 dataframe 运行.explain(optimizedTrue)我们会得到一个不同的表达式这是 Polars 认为执行相同操作更优的方式。print(df.explain(optimizedTrue))AGGREGATE[col(col_1_double).sum()]BY[col(col_2)]FROM WITH_COLUMNS:[[(col(col_1))*(2)].alias(col_1_double)]DF[col_0,col_1,col_2];PROJECT*/3COLUMNS;SELECTION:[(col(col_0)) (0)]这实际上是当你对懒态 dataframe 调用.collect()时运行的优化表达式。这不仅仅只是花哨和有趣它还可以带来一些相当严重的性能提升。在这里我正在对两个相同的数据 frame 运行相同的操作一个急切一个懒态。我在 10 次运行中平均执行时间并计算平均速度差异。Performing the same operations on the same data between two dataframes, one with eager execution and one with lazy execution, and calculating the difference in execution speed. importpolarsasplimportnumpyasnpimporttime# Specifying constantsnum_rows20_000_000# 20 million rowsnum_cols10# 10 columnsn10# Number of times to repeat the test# Generate random datanp.random.seed(0)# Set seed for reproducibilitydata{fcol_{i}:np.random.randn(num_rows)foriinrange(num_cols)}# Define a function that works for both lazy and eager DataFramesdefapply_transformations(df):dfdf.filter(pl.col(col_0)0)# Filter rows where col_0 is greater than 0dfdf.with_columns((pl.col(col_1)*2).alias(col_1_double))# Double col_1dfdf.group_by(col_2).agg(pl.sum(col_1_double))# Group by col_2 and aggregatereturndf# Variables to store total durations for eager and lazy executiontotal_eager_duration0total_lazy_duration0# Perform the test n timesforiinrange(n):print(fRun{i1}/{n})# Create fresh DataFrames for each run (polars operations can be in-place, so ensure clean DF)df1pl.DataFrame(data)df2pl.DataFrame(data).lazy()# Measure eager execution timestart_time_eagertime.time()eager_resultapply_transformations(df1)# Eager executioneager_durationtime.time()-start_time_eager total_eager_durationeager_durationprint(fEager execution time:{eager_duration:.2f}seconds)# Measure lazy execution timestart_time_lazytime.time()lazy_resultapply_transformations(df2).collect()# Lazy executionlazy_durationtime.time()-start_time_lazy total_lazy_durationlazy_durationprint(fLazy execution time:{lazy_duration:.2f}seconds)# Calculating the average execution timeaverage_eager_durationtotal_eager_duration/n average_lazy_durationtotal_lazy_duration/n#calculating how much faster lazy execution wasfaster(average_eager_duration-average_lazy_duration)/average_eager_duration*100print(fnAverage eager execution time over{n}runs:{average_eager_duration:.2f}seconds)print(fAverage lazy execution time over{n}runs:{average_lazy_duration:.2f}seconds)print(fLazy took{faster:.2f}% less time)Run1/10Eager execution time:3.07seconds Lazy execution time:2.70seconds Run2/10Eager execution time:4.17seconds Lazy execution time:2.69seconds Run3/10Eager execution time:2.97seconds Lazy execution time:2.76seconds Run4/10Eager execution time:4.21seconds Lazy execution time:2.74seconds Run5/10Eager execution time:2.97seconds Lazy execution time:2.77seconds Run6/10Eager execution time:4.12seconds Lazy execution time:2.80seconds Run7/10Eager execution time:3.00seconds Lazy execution time:2.72seconds Run8/10Eager execution time:4.53seconds Lazy execution time:2.76seconds Run9/10Eager execution time:3.14seconds Lazy execution time:3.08seconds Run10/10Eager execution time:4.26seconds Lazy execution time:2.77seconds Average eager execution time over10runs:3.64seconds Average lazy execution time over10runs:2.78seconds Lazy took23.75%less time23.75%的性能提升不容小觑并且是由懒执行在 Pandas 中不存在实现的。在幕后当你使用 Polars 懒态 dataframe 时你实际上是在定义一个高级计算图Polars 会对其进行各种花哨的魔法操作。在优化查询后它执行查询这意味着你得到的结果与使用急切 dataframe 得到的结果相同但通常更快。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/5686465ddadc0eac22cf9fc9bf675e0b.png在 Polars 中调用查询后启动的操作的高级分解。值得注意的是急切执行本身有许多优化改进比如原生多核支持这在懒执行中存在并得到了改进。个人而言我是个 Pandas 粉丝并没有真正看到有充分的理由去切换。我想“它可能更好但可能还不够好到让我放弃我最基本工具的程度”。如果你有同样的感觉而且23.75%的提升幅度没有让你皱眉那么我确实有一些结果要给你看。介绍 Polars 的 GPU 执行这个功能是最新发布的所以我不能 100%确定你将如何在你自己的环境中利用 GPU 加速。在撰写本文时我得到了一个 wheel 文件这就像一个可以安装的本地库。我有一种印象在本文发布后你可以在你的机器上使用以下命令来安装带有 GPU 加速的 polar。pip install polars[gpu]--extra-index-urlhttps://pypi.nvidia.com我还预计如果那不起作用你可以在polars pypi页面上找到一些文档。无论如何一旦你启动并运行你就可以开始在你的 GPU 上使用 polar 的强大功能。你唯一需要做的是在collect一个懒的 dataframe 时指定 GPU 作为引擎。在比较之前测试中的急切执行和懒执行的基础上让我们再比较一下懒执行与 GPU 引擎。我们可以通过以下行results df.collect(enginegpu_engine)来实现其中gpu_engine是基于以下内容指定的gpu_enginepl.GPUEngine(device0,# This is the defaultraise_on_failTrue,# Fail loudly if we cant run on the GPU.)GPU 执行引擎不支持所有 polar 功能默认情况下会回退到 CPU。通过设置raise_on_failTrue我们指定如果 GPU 执行不受支持则代码应抛出异常。好的这是实际的代码。Performing the same operations on the same data between three dataframes, one with eager execution, one with lazy execution, and one with lazy execution and GPU acceleration. Calculating the difference in execution speed between the three. importpolarsasplimportnumpyasnpimporttime# Creating a large random DataFramenum_rows20_000_000# 20 million rowsnum_cols10# 10 columnsn10# Number of times to repeat the test# Generate random datanp.random.seed(0)# Set seed for reproducibilitydata{fcol_{i}:np.random.randn(num_rows)foriinrange(num_cols)}# Defining a function that works for both lazy and eager DataFramesdefapply_transformations(df):dfdf.filter(pl.col(col_0)0)# Filter rows where col_0 is greater than 0dfdf.with_columns((pl.col(col_1)*2).alias(col_1_double))# Double col_1dfdf.group_by(col_2).agg(pl.sum(col_1_double))# Group by col_2 and aggregatereturndf# Variables to store total durations for eager and lazy executiontotal_eager_duration0total_lazy_duration0total_lazy_GPU_duration0# Performing the test n timesforiinrange(n):print(fRun{i1}/{n})# Create fresh DataFrames for each run (polars operations can be in-place, so ensure clean DF)df1pl.DataFrame(data)df2pl.DataFrame(data).lazy()df2pl.DataFrame(data).lazy()# Measure eager execution timestart_time_eagertime.time()eager_resultapply_transformations(df1)# Eager executioneager_durationtime.time()-start_time_eager total_eager_durationeager_durationprint(fEager execution time:{eager_duration:.2f}seconds)# Measure lazy execution timestart_time_lazytime.time()lazy_resultapply_transformations(df2).collect()# Lazy executionlazy_durationtime.time()-start_time_lazy total_lazy_durationlazy_durationprint(fLazy execution time:{lazy_duration:.2f}seconds)# Defining GPU Enginegpu_enginepl.GPUEngine(device0,# This is the defaultraise_on_failTrue,# Fail loudly if we cant run on the GPU.)# Measure lazy execution timestart_time_lazy_GPUtime.time()lazy_resultapply_transformations(df2).collect(enginegpu_engine)# Lazy execution with GPUlazy_GPU_durationtime.time()-start_time_lazy_GPU total_lazy_GPU_durationlazy_GPU_durationprint(fLazy execution time:{lazy_GPU_duration:.2f}seconds)# Calculating the average execution timeaverage_eager_durationtotal_eager_duration/n average_lazy_durationtotal_lazy_duration/n average_lazy_GPU_durationtotal_lazy_GPU_duration/n#calculating how much faster lazy execution wasfaster_1(average_eager_duration-average_lazy_duration)/average_eager_duration*100faster_2(average_lazy_duration-average_lazy_GPU_duration)/average_lazy_duration*100faster_3(average_eager_duration-average_lazy_GPU_duration)/average_eager_duration*100print(fnAverage eager execution time over{n}runs:{average_eager_duration:.2f}seconds)print(fAverage lazy execution time over{n}runs:{average_lazy_duration:.2f}seconds)print(fAverage lazy execution time over{n}runs:{average_lazy_GPU_duration:.2f}seconds)print(fLazy was{faster_1:.2f}% faster than eager)print(fGPU was{faster_2:.2f}% faster than CPU Lazy and{faster_3:.2f}% faster than CPU eager)Run1/10Eager execution time:0.74seconds Lazy execution time:0.66seconds Lazy execution time:0.17seconds Run2/10Eager execution time:0.72seconds Lazy execution time:0.65seconds Lazy execution time:0.17seconds Run3/10Eager execution time:0.82seconds Lazy execution time:0.76seconds Lazy execution time:0.17seconds Run4/10Eager execution time:0.81seconds Lazy execution time:0.69seconds Lazy execution time:0.18seconds Run5/10Eager execution time:0.79seconds Lazy execution time:0.66seconds Lazy execution time:0.18seconds Run6/10Eager execution time:0.75seconds Lazy execution time:0.63seconds Lazy execution time:0.18seconds Run7/10Eager execution time:0.77seconds Lazy execution time:0.72seconds Lazy execution time:0.18seconds Run8/10Eager execution time:0.77seconds Lazy execution time:0.72seconds Lazy execution time:0.17seconds Run9/10Eager execution time:0.77seconds Lazy execution time:0.72seconds Lazy execution time:0.17seconds Run10/10Eager execution time:0.77seconds Lazy execution time:0.70seconds Lazy execution time:0.17seconds Average eager execution time over10runs:0.77seconds Average lazy execution time over10runs:0.69seconds Average lazy execution time over10runs:0.17seconds Lazy was10.30%faster than eager GPU was74.78%faster than CPU Lazyand77.38%faster than CPU eager(注意这是一个与之前测试类似的测试但在不同的、更大的机器上。因此执行时间与之前的测试不同)是的。74.78%更快。而且这甚至不是一个特别大的数据集。人们可能会期待在更大的数据集上获得更大的性能提升。不幸的是我无法分享 Nvidia 和 Polar 团队提供的演示文稿但我可以描述我所理解在底层发生的事情。基本上Polar 有几个执行引擎用于各种任务他们基本上只是添加了一个支持 GPU 的引擎。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/1b30fc04306f74ea72bc818f29c40bcd.png在输入了一大批查询之后查询优化器优化查询并将操作发送到多种执行引擎之一。现在有一个新的由 GPU 驱动的执行引擎。根据我的理解这些引擎是即时调用的既基于可用的硬件也基于正在执行的查询。一些查询高度可并行化在 GPU 上表现极好而那些不太可并行化的操作则可以在 CPU 上的内存引擎中完成。从理论上讲这使得 CUDA 加速的 polar 几乎总是更快我发现这一点在数据集较大的情况下尤其明显。抽象内存管理英伟达团队提出的一个关键观点是新的查询优化器足够聪明能够处理 CPU 和 GPU 之间的内存管理。对于那些没有太多 GPU 编程经验的人来说CPU 和 GPU 有不同的内存CPU 使用 RAM 来存储信息而 GPU 使用 vRAM这是存储在 GPU 本身上的。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/724cdeb618757a744eee39257bac30db.pngCPU 和 GPU 有点像独立的计算机各自拥有自己的资源彼此之间进行通信。CPU 进行计算其 RAM 存储数据而 GPU 也进行计算其显卡上的 vRAM 也存储数据。这些独立且某种程度上是自主的系统需要在复杂任务上协同工作。此图来自我关于CUDA 编程的 AI的文章。可以想象一个场景即创建一个 polars 数据框并在 GPU 执行引擎上执行。然后可以想象一个需要该数据框与仍在 CPU 上的另一个数据框交互的操作。polars 查询优化器能够通过在 CPU 和 GPU 之间按需传递数据来理解和处理这种差异。对我来说这是一项巨大的资产但也存在一些棘手的不便。当你在大型的重负载例如构建 AI 模型上使用 GPU 时通常需要严格管理内存消耗。我想象一下具有大型模型的工作流程这些模型占据了 GPU 的大量空间可能会遇到 polars 在您的数据上随意操作的问题。我注意到 GPU 执行引擎似乎被指向单个 GPU所以可能拥有多个 GPU 的机器可以更严格地隔离内存。尽管如此我认为这对大多数人来说几乎没有实际意义。一般来说当进行数据科学工作时你从原始数据开始进行大量数据处理然后保存旨在为模型准备就绪的工件。我很难想到一个必须在同一台机器上同时进行大数据处理和建模工作的用例。如果你认为这是一个大问题那么英伟达和 Polars 团队目前正在调查显式内存控制这可能在未来的版本中出现。对于纯粹的数据处理工作负载我想象自动处理 RAM 和 vRAM 将大大节省许多数据科学家和工程师的时间。结论非常吸引人的内容而且是最新发布的。通常我需要花费数周时间来审查已经确立数月甚至数年的主题所以“预发布”对我来说有点新鲜。坦白说我不知道这对我的一般工作流程会有多大影响。我可能仍然会在谷歌 Colab 的牛仔式编码中大量使用 pandas因为我对它很舒服但面对大数据框和计算密集型查询时我想我会更频繁地转向 Polars并且我预计最终会将它整合到我的工作流程的核心部分。这其中的一个重要原因是 GPU 加速的 Polars 在几乎任何其他数据框工具上都能带来天文数字的速度提升。加入直观且详尽解释在 IAEE你可以找到长篇内容就像你刚刚读到的文章基于我的数据科学家、工程总监和企业家经验的思想碎片一个专注于学习 AI 的 Discord 社区定期讲座和办公时间https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/f7bb8f638adbb404394a7fc1fa71ebb1.png加入 IAEE