From 6509c8a0723ff4f3511162114f2226785bc4f912 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 8 Jan 2021 09:08:21 -0600 Subject: [PATCH 01/49] Add in data source v2, csv file and stub test --- .../src/main/python/datasourcev2_read.py | 71 ++++++++++++++++++ .../src/test/resources/people.csv | 66 ++++++++++++++++ ...-datasource-v2-columnar-1.0.0-SNAPSHOT.jar | Bin 0 -> 76751 bytes 3 files changed, 137 insertions(+) create mode 100644 integration_tests/src/main/python/datasourcev2_read.py create mode 100644 integration_tests/src/test/resources/people.csv create mode 100644 integration_tests/src/test/resources/spark-datasource-v2-columnar-1.0.0-SNAPSHOT.jar diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py new file mode 100644 index 00000000000..681217740c0 --- /dev/null +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -0,0 +1,71 @@ +# Copyright (c) 2021, NVIDIA CORPORATION. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from asserts import assert_gpu_and_cpu_are_equal_collect, assert_gpu_and_cpu_writes_are_equal_collect, assert_gpu_fallback_collect +from datetime import date, datetime, timezone +from data_gen import * +from marks import * +from pyspark.sql.types import * +from spark_session import with_cpu_session, with_gpu_session + +def read_parquet_df(data_path): + return lambda spark : spark.read.parquet(data_path) + +def read_parquet_sql(data_path): + return lambda spark : spark.sql('select * from parquet.`{}`'.format(data_path)) + + +# Override decimal_gens because decimal with negative scale is unsupported in parquet reading +decimal_gens = [DecimalGen(), DecimalGen(precision=7, scale=3), DecimalGen(precision=10, scale=10), + DecimalGen(precision=9, scale=0), DecimalGen(precision=18, scale=15)] + +parquet_gens_list = [[byte_gen, short_gen, int_gen, long_gen, float_gen, double_gen, + string_gen, boolean_gen, date_gen, + TimestampGen(start=datetime(1900, 1, 1, tzinfo=timezone.utc)), ArrayGen(byte_gen), + ArrayGen(long_gen), ArrayGen(string_gen), ArrayGen(date_gen), + ArrayGen(TimestampGen(start=datetime(1900, 1, 1, tzinfo=timezone.utc))), + ArrayGen(DecimalGen()), + ArrayGen(ArrayGen(byte_gen)), + StructGen([['child0', ArrayGen(byte_gen)], ['child1', byte_gen], ['child2', float_gen], ['child3', DecimalGen()]]), + ArrayGen(StructGen([['child0', string_gen], ['child1', double_gen], ['child2', int_gen]]))] + + map_gens_sample + decimal_gens, + pytest.param([timestamp_gen], marks=pytest.mark.xfail(reason='https://github.com/NVIDIA/spark-rapids/issues/132'))] + +# test with original parquet file reader, the multi-file parallel reader for cloud, and coalesce file reader for +# non-cloud +original_parquet_file_reader_conf = {'spark.rapids.sql.format.parquet.reader.type': 'PERFILE'} +multithreaded_parquet_file_reader_conf = {'spark.rapids.sql.format.parquet.reader.type': 'MULTITHREADED'} +coalesce_parquet_file_reader_conf = {'spark.rapids.sql.format.parquet.reader.type': 'COALESCING'} +reader_opt_confs = [original_parquet_file_reader_conf, multithreaded_parquet_file_reader_conf, + coalesce_parquet_file_reader_conf] + +@pytest.mark.parametrize('parquet_gens', parquet_gens_list, ids=idfn) +@pytest.mark.parametrize('read_func', [read_parquet_df, read_parquet_sql]) +@pytest.mark.parametrize('reader_confs', reader_opt_confs) +@pytest.mark.parametrize('v1_enabled_list', ["", "parquet"]) +def test_read_round_trip(spark_tmp_path, parquet_gens, read_func, reader_confs, v1_enabled_list): + gen_list = [('_c' + str(i), gen) for i, gen in enumerate(parquet_gens)] + data_path = spark_tmp_path + '/PARQUET_DATA' + with_cpu_session( + lambda spark : gen_df(spark, gen_list).write.parquet(data_path), + conf={'spark.sql.legacy.parquet.datetimeRebaseModeInWrite': 'CORRECTED'}) + all_confs = reader_confs.copy() + all_confs.update({'spark.sql.sources.useV1SourceList': v1_enabled_list, 'spark.sql.legacy.parquet.datetimeRebaseModeInRead': 'CORRECTED'}) + # once https://github.com/NVIDIA/spark-rapids/issues/1126 is in we can remove spark.sql.legacy.parquet.datetimeRebaseModeInRead config which is a workaround + # for nested timestamp/date support + assert_gpu_and_cpu_are_equal_collect(read_func(data_path), + conf=all_confs) + diff --git a/integration_tests/src/test/resources/people.csv b/integration_tests/src/test/resources/people.csv new file mode 100644 index 00000000000..d9781c69aef --- /dev/null +++ b/integration_tests/src/test/resources/people.csv @@ -0,0 +1,66 @@ +Jackelyn,23,Engineer +Jeannie,40,Banker +Mariella,20,Professor +Ardith,33,Professor +Elena,41,Banker +Noma,39,Student +Corliss,24,Student +Denae,24,Banker +Ned,44,Professor +Karolyn,,Engineer +Cornelia,45,Sales Manager +Kiyoko,41,Banker +Denisha,45,Engineer +Hilton,21,Sales Manager +Becky,32,Sales Manager +Wendie,29,Banker +Veronica,25,Sales Manager +Carolina,26,Student +Laquita,47,Banker +Stephani,,Professor +Emile,29,Professor +Octavio,35,Banker +Florencio,34,Banker +Elna,38,Banker +Cherri,23,Banker +Raleigh,47,Banker +Hollis,32,Professor +Charlette,35,Professor +Yetta,31,Student +Alfreda,,Engineer +Brigette,45,Banker +Maryann,40,Sales Manager +Miki,37,Banker +Pearle,37,Sales Manager +Damian,21,Sales Manager +Theodore,34,Student +Kristi,46,Engineer +Izetta,45,Professor +Tammi,43,Engineer +Star,,Sales Manager +Kaylee,27,Professor +Lakeshia,48,Professor +Elba,43,Sales Manager +Valencia,20,Engineer +Randa,35,Banker +Lourie,36,Professor +Tracie,31,Banker +Antwan,40,Professor +Gerry,23,Student +Jason,,Banker +Steve,51,CEO +Faker,25,Gamer +가나다라마바사,30,Banker +가나다마바사,20,Banker +가나다나다라마바,21,Professor +가나다나라마바사,22,Engineer +가나나나가나나,23,Sales Manager +가나다나다나나다,24,Banker +나다나다가나다,25,Student +Elon,,CEO +Yohwan,38,Gamer +Kitaek,48,Jobless +Kijung,22,Tutor +KiWoo,23,Tutor +Dongik,43,CEO +Dasong,9,Student \ No newline at end of file diff --git a/integration_tests/src/test/resources/spark-datasource-v2-columnar-1.0.0-SNAPSHOT.jar b/integration_tests/src/test/resources/spark-datasource-v2-columnar-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000000000000000000000000000000000000..1fe8f39e9772c30d107e894e16766fa761a8e3ec GIT binary patch literal 76751 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%0D0I|L(+R-RT@?^qXE;AaX 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 S2y_hi`3SUP69aI02>KsA+VKkj literal 0 HcmV?d00001 From 1777628c54b2e5609e1b9b5b9e1aae83b8ab69f6 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 8 Jan 2021 10:00:09 -0600 Subject: [PATCH 02/49] update datasource v2 test --- .../src/main/python/datasourcev2_read.py | 82 ++++++++----------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index 681217740c0..055d8ee60b3 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -14,12 +14,9 @@ import pytest -from asserts import assert_gpu_and_cpu_are_equal_collect, assert_gpu_and_cpu_writes_are_equal_collect, assert_gpu_fallback_collect -from datetime import date, datetime, timezone -from data_gen import * -from marks import * +from asserts import assert_gpu_and_cpu_are_equal_collect, from pyspark.sql.types import * -from spark_session import with_cpu_session, with_gpu_session +from spark_session import with_cpu_session def read_parquet_df(data_path): return lambda spark : spark.read.parquet(data_path) @@ -27,45 +24,38 @@ def read_parquet_df(data_path): def read_parquet_sql(data_path): return lambda spark : spark.sql('select * from parquet.`{}`'.format(data_path)) - -# Override decimal_gens because decimal with negative scale is unsupported in parquet reading -decimal_gens = [DecimalGen(), DecimalGen(precision=7, scale=3), DecimalGen(precision=10, scale=10), - DecimalGen(precision=9, scale=0), DecimalGen(precision=18, scale=15)] - -parquet_gens_list = [[byte_gen, short_gen, int_gen, long_gen, float_gen, double_gen, - string_gen, boolean_gen, date_gen, - TimestampGen(start=datetime(1900, 1, 1, tzinfo=timezone.utc)), ArrayGen(byte_gen), - ArrayGen(long_gen), ArrayGen(string_gen), ArrayGen(date_gen), - ArrayGen(TimestampGen(start=datetime(1900, 1, 1, tzinfo=timezone.utc))), - ArrayGen(DecimalGen()), - ArrayGen(ArrayGen(byte_gen)), - StructGen([['child0', ArrayGen(byte_gen)], ['child1', byte_gen], ['child2', float_gen], ['child3', DecimalGen()]]), - ArrayGen(StructGen([['child0', string_gen], ['child1', double_gen], ['child2', int_gen]]))] + - map_gens_sample + decimal_gens, - pytest.param([timestamp_gen], marks=pytest.mark.xfail(reason='https://github.com/NVIDIA/spark-rapids/issues/132'))] - -# test with original parquet file reader, the multi-file parallel reader for cloud, and coalesce file reader for -# non-cloud -original_parquet_file_reader_conf = {'spark.rapids.sql.format.parquet.reader.type': 'PERFILE'} -multithreaded_parquet_file_reader_conf = {'spark.rapids.sql.format.parquet.reader.type': 'MULTITHREADED'} -coalesce_parquet_file_reader_conf = {'spark.rapids.sql.format.parquet.reader.type': 'COALESCING'} -reader_opt_confs = [original_parquet_file_reader_conf, multithreaded_parquet_file_reader_conf, - coalesce_parquet_file_reader_conf] - -@pytest.mark.parametrize('parquet_gens', parquet_gens_list, ids=idfn) -@pytest.mark.parametrize('read_func', [read_parquet_df, read_parquet_sql]) -@pytest.mark.parametrize('reader_confs', reader_opt_confs) -@pytest.mark.parametrize('v1_enabled_list', ["", "parquet"]) -def test_read_round_trip(spark_tmp_path, parquet_gens, read_func, reader_confs, v1_enabled_list): - gen_list = [('_c' + str(i), gen) for i, gen in enumerate(parquet_gens)] - data_path = spark_tmp_path + '/PARQUET_DATA' - with_cpu_session( - lambda spark : gen_df(spark, gen_list).write.parquet(data_path), - conf={'spark.sql.legacy.parquet.datetimeRebaseModeInWrite': 'CORRECTED'}) - all_confs = reader_confs.copy() - all_confs.update({'spark.sql.sources.useV1SourceList': v1_enabled_list, 'spark.sql.legacy.parquet.datetimeRebaseModeInRead': 'CORRECTED'}) - # once https://github.com/NVIDIA/spark-rapids/issues/1126 is in we can remove spark.sql.legacy.parquet.datetimeRebaseModeInRead config which is a workaround - # for nested timestamp/date support - assert_gpu_and_cpu_are_equal_collect(read_func(data_path), - conf=all_confs) +def createPeopleCSVDf(spark): + peopleCSVLocation = "people.csv" + return spark.read.format("csv")\ + .option("header", "false")\ + .option("inferSchema", "true")\ + .load(peopleCSVLocation)\ + .withColumnRenamed("_c0", "name")\ + .withColumnRenamed("_c1", "age")\ + .withColumnRenamed("_c2", "job") + + +catalogName = "columnar" +tableName = "people" +columnarTableName = catalogName + "." + tableName + +def setupInMemoryTableWithPartitioning(spark): + spark.sql("create database IF NOT EXISTS " + catalogName) + peopleCSVDf = createPeopleCSVDf(spark) + peopleCSVDf.createOrReplaceTempView("people_csv") + spark.table("people_csv").write.partitionBy("job").saveAsTable(columnarTableName) + +def setupInMemoryTableNoPartitioning(spark): + spark.sql("create database IF NOT EXISTS " + catalogName) + peopleCSVDf = createPeopleCSVDf(spark) + peopleCSVDf.createOrReplaceTempView("people_csv") + spark.table("people_csv").write.saveAsTable(columnarTableName) + +def readTable(spark): + spark.table(columnarTableName)\ + .orderBy("name", "age") + +def test_read_round_trip_partitioned(spark_tmp_path): + with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark)) + assert_gpu_and_cpu_are_equal_collect(readTable) From dbbaf309515c81482f1dcac1bf08b6138e16202c Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 8 Jan 2021 10:06:51 -0600 Subject: [PATCH 03/49] fix up test issues --- .../src/main/python/datasourcev2_read.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index 055d8ee60b3..36cd1132b89 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -14,7 +14,7 @@ import pytest -from asserts import assert_gpu_and_cpu_are_equal_collect, +from asserts import assert_gpu_and_cpu_are_equal_collect from pyspark.sql.types import * from spark_session import with_cpu_session @@ -24,8 +24,7 @@ def read_parquet_df(data_path): def read_parquet_sql(data_path): return lambda spark : spark.sql('select * from parquet.`{}`'.format(data_path)) -def createPeopleCSVDf(spark): - peopleCSVLocation = "people.csv" +def createPeopleCSVDf(spark, peopleCSVLocation): return spark.read.format("csv")\ .option("header", "false")\ .option("inferSchema", "true")\ @@ -39,15 +38,15 @@ def createPeopleCSVDf(spark): tableName = "people" columnarTableName = catalogName + "." + tableName -def setupInMemoryTableWithPartitioning(spark): +def setupInMemoryTableWithPartitioning(spark, csv): spark.sql("create database IF NOT EXISTS " + catalogName) - peopleCSVDf = createPeopleCSVDf(spark) + peopleCSVDf = createPeopleCSVDf(spark, csv) peopleCSVDf.createOrReplaceTempView("people_csv") spark.table("people_csv").write.partitionBy("job").saveAsTable(columnarTableName) -def setupInMemoryTableNoPartitioning(spark): +def setupInMemoryTableNoPartitioning(spark, csv): spark.sql("create database IF NOT EXISTS " + catalogName) - peopleCSVDf = createPeopleCSVDf(spark) + peopleCSVDf = createPeopleCSVDf(spark, csv) peopleCSVDf.createOrReplaceTempView("people_csv") spark.table("people_csv").write.saveAsTable(columnarTableName) @@ -55,7 +54,8 @@ def readTable(spark): spark.table(columnarTableName)\ .orderBy("name", "age") -def test_read_round_trip_partitioned(spark_tmp_path): - with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark)) +@pytest.mark.parametrize('csv', ['people.csv']) +def test_read_round_trip_partitioned(std_input_path, csv): + with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark, std_input_path + csv)) assert_gpu_and_cpu_are_equal_collect(readTable) From 927abfe701a388613274aec273c60b322677ec54 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 8 Jan 2021 10:07:32 -0600 Subject: [PATCH 04/49] add no partitioned test --- integration_tests/src/main/python/datasourcev2_read.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index 36cd1132b89..5efa1d73b64 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -59,3 +59,7 @@ def test_read_round_trip_partitioned(std_input_path, csv): with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark, std_input_path + csv)) assert_gpu_and_cpu_are_equal_collect(readTable) +@pytest.mark.parametrize('csv', ['people.csv']) +def test_read_round_trip_no_partitioned(std_input_path, csv): + with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, std_input_path + csv)) + assert_gpu_and_cpu_are_equal_collect(readTable) \ No newline at end of file From 39dc434c95458069deeab918c6a2d209b9c303ee Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 8 Jan 2021 11:15:15 -0600 Subject: [PATCH 05/49] Fix up test to properly work with in memory table datasource --- .../src/main/python/datasourcev2_read.py | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index 5efa1d73b64..fbc85ef80e8 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -36,30 +36,54 @@ def createPeopleCSVDf(spark, peopleCSVLocation): catalogName = "columnar" tableName = "people" +tableNameNoPart = "peoplenopart" columnarTableName = catalogName + "." + tableName +columnarTableNameNoPart = catalogName + "." + tableNameNoPart +columnarClass = 'org.apache.spark.sql.connector.InMemoryTableCatalog' def setupInMemoryTableWithPartitioning(spark, csv): - spark.sql("create database IF NOT EXISTS " + catalogName) peopleCSVDf = createPeopleCSVDf(spark, csv) peopleCSVDf.createOrReplaceTempView("people_csv") spark.table("people_csv").write.partitionBy("job").saveAsTable(columnarTableName) def setupInMemoryTableNoPartitioning(spark, csv): - spark.sql("create database IF NOT EXISTS " + catalogName) peopleCSVDf = createPeopleCSVDf(spark, csv) peopleCSVDf.createOrReplaceTempView("people_csv") - spark.table("people_csv").write.saveAsTable(columnarTableName) + spark.table("people_csv").write.saveAsTable(columnarTableNameNoPart) -def readTable(spark): - spark.table(columnarTableName)\ +def readTable(csvPath, tableToRead): + return lambda spark: spark.table(tableToRead)\ .orderBy("name", "age") +def createDatabase(spark): + spark.sql("create database IF NOT EXISTS " + catalogName) + spark.sql("use " + catalogName) + +def cleanupDatabase(spark): + spark.sql("drop table IF EXISTS " + tableName) + spark.sql("drop table IF EXISTS " + tableNameNoPart) + spark.sql("drop database IF EXISTS " + catalogName) + +@pytest.fixture(autouse=True) +def setupAndCleanUp(): + with_cpu_session(lambda spark : createDatabase(spark), + conf={'spark.sql.catalog.columnar': columnarClass}) + yield + with_cpu_session(lambda spark : cleanupDatabase(spark), + conf={'spark.sql.catalog.columnar': columnarClass}) + @pytest.mark.parametrize('csv', ['people.csv']) def test_read_round_trip_partitioned(std_input_path, csv): - with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark, std_input_path + csv)) - assert_gpu_and_cpu_are_equal_collect(readTable) + csvPath = std_input_path + "/" + csv + with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark, csvPath), + conf={'spark.sql.catalog.columnar': columnarClass}) + assert_gpu_and_cpu_are_equal_collect(readTable(csvPath, columnarTableName), + conf={'spark.sql.catalog.columnar': columnarClass}) @pytest.mark.parametrize('csv', ['people.csv']) def test_read_round_trip_no_partitioned(std_input_path, csv): - with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, std_input_path + csv)) - assert_gpu_and_cpu_are_equal_collect(readTable) \ No newline at end of file + csvPath = std_input_path + "/" + csv + with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, csvPath), + conf={'spark.sql.catalog.columnar': columnarClass}) + assert_gpu_and_cpu_are_equal_collect(readTable(csvPath, columnarTableNameNoPart), + conf={'spark.sql.catalog.columnar': columnarClass}) From 78a54ff13b9c021bf6168dede24294ef5bc15ec0 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 13 Jan 2021 15:13:28 -0600 Subject: [PATCH 06/49] logs in hostcolumnar --- .../scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 295655972f6..1fca3d8f1c3 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -17,17 +17,21 @@ package com.nvidia.spark.rapids import org.apache.spark.TaskContext +import org.apache.spark.internal.Logging import org.apache.spark.rdd.RDD import org.apache.spark.sql.catalyst.InternalRow import org.apache.spark.sql.catalyst.expressions.Attribute import org.apache.spark.sql.execution.{SparkPlan, UnaryExecNode} import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics} import org.apache.spark.sql.types._ -import org.apache.spark.sql.vectorized.{ColumnarBatch, ColumnVector} +import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, ColumnVector} -object HostColumnarToGpu { +object HostColumnarToGpu extends Logging { def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, nullable: Boolean, rows: Int): Unit = { + if (cv.isInstanceOf[ArrowColumnVector]) { + logWarning("looking at arrow column vector") + } (cv.dataType(), nullable) match { case (ByteType | BooleanType, true) => for (i <- 0 until rows) { From 19d9685985e5be5de88715e9482d92e3cd087757 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 15 Jan 2021 10:40:29 -0600 Subject: [PATCH 07/49] Add test code and AccessibleArrowcolumnVector --- .../spark/rapids/HostColumnarToGpu.scala | 20 + .../rapids/AccessibleArrowColumnVector.java | 506 ++++++++++++++++++ 2 files changed, 526 insertions(+) create mode 100644 sql-plugin/src/main/scala/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 1fca3d8f1c3..c0ec8187ef7 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -16,6 +16,8 @@ package com.nvidia.spark.rapids +import ai.rapids.cudf._ + import org.apache.spark.TaskContext import org.apache.spark.internal.Logging import org.apache.spark.rdd.RDD @@ -31,6 +33,24 @@ object HostColumnarToGpu extends Logging { nullable: Boolean, rows: Int): Unit = { if (cv.isInstanceOf[ArrowColumnVector]) { logWarning("looking at arrow column vector") + // TODO - how make sure off heap? + // could create HostMemoryBuffer(addr, length)' + val arrowVec = cv.asInstanceOf[ArrowColumnVector] + // TODO - accessor is private to ArrowColumnVector!!! + // ValueVector => ArrowBuf + + + val arrowDataAddr = arrowVec.accessor.vector.getDataBuffer.memoryAddress() + val arrowDataValidity = arrowVec.accessor.vector.getValidityBuffer.memoryAddress() + val arrowDataOffsetBuf = arrowVec.accessor.vector.getOffsetBuffer.memoryAddress() + + // ArrowBuf length instead? = capacity() + val arrowDataLen = arrowVec.accessor.vector.getBufferSize() // ? + + // need multiple for validity and offset??? + val hmb = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) + + } (cv.dataType(), nullable) match { case (ByteType | BooleanType, true) => diff --git a/sql-plugin/src/main/scala/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java b/sql-plugin/src/main/scala/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java new file mode 100644 index 00000000000..43c968a8440 --- /dev/null +++ b/sql-plugin/src/main/scala/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.spark.sql.vectorized.rapids; + +import org.apache.arrow.vector.*; +import org.apache.arrow.vector.complex.*; +import org.apache.arrow.vector.holders.NullableVarCharHolder; + +import org.apache.spark.sql.util.ArrowUtils; +import org.apache.spark.sql.types.*; +import org.apache.spark.unsafe.types.UTF8String; + +/** + * A column vector backed by Apache Arrow. Currently calendar interval type and map type are not + * supported. + */ +public final class AccessibleArrowColumnVector extends ColumnVector { + + private final AccessibleArrowVectorAccessor accessor; + private AccessibleArrowColumnVector[] childColumns; + + @Override + public boolean hasNull() { + return accessor.getNullCount() > 0; + } + + @Override + public int numNulls() { + return accessor.getNullCount(); + } + + @Override + public void close() { + if (childColumns != null) { + for (int i = 0; i < childColumns.length; i++) { + childColumns[i].close(); + childColumns[i] = null; + } + childColumns = null; + } + accessor.close(); + } + + @Override + public boolean isNullAt(int rowId) { + return accessor.isNullAt(rowId); + } + + @Override + public boolean getBoolean(int rowId) { + return accessor.getBoolean(rowId); + } + + @Override + public byte getByte(int rowId) { + return accessor.getByte(rowId); + } + + @Override + public short getShort(int rowId) { + return accessor.getShort(rowId); + } + + @Override + public int getInt(int rowId) { + return accessor.getInt(rowId); + } + + @Override + public long getLong(int rowId) { + return accessor.getLong(rowId); + } + + @Override + public float getFloat(int rowId) { + return accessor.getFloat(rowId); + } + + @Override + public double getDouble(int rowId) { + return accessor.getDouble(rowId); + } + + @Override + public Decimal getDecimal(int rowId, int precision, int scale) { + if (isNullAt(rowId)) return null; + return accessor.getDecimal(rowId, precision, scale); + } + + @Override + public UTF8String getUTF8String(int rowId) { + if (isNullAt(rowId)) return null; + return accessor.getUTF8String(rowId); + } + + @Override + public byte[] getBinary(int rowId) { + if (isNullAt(rowId)) return null; + return accessor.getBinary(rowId); + } + + @Override + public ColumnarArray getArray(int rowId) { + if (isNullAt(rowId)) return null; + return accessor.getArray(rowId); + } + + @Override + public ColumnarMap getMap(int rowId) { + if (isNullAt(rowId)) return null; + return accessor.getMap(rowId); + } + + @Override + public AccessibleArrowColumnVector getChild(int ordinal) { return childColumns[ordinal]; } + + public AccessibleArrowColumnVector(ValueVector vector) { + super(ArrowUtils.fromArrowField(vector.getField())); + + if (vector instanceof BitVector) { + accessor = new AccessibleBooleanAccessor((BitVector) vector); + } else if (vector instanceof TinyIntVector) { + accessor = new AccessibleByteAccessor((TinyIntVector) vector); + } else if (vector instanceof SmallIntVector) { + accessor = new AccessibleShortAccessor((SmallIntVector) vector); + } else if (vector instanceof IntVector) { + accessor = new AccessibleIntAccessor((IntVector) vector); + } else if (vector instanceof BigIntVector) { + accessor = new AccessibleLongAccessor((BigIntVector) vector); + } else if (vector instanceof Float4Vector) { + accessor = new AccessibleFloatAccessor((Float4Vector) vector); + } else if (vector instanceof Float8Vector) { + accessor = new AccessibleDoubleAccessor((Float8Vector) vector); + } else if (vector instanceof DecimalVector) { + accessor = new AccessibleDecimalAccessor((DecimalVector) vector); + } else if (vector instanceof VarCharVector) { + accessor = new AccessibleStringAccessor((VarCharVector) vector); + } else if (vector instanceof VarBinaryVector) { + accessor = new AccessibleBinaryAccessor((VarBinaryVector) vector); + } else if (vector instanceof DateDayVector) { + accessor = new AccessibleDateAccessor((DateDayVector) vector); + } else if (vector instanceof TimeStampMicroTZVector) { + accessor = new AccessibleTimestampAccessor((TimeStampMicroTZVector) vector); + } else if (vector instanceof MapVector) { + MapVector mapVector = (MapVector) vector; + accessor = new AccessibleMapAccessor(mapVector); + } else if (vector instanceof ListVector) { + ListVector listVector = (ListVector) vector; + accessor = new AccessibleArrayAccessor(listVector); + } else if (vector instanceof StructVector) { + StructVector structVector = (StructVector) vector; + accessor = new AccessibleStructAccessor(structVector); + + childColumns = new AccessibleArrowColumnVector[structVector.size()]; + for (int i = 0; i < childColumns.length; ++i) { + childColumns[i] = new AccessibleArrowColumnVector(structVector.getVectorById(i)); + } + } else { + throw new UnsupportedOperationException(); + } + } + + private abstract static class AccessibleArrowVectorAccessor { + + private final ValueVector vector; + + AccessibleArrowVectorAccessor(ValueVector vector) { + this.vector = vector; + } + + ValueVector getVector() { + return vector + } + + // TODO: should be final after removing ArrayAccessor workaround + boolean isNullAt(int rowId) { + return vector.isNull(rowId); + } + + final int getNullCount() { + return vector.getNullCount(); + } + + final void close() { + vector.close(); + } + + boolean getBoolean(int rowId) { + throw new UnsupportedOperationException(); + } + + byte getByte(int rowId) { + throw new UnsupportedOperationException(); + } + + short getShort(int rowId) { + throw new UnsupportedOperationException(); + } + + int getInt(int rowId) { + throw new UnsupportedOperationException(); + } + + long getLong(int rowId) { + throw new UnsupportedOperationException(); + } + + float getFloat(int rowId) { + throw new UnsupportedOperationException(); + } + + double getDouble(int rowId) { + throw new UnsupportedOperationException(); + } + + Decimal getDecimal(int rowId, int precision, int scale) { + throw new UnsupportedOperationException(); + } + + UTF8String getUTF8String(int rowId) { + throw new UnsupportedOperationException(); + } + + byte[] getBinary(int rowId) { + throw new UnsupportedOperationException(); + } + + ColumnarArray getArray(int rowId) { + throw new UnsupportedOperationException(); + } + + ColumnarMap getMap(int rowId) { + throw new UnsupportedOperationException(); + } + } + + private static class AccessibleBooleanAccessor extends AccessibleArrowVectorAccessor { + + private final BitVector accessor; + + AccessibleBooleanAccessor(BitVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final boolean getBoolean(int rowId) { + return accessor.get(rowId) == 1; + } + } + + private static class AccessibleByteAccessor extends AccessibleArrowVectorAccessor { + + private final TinyIntVector accessor; + + AccessibleByteAccessor(TinyIntVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final byte getByte(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleShortAccessor extends AccessibleArrowVectorAccessor { + + private final SmallIntVector accessor; + + AccessibleShortAccessor(SmallIntVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final short getShort(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleIntAccessor extends AccessibleArrowVectorAccessor { + + private final IntVector accessor; + + AccessibleIntAccessor(IntVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final int getInt(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleLongAccessor extends AccessibleArrowVectorAccessor { + + private final BigIntVector accessor; + + AccessibleLongAccessor(BigIntVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final long getLong(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleFloatAccessor extends AccessibleArrowVectorAccessor { + + private final Float4Vector accessor; + + AccessibleFloatAccessor(Float4Vector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final float getFloat(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleDoubleAccessor extends AccessibleArrowVectorAccessor { + + private final Float8Vector accessor; + + AccessibleDoubleAccessor(Float8Vector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final double getDouble(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleDecimalAccessor extends AccessibleArrowVectorAccessor { + + private final DecimalVector accessor; + + AccessibleDecimalAccessor(DecimalVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final Decimal getDecimal(int rowId, int precision, int scale) { + if (isNullAt(rowId)) return null; + return Decimal.apply(accessor.getObject(rowId), precision, scale); + } + } + + private static class AccessibleStringAccessor extends AccessibleArrowVectorAccessor { + + private final VarCharVector accessor; + private final NullableVarCharHolder stringResult = new NullableVarCharHolder(); + + AccessibleStringAccessor(VarCharVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final UTF8String getUTF8String(int rowId) { + accessor.get(rowId, stringResult); + if (stringResult.isSet == 0) { + return null; + } else { + return UTF8String.fromAddress(null, + stringResult.buffer.memoryAddress() + stringResult.start, + stringResult.end - stringResult.start); + } + } + } + + private static class AccessibleBinaryAccessor extends AccessibleArrowVectorAccessor { + + private final VarBinaryVector accessor; + + AccessibleBinaryAccessor(VarBinaryVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final byte[] getBinary(int rowId) { + return accessor.getObject(rowId); + } + } + + private static class AccessibleDateAccessor extends AccessibleArrowVectorAccessor { + + private final DateDayVector accessor; + + AccessibleDateAccessor(DateDayVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final int getInt(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleTimestampAccessor extends AccessibleArrowVectorAccessor { + + private final TimeStampMicroTZVector accessor; + + AccessibleTimestampAccessor(TimeStampMicroTZVector vector) { + super(vector); + this.accessor = vector; + } + + @Override + final long getLong(int rowId) { + return accessor.get(rowId); + } + } + + private static class AccessibleArrayAccessor extends AccessibleArrowVectorAccessor { + + private final ListVector accessor; + private final AccessibleArrowColumnVector arrayData; + + AccessibleArrayAccessor(ListVector vector) { + super(vector); + this.accessor = vector; + this.arrayData = new AccessibleArrowColumnVector(vector.getDataVector()); + } + + @Override + final boolean isNullAt(int rowId) { + // TODO: Workaround if vector has all non-null values, see ARROW-1948 + if (accessor.getValueCount() > 0 && accessor.getValidityBuffer().capacity() == 0) { + return false; + } else { + return super.isNullAt(rowId); + } + } + + @Override + final ColumnarArray getArray(int rowId) { + int start = accessor.getElementStartIndex(rowId); + int end = accessor.getElementEndIndex(rowId); + return new ColumnarArray(arrayData, start, end - start); + } + } + + /** + * Any call to "get" method will throw UnsupportedOperationException. + * + * Access struct values in a AccessibleArrowColumnVector doesn't use this accessor. Instead, it uses + * getStruct() method defined in the parent class. Any call to "get" method in this class is a + * bug in the code. + * + */ + private static class AccessibleStructAccessor extends AccessibleArrowVectorAccessor { + + AccessibleStructAccessor(StructVector vector) { + super(vector); + } + } + + private static class AccessibleMapAccessor extends AccessibleArrowVectorAccessor { + private final MapVector accessor; + private final AccessibleArrowColumnVector keys; + private final AccessibleArrowColumnVector values; + + AccessibleMapAccessor(MapVector vector) { + super(vector); + this.accessor = vector; + StructVector entries = (StructVector) vector.getDataVector(); + this.keys = new AccessibleArrowColumnVector(entries.getChild(MapVector.KEY_NAME)); + this.values = new AccessibleArrowColumnVector(entries.getChild(MapVector.VALUE_NAME)); + } + + @Override + final ColumnarMap getMap(int rowId) { + int index = rowId * MapVector.OFFSET_WIDTH; + int offset = accessor.getOffsetBuffer().getInt(index); + int length = accessor.getInnerValueCountAt(rowId); + return new ColumnarMap(keys, values, offset, length); + } + } +} From e012476847451bf3a88e7a3731a6aca6f3cade46 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 15 Jan 2021 10:48:23 -0600 Subject: [PATCH 08/49] Fix accesible retrieval --- .../rapids/AccessibleArrowColumnVector.java | 9 ++++++++- .../spark/rapids/HostColumnarToGpu.scala | 20 +++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) rename sql-plugin/src/main/{scala => java}/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java (98%) diff --git a/sql-plugin/src/main/scala/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java similarity index 98% rename from sql-plugin/src/main/scala/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java rename to sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java index 43c968a8440..6ddfe930a31 100644 --- a/sql-plugin/src/main/scala/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java +++ b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java @@ -23,6 +23,9 @@ import org.apache.spark.sql.util.ArrowUtils; import org.apache.spark.sql.types.*; +import org.apache.spark.sql.vectorized.ColumnarArray; +import org.apache.spark.sql.vectorized.ColumnarMap; +import org.apache.spark.sql.vectorized.ColumnVector; import org.apache.spark.unsafe.types.UTF8String; /** @@ -34,6 +37,10 @@ public final class AccessibleArrowColumnVector extends ColumnVector { private final AccessibleArrowVectorAccessor accessor; private AccessibleArrowColumnVector[] childColumns; + public ValueVector getArrowValueVector() { + return accessor.vector; + } + @Override public boolean hasNull() { return accessor.getNullCount() > 0; @@ -184,7 +191,7 @@ private abstract static class AccessibleArrowVectorAccessor { } ValueVector getVector() { - return vector + return vector; } // TODO: should be final after removing ArrayAccessor workaround diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index c0ec8187ef7..672f3555264 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -27,25 +27,33 @@ import org.apache.spark.sql.execution.{SparkPlan, UnaryExecNode} import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics} import org.apache.spark.sql.types._ import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, ColumnVector} +import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, nullable: Boolean, rows: Int): Unit = { - if (cv.isInstanceOf[ArrowColumnVector]) { + if (cv.isInstanceOf[AccessibleArrowColumnVector]) { logWarning("looking at arrow column vector") // TODO - how make sure off heap? // could create HostMemoryBuffer(addr, length)' - val arrowVec = cv.asInstanceOf[ArrowColumnVector] + val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] // TODO - accessor is private to ArrowColumnVector!!! // ValueVector => ArrowBuf + val buffers = arrowVec.getArrowValueVector.getBuffers(false) + logWarning("buffer num is " + buffers.size) - val arrowDataAddr = arrowVec.accessor.vector.getDataBuffer.memoryAddress() - val arrowDataValidity = arrowVec.accessor.vector.getValidityBuffer.memoryAddress() - val arrowDataOffsetBuf = arrowVec.accessor.vector.getOffsetBuffer.memoryAddress() + val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.memoryAddress() + val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() + val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer.memoryAddress() // ArrowBuf length instead? = capacity() - val arrowDataLen = arrowVec.accessor.vector.getBufferSize() // ? + val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? + val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? + val arrowDataOffsetLen = arrowVec.getArrowValueVector.getBufferSize() // ? + + logWarning(s"lens data: ${arrowDataLen} validity: $arrowDataValidityLen " + + s"offset: $arrowDataOffsetLen") // need multiple for validity and offset??? val hmb = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) From 29f3d25c62b84aed05fa7ca40c16228a2fa561d9 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 15 Jan 2021 12:10:39 -0600 Subject: [PATCH 09/49] working --- .../src/main/python/datasourcev2_read.py | 7 ------- .../com/nvidia/spark/rapids/HostColumnarToGpu.scala | 11 ++++++----- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index fbc85ef80e8..cf92bf0b1c9 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -18,12 +18,6 @@ from pyspark.sql.types import * from spark_session import with_cpu_session -def read_parquet_df(data_path): - return lambda spark : spark.read.parquet(data_path) - -def read_parquet_sql(data_path): - return lambda spark : spark.sql('select * from parquet.`{}`'.format(data_path)) - def createPeopleCSVDf(spark, peopleCSVLocation): return spark.read.format("csv")\ .option("header", "false")\ @@ -33,7 +27,6 @@ def createPeopleCSVDf(spark, peopleCSVLocation): .withColumnRenamed("_c1", "age")\ .withColumnRenamed("_c2", "job") - catalogName = "columnar" tableName = "people" tableNameNoPart = "peoplenopart" diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 672f3555264..f07983f2134 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -32,6 +32,7 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, nullable: Boolean, rows: Int): Unit = { + logWarning("host oclunnar to gpu cv is type: " + cv.getClass().toString()) if (cv.isInstanceOf[AccessibleArrowColumnVector]) { logWarning("looking at arrow column vector") // TODO - how make sure off heap? @@ -45,18 +46,18 @@ object HostColumnarToGpu extends Logging { val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.memoryAddress() val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() - val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer.memoryAddress() + // val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer.memoryAddress() // ArrowBuf length instead? = capacity() val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? - val arrowDataOffsetLen = arrowVec.getArrowValueVector.getBufferSize() // ? + // val arrowDataOffsetLen = arrowVec.getArrowValueVector.getBufferSize() // ? - logWarning(s"lens data: ${arrowDataLen} validity: $arrowDataValidityLen " + - s"offset: $arrowDataOffsetLen") + logWarning(s"lens data: ${arrowDataLen} validity: $arrowDataValidityLen ") + // s"offset: $arrowDataOffsetLen") // need multiple for validity and offset??? - val hmb = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) + // val hmb = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) } From a983d338597781814403b6f500f0b0c5c72aa4a3 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 20 Jan 2021 08:22:56 -0600 Subject: [PATCH 10/49] more debug --- .../spark/rapids/HostColumnarToGpu.scala | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index f07983f2134..294fd1db21a 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -18,6 +18,9 @@ package com.nvidia.spark.rapids import ai.rapids.cudf._ +import org.apache.arrow.vector.ValueVector +import org.apache.arrow.vector.types.pojo.ArrowType + import org.apache.spark.TaskContext import org.apache.spark.internal.Logging import org.apache.spark.rdd.RDD @@ -30,6 +33,7 @@ import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, Column import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { + def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, nullable: Boolean, rows: Int): Unit = { logWarning("host oclunnar to gpu cv is type: " + cv.getClass().toString()) @@ -46,7 +50,21 @@ object HostColumnarToGpu extends Logging { val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.memoryAddress() val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() - // val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer.memoryAddress() + try { + val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer + if (arrowDataOffsetBuf != null) { + logWarning("arrow data offset buffer addrs: " + arrowDataOffsetBuf.memoryAddress()) + } else { + logWarning("arrow data offset buffer is null") + } + if (arrowDataOffsetBuf != null) { + val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.capacity()// ? + logWarning("arrow data offset buffer capcity is: " + arrowDataOffsetLen) + } + } catch { + case e: UnsupportedOperationException => + logWarning("unsupported op getOffsetBuffer") + } // ArrowBuf length instead? = capacity() val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? From 14844f2d608100fcaad91e8741c252ac727d958e Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 20 Jan 2021 13:27:53 -0600 Subject: [PATCH 11/49] first go --- .../nvidia/spark/rapids/GpuColumnVector.java | 103 ++++++++++++++++++ .../spark/rapids/GpuCoalesceBatches.scala | 6 +- .../spark/rapids/HostColumnarToGpu.scala | 56 ++++++++-- 3 files changed, 150 insertions(+), 15 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index e843a7b668e..3d31b8a3c3c 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -23,6 +23,9 @@ import ai.rapids.cudf.Schema; import ai.rapids.cudf.Table; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.spark.sql.catalyst.expressions.Attribute; import org.apache.spark.sql.types.*; import org.apache.spark.sql.vectorized.ColumnVector; @@ -118,6 +121,105 @@ private static HostColumnVector.DataType convertFrom(DataType spark, boolean nul } } + public static final class GpuArrowColumnarBatchBuilder implements AutoCloseable { + private static final Logger logger = LoggerFactory.getLogger(GpuArrowColumnarBatchBuilder.class); + private final ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder[] builders; + private final StructField[] fields; + + /** + * A collection of builders for building up columnar data. + * @param schema the schema of the batch. + * @param rows the maximum number of rows in this batch. + * @param batch if this is going to copy a ColumnarBatch in a non GPU format that batch + * we are going to copy. If not this may be null. This is used to get an idea + * of how big to allocate buffers that do not necessarily correspond to the + * number of rows. + */ + public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch batch) { + fields = schema.fields(); + int len = fields.length; + builders = new ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder[len]; + boolean success = false; + try { + logger.warn("schema contains: " + schema.toString()); + for (int i = 0; i < len; i++) { + StructField field = fields[i]; + logger.warn("field datatype: " + field.dataType() + " converted to: " + convertFrom(field.dataType(), field.nullable())); + + builders[i] = new ArrowHostColumnVector.ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), rows); + } + success = true; + } finally { + if (!success) { + for (ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder b: builders) { + if (b != null) { + b.close(); + } + } + } + } + } + + public ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder builder(int i) { + return builders[i]; + } + + public ColumnarBatch build(int rows) { + ColumnVector[] vectors = new ColumnVector[builders.length]; + boolean success = false; + try { + for (int i = 0; i < builders.length; i++) { + /* + ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); + vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); + builders[i] = null; + */ + } + ColumnarBatch ret = new ColumnarBatch(vectors, rows); + success = true; + return ret; + } finally { + if (!success) { + for (ColumnVector vec: vectors) { + if (vec != null) { + vec.close(); + } + } + } + } + } + + public HostColumnVector[] buildHostColumns() { + ArrowHostColumnVector[] vectors = new ArrowHostColumnVector[builders.length]; + try { + for (int i = 0; i < builders.length; i++) { + vectors[i] = builders[i].build(); + builders[i] = null; + } + ArrowHostColumnVector[] result = vectors; + vectors = null; + return result; + } finally { + if (vectors != null) { + for (ArrowHostColumnVector v : vectors) { + if (v != null) { + v.close(); + } + } + } + } + } + + @Override + public void close() { + for (ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder b: builders) { + if (b != null) { + b.close(); + } + } + } + } + public static final class GpuColumnarBatchBuilder implements AutoCloseable { private final ai.rapids.cudf.HostColumnVector.ColumnBuilder[] builders; private final StructField[] fields; @@ -211,6 +313,7 @@ public void close() { } } + private static DType toRapidsOrNull(DataType type) { if (type instanceof LongType) { return DType.INT64; diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala index 2cebd4e6d1d..6a4363898e8 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala @@ -199,7 +199,7 @@ abstract class AbstractGpuCoalesceIterator( /** * Called first to initialize any state needed for a new batch to be created. */ - def initNewBatch(): Unit + def initNewBatch(batch: ColumnarBatch): Unit /** * Called to add a new batch to the final output batch. The batch passed in will @@ -372,7 +372,7 @@ class GpuCoalesceIteratorNoSpill(iter: Iterator[ColumnarBatch], private[this] var codec: TableCompressionCodec = _ - override def initNewBatch(): Unit = { + override def initNewBatch(batch: ColumnarBatch): Unit = { batches.clear() compressedBatchIndices.clear() } @@ -485,7 +485,7 @@ class GpuCoalesceIterator(iter: Iterator[ColumnarBatch], private val batches: ArrayBuffer[SpillableColumnarBatch] = ArrayBuffer.empty private var maxDeviceMemory: Long = 0 - override def initNewBatch(): Unit = { + override def initNewBatch(batch: ColumnarBatch): Unit = { batches.safeClose() batches.clear() } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 294fd1db21a..c3c492a7c81 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -34,7 +34,7 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { - def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, + def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder, nullable: Boolean, rows: Int): Unit = { logWarning("host oclunnar to gpu cv is type: " + cv.getClass().toString()) if (cv.isInstanceOf[AccessibleArrowColumnVector]) { @@ -58,7 +58,7 @@ object HostColumnarToGpu extends Logging { logWarning("arrow data offset buffer is null") } if (arrowDataOffsetBuf != null) { - val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.capacity()// ? + val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.capacity() // ? logWarning("arrow data offset buffer capcity is: " + arrowDataOffsetLen) } } catch { @@ -72,13 +72,20 @@ object HostColumnarToGpu extends Logging { // val arrowDataOffsetLen = arrowVec.getArrowValueVector.getBufferSize() // ? logWarning(s"lens data: ${arrowDataLen} validity: $arrowDataValidityLen ") - // s"offset: $arrowDataOffsetLen") + // s"offset: $arrowDataOffsetLen") // need multiple for validity and offset??? // val hmb = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) + } else { + throw new Exception("not arrow data shouldn't be here!") } + } + + def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, + nullable: Boolean, rows: Int): Unit = { + (cv.dataType(), nullable) match { case (ByteType | BooleanType, true) => for (i <- 0 until rows) { @@ -226,6 +233,8 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], assert(goal != RequireSingleBatch) var batchBuilder: GpuColumnVector.GpuColumnarBatchBuilder = _ + var arrowBatchBuilder: GpuColumnVector.GpuArrowColumnarBatchBuilder = _ + var useArrow: Boolean = false var totalRows = 0 var maxDeviceMemory: Long = 0 @@ -233,25 +242,47 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], * Initialize the builders using an estimated row count based on the schema and the desired * batch size defined by [[RapidsConf.GPU_BATCH_SIZE_BYTES]]. */ - override def initNewBatch(): Unit = { + override def initNewBatch(batch: ColumnarBatch): Unit = { if (batchBuilder != null) { batchBuilder.close() batchBuilder = null } - // when reading host batches it is essential to read the data immediately and pass to a - // builder and we need to determine how many rows to allocate in the builder based on the - // schema and desired batch size - batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, - GpuBatchUtils.estimateGpuMemory(schema, 512), 512) - batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) + if (arrowBatchBuilder != null) { + arrowBatchBuilder.close() + arrowBatchBuilder = null + } + + if (batch.numCols() > 0) { + if (batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { + batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, + GpuBatchUtils.estimateGpuMemory(schema, 512), 512) + useArrow = true + arrowBatchBuilder = + new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, null) + } else { + // when reading host batches it is essential to read the data immediately and pass to a + // builder and we need to determine how many rows to allocate in the builder based on the + // schema and desired batch size + batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, + GpuBatchUtils.estimateGpuMemory(schema, 512), 512) + batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) + + } + } totalRows = 0 } override def addBatchToConcat(batch: ColumnarBatch): Unit = { val rows = batch.numRows() for (i <- 0 until batch.numCols()) { - HostColumnarToGpu.columnarCopy(batch.column(i), batchBuilder.builder(i), - schema.fields(i).nullable, rows) + if (useArrow) { + HostColumnarToGpu.columnarCopy(batch.column(i), arrowBatchBuilder.builder(i), + schema.fields(i).nullable, rows) + } else { + HostColumnarToGpu.columnarCopy(batch.column(i), batchBuilder.builder(i), + schema.fields(i).nullable, rows) + } + } totalRows += rows } @@ -351,6 +382,7 @@ case class HostColumnarToGpu(child: SparkPlan, goal: CoalesceGoal) val outputSchema = schema val batches = child.executeColumnar() + batches.mapPartitions { iter => new HostToGpuCoalesceIterator(iter, goal, outputSchema, numInputRows, numInputBatches, numOutputRows, numOutputBatches, collectTime, concatTime, From 6e9fdb95b87df97722accc965a02f9fc27a7202f Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 20 Jan 2021 13:43:38 -0600 Subject: [PATCH 12/49] building --- .../src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java | 3 ++- .../scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 3d31b8a3c3c..76710058be7 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -18,6 +18,7 @@ import ai.rapids.cudf.ColumnView; import ai.rapids.cudf.DType; +import ai.rapids.cudf.ArrowHostColumnVector; import ai.rapids.cudf.HostColumnVector; import ai.rapids.cudf.Scalar; import ai.rapids.cudf.Schema; @@ -189,7 +190,7 @@ public ColumnarBatch build(int rows) { } } - public HostColumnVector[] buildHostColumns() { + public ArrowHostColumnVector[] buildHostColumns() { ArrowHostColumnVector[] vectors = new ArrowHostColumnVector[builders.length]; try { for (int i = 0; i < builders.length; i++) { diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala index 6a4363898e8..20135a7c34c 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala @@ -331,7 +331,7 @@ abstract class AbstractGpuCoalesceIterator( private def addBatch(batch: ColumnarBatch): Unit = { if (!batchInitialized) { - initNewBatch() + initNewBatch(batch) batchInitialized = true } addBatchToConcat(batch) From d3db4ce145bc03364bcf83b6192e39925079b8a3 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 20 Jan 2021 16:44:05 -0600 Subject: [PATCH 13/49] more changes --- .../nvidia/spark/rapids/GpuColumnVector.java | 22 +++++++++++++++-- .../spark/rapids/HostColumnarToGpu.scala | 24 ++++++------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 76710058be7..87e8bf669a0 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -122,7 +122,17 @@ private static HostColumnVector.DataType convertFrom(DataType spark, boolean nul } } - public static final class GpuArrowColumnarBatchBuilder implements AutoCloseable { + // interface??? - then can't use autocloseable here + public abstract class GpuColumnarBatchBuilderBase implements AutoCloseable { + public abstract ColumnarBatch build(int rows); + + public abstract void close(); + + public abstract void copyColumnar(ColumnVector cv, int colNum, boolean nullable, int rows); + } + + + public static final class GpuArrowColumnarBatchBuilder implements GpuColumnarBatchBuilderBase { private static final Logger logger = LoggerFactory.getLogger(GpuArrowColumnarBatchBuilder.class); private final ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder[] builders; private final StructField[] fields; @@ -161,6 +171,10 @@ public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch b } } + public void copyColumnar(ColumnVector cv, int colNum, boolean nullable, int rows) { + HostColumnarToGpu.arrowColumnarCopy(cv, builder(colNum), nullable, rows); + } + public ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder builder(int i) { return builders[i]; } @@ -221,7 +235,7 @@ public void close() { } } - public static final class GpuColumnarBatchBuilder implements AutoCloseable { + public static final class GpuColumnarBatchBuilder implements GpuColumnarBatchBuilderBase { private final ai.rapids.cudf.HostColumnVector.ColumnBuilder[] builders; private final StructField[] fields; @@ -256,6 +270,10 @@ public GpuColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch batch) } } + public void copyColumnar(ColumnVector cv, int colNum, boolean nullable, int rows) { + HostColumnarToGpu.columnarCopy(cv, builder(colNum), nullable, rows); + } + public ai.rapids.cudf.HostColumnVector.ColumnBuilder builder(int i) { return builders[i]; } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index c3c492a7c81..4170985f9d3 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -34,8 +34,10 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { - def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder, - nullable: Boolean, rows: Int): Unit = { + def arrowColumnarCopy(cv: ColumnVector, + b: ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder, + nullable: Boolean, + rows: Int): Unit = { logWarning("host oclunnar to gpu cv is type: " + cv.getClass().toString()) if (cv.isInstanceOf[AccessibleArrowColumnVector]) { logWarning("looking at arrow column vector") @@ -232,8 +234,7 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], // RequireSingleBatch goal is intentionally not supported in this iterator assert(goal != RequireSingleBatch) - var batchBuilder: GpuColumnVector.GpuColumnarBatchBuilder = _ - var arrowBatchBuilder: GpuColumnVector.GpuArrowColumnarBatchBuilder = _ + var batchBuilder: GpuColumnVector.GpuColumnarBatchBuilderBase = _ var useArrow: Boolean = false var totalRows = 0 var maxDeviceMemory: Long = 0 @@ -247,17 +248,13 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], batchBuilder.close() batchBuilder = null } - if (arrowBatchBuilder != null) { - arrowBatchBuilder.close() - arrowBatchBuilder = null - } if (batch.numCols() > 0) { if (batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, GpuBatchUtils.estimateGpuMemory(schema, 512), 512) useArrow = true - arrowBatchBuilder = + batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, null) } else { // when reading host batches it is essential to read the data immediately and pass to a @@ -275,14 +272,7 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], override def addBatchToConcat(batch: ColumnarBatch): Unit = { val rows = batch.numRows() for (i <- 0 until batch.numCols()) { - if (useArrow) { - HostColumnarToGpu.columnarCopy(batch.column(i), arrowBatchBuilder.builder(i), - schema.fields(i).nullable, rows) - } else { - HostColumnarToGpu.columnarCopy(batch.column(i), batchBuilder.builder(i), - schema.fields(i).nullable, rows) - } - + batchBuilder.copyColumnar(batch.column(i), i, schema.fields(i).nullable, rows) } totalRows += rows } From fb006cb0017f3ddab30e40a98b1cd92d94c723e5 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 20 Jan 2021 16:48:15 -0600 Subject: [PATCH 14/49] fix static --- .../main/java/com/nvidia/spark/rapids/GpuColumnVector.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 87e8bf669a0..a1c3b4fef2e 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -123,7 +123,7 @@ private static HostColumnVector.DataType convertFrom(DataType spark, boolean nul } // interface??? - then can't use autocloseable here - public abstract class GpuColumnarBatchBuilderBase implements AutoCloseable { + public static abstract class GpuColumnarBatchBuilderBase implements AutoCloseable { public abstract ColumnarBatch build(int rows); public abstract void close(); @@ -132,7 +132,7 @@ public abstract class GpuColumnarBatchBuilderBase implements AutoCloseable { } - public static final class GpuArrowColumnarBatchBuilder implements GpuColumnarBatchBuilderBase { + public static final class GpuArrowColumnarBatchBuilder extends GpuColumnarBatchBuilderBase { private static final Logger logger = LoggerFactory.getLogger(GpuArrowColumnarBatchBuilder.class); private final ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder[] builders; private final StructField[] fields; @@ -235,7 +235,7 @@ public void close() { } } - public static final class GpuColumnarBatchBuilder implements GpuColumnarBatchBuilderBase { + public static final class GpuColumnarBatchBuilder extends GpuColumnarBatchBuilderBase { private final ai.rapids.cudf.HostColumnVector.ColumnBuilder[] builders; private final StructField[] fields; From eb1dd1400192bbdde288ff7041a09b402568956c Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 21 Jan 2021 08:33:08 -0600 Subject: [PATCH 15/49] more changes --- .../spark/rapids/HostColumnarToGpu.scala | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 4170985f9d3..8a0c23c4d07 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -17,7 +17,6 @@ package com.nvidia.spark.rapids import ai.rapids.cudf._ - import org.apache.arrow.vector.ValueVector import org.apache.arrow.vector.types.pojo.ArrowType @@ -87,7 +86,6 @@ object HostColumnarToGpu extends Logging { def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, nullable: Boolean, rows: Int): Unit = { - (cv.dataType(), nullable) match { case (ByteType | BooleanType, true) => for (i <- 0 until rows) { @@ -235,7 +233,6 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], assert(goal != RequireSingleBatch) var batchBuilder: GpuColumnVector.GpuColumnarBatchBuilderBase = _ - var useArrow: Boolean = false var totalRows = 0 var maxDeviceMemory: Long = 0 @@ -250,20 +247,18 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], } if (batch.numCols() > 0) { + // when reading host batches it is essential to read the data immediately and pass to a + // builder and we need to determine how many rows to allocate in the builder based on the + // schema and desired batch size + + // TODO - batch row limit right for arrow? + batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, + GpuBatchUtils.estimateGpuMemory(schema, 512), 512) if (batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { - batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, - GpuBatchUtils.estimateGpuMemory(schema, 512), 512) - useArrow = true batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, null) } else { - // when reading host batches it is essential to read the data immediately and pass to a - // builder and we need to determine how many rows to allocate in the builder based on the - // schema and desired batch size - batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, - GpuBatchUtils.estimateGpuMemory(schema, 512), 512) batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) - } } totalRows = 0 From 1414079e2461df781f096bac4a524f55c0dd3b89 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 22 Jan 2021 11:16:57 -0600 Subject: [PATCH 16/49] more cudf changes --- .../nvidia/spark/rapids/GpuColumnVector.java | 14 +++++-- .../spark/rapids/HostColumnarToGpu.scala | 40 ++++++++++--------- .../spark/rapids/RapidsHostMemoryStore.scala | 1 + 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index a1c3b4fef2e..7e18cef908d 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -151,13 +151,20 @@ public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch b int len = fields.length; builders = new ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder[len]; boolean success = false; + + if (batch.numRows() > rows) { + // todo - HOW DO WE SPLIT arrow batches here? address + X + // esimated Rows below is not actually used right now in ArrowColumnBuilder + } + try { logger.warn("schema contains: " + schema.toString()); for (int i = 0; i < len; i++) { StructField field = fields[i]; - logger.warn("field datatype: " + field.dataType() + " converted to: " + convertFrom(field.dataType(), field.nullable())); + logger.warn("field name: " + field.name() + " datatype: " + field.dataType() + " converted to: " + convertFrom(field.dataType(), field.nullable())); - builders[i] = new ArrowHostColumnVector.ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), rows); + // TODO change batch.numRows() to rows if doing estimated and splitting + builders[i] = new ArrowHostColumnVector.ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), batch.numRows(), field.name()); } success = true; } finally { @@ -184,11 +191,10 @@ public ColumnarBatch build(int rows) { boolean success = false; try { for (int i = 0; i < builders.length; i++) { - /* + ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); builders[i] = null; - */ } ColumnarBatch ret = new ColumnarBatch(vectors, rows); success = true; diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 8a0c23c4d07..15a60db9e63 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -33,11 +33,12 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { - def arrowColumnarCopy(cv: ColumnVector, - b: ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder, + def arrowColumnarCopy( + cv: ColumnVector, + ab: ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder, nullable: Boolean, rows: Int): Unit = { - logWarning("host oclunnar to gpu cv is type: " + cv.getClass().toString()) + logWarning("host columnar to gpu cv is type: " + cv.getClass().toString()) if (cv.isInstanceOf[AccessibleArrowColumnVector]) { logWarning("looking at arrow column vector") // TODO - how make sure off heap? @@ -50,34 +51,31 @@ object HostColumnarToGpu extends Logging { logWarning("buffer num is " + buffers.size) val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.memoryAddress() + val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? + val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) + ab.setDataBuf(hostDataBuf) val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() + val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? + val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen) + + ab.setValidityBuf(hostValidBuf) try { val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer if (arrowDataOffsetBuf != null) { logWarning("arrow data offset buffer addrs: " + arrowDataOffsetBuf.memoryAddress()) + val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.capacity() + val hostValidBuf = + new HostMemoryBuffer(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) + ab.setOffsetBuf(hostValidBuf) } else { logWarning("arrow data offset buffer is null") } - if (arrowDataOffsetBuf != null) { - val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.capacity() // ? - logWarning("arrow data offset buffer capcity is: " + arrowDataOffsetLen) - } } catch { case e: UnsupportedOperationException => logWarning("unsupported op getOffsetBuffer") } - // ArrowBuf length instead? = capacity() - val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? - val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? - // val arrowDataOffsetLen = arrowVec.getArrowValueVector.getBufferSize() // ? - logWarning(s"lens data: ${arrowDataLen} validity: $arrowDataValidityLen ") - // s"offset: $arrowDataOffsetLen") - - // need multiple for validity and offset??? - // val hmb = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) - } else { throw new Exception("not arrow data shouldn't be here!") @@ -251,12 +249,16 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], // builder and we need to determine how many rows to allocate in the builder based on the // schema and desired batch size - // TODO - batch row limit right for arrow? + // TODO - batch row limit right for arrow? Do we want to allow splitting or just single? batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, GpuBatchUtils.estimateGpuMemory(schema, 512), 512) + if (batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { + if (batch.numRows() > batchRowLimit) { + logWarning(s"Arrow batch num rows: ${batch.numRows()} > then row limit: $batchRowLimit") + } batchBuilder = - new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, null) + new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala index b23729083e0..b8d3208e14b 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala @@ -45,6 +45,7 @@ class RapidsHostMemoryStore( } var buffer: HostMemoryBuffer = null + buffer.getLength while (buffer == null) { buffer = PinnedMemoryPool.tryAllocate(size) if (buffer != null) { From 396a48855e671b07981749090d2f7a8334c816d1 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 22 Jan 2021 17:11:05 -0600 Subject: [PATCH 17/49] debug --- .../main/java/com/nvidia/spark/rapids/GpuColumnVector.java | 5 ++++- .../scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 7e18cef908d..08285efe159 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -188,14 +188,17 @@ public ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder builder(int i) { public ColumnarBatch build(int rows) { ColumnVector[] vectors = new ColumnVector[builders.length]; + logger.warn(" in build column batch for arrow"); boolean success = false; try { for (int i = 0; i < builders.length; i++) { - + logger.warn(" calling build and put on device: " + i); ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); + logger.warn(" createing gpu column vector"); vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); builders[i] = null; } + logger.warn(" createing columnar batch"); ColumnarBatch ret = new ColumnarBatch(vectors, rows); success = true; return ret; diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 15a60db9e63..945926b847b 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -48,7 +48,7 @@ object HostColumnarToGpu extends Logging { // ValueVector => ArrowBuf val buffers = arrowVec.getArrowValueVector.getBuffers(false) - logWarning("buffer num is " + buffers.size) + logWarning("num buffers is " + buffers.size) val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.memoryAddress() val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? @@ -57,8 +57,8 @@ object HostColumnarToGpu extends Logging { val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen) - ab.setValidityBuf(hostValidBuf) + logWarning(s"buffer data is: $hostDataBuf validitiy buffer is: $hostValidBuf") try { val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer if (arrowDataOffsetBuf != null) { @@ -267,10 +267,12 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], } override def addBatchToConcat(batch: ColumnarBatch): Unit = { + logWarning("in add batch") val rows = batch.numRows() for (i <- 0 until batch.numCols()) { batchBuilder.copyColumnar(batch.column(i), i, schema.fields(i).nullable, rows) } + logWarning("done in add batch") totalRows += rows } @@ -279,6 +281,7 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], } override def concatAllAndPutOnGPU(): ColumnarBatch = { + logWarning("concatallandput on gpu") // About to place data back on the GPU GpuSemaphore.acquireIfNecessary(TaskContext.get()) From 17e446b101b6354a1ab1e673ef181f98c07df8c0 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Mon, 25 Jan 2021 15:24:32 -0600 Subject: [PATCH 18/49] working checkpoint --- .../java/com/nvidia/spark/rapids/GpuColumnVector.java | 4 +++- .../scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 08285efe159..2c159dc2b90 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -192,7 +192,7 @@ public ColumnarBatch build(int rows) { boolean success = false; try { for (int i = 0; i < builders.length; i++) { - logger.warn(" calling build and put on device: " + i); + logger.warn(" calling build and put on device: " + i + " type is: " + fields[i].dataType()); ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); logger.warn(" createing gpu column vector"); vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); @@ -245,6 +245,7 @@ public void close() { } public static final class GpuColumnarBatchBuilder extends GpuColumnarBatchBuilderBase { + private static final Logger logger = LoggerFactory.getLogger(GpuColumnarBatchBuilder.class); private final ai.rapids.cudf.HostColumnVector.ColumnBuilder[] builders; private final StructField[] fields; @@ -293,6 +294,7 @@ public ColumnarBatch build(int rows) { try { for (int i = 0; i < builders.length; i++) { ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); + logger.warn("done building and put not arrow"); vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); builders[i] = null; } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 945926b847b..3cb8dd6a6eb 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -52,8 +52,14 @@ object HostColumnarToGpu extends Logging { val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.memoryAddress() val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? - val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataLen) + val arrowDataMem = arrowVec.getArrowValueVector.getDataBuffer.getActualMemoryConsumed() // ? + + val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 + val arrowDataVals = arrowVec.getArrowValueVector.getValueCount() // 20 + logWarning(s"arrow data lenght is: $arrowDataLen capcity $arrowDataCap memory: $arrowDataMem num values $arrowDataVals") + val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem) ab.setDataBuf(hostDataBuf) + // TODO - need to check null count as validiting isn't required val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen) From 47de41bad90c29474ec924ca64cd63e4c25ed6ad Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Mon, 25 Jan 2021 17:37:48 -0600 Subject: [PATCH 19/49] working without mem crash on free --- .../nvidia/spark/rapids/GpuColumnVector.java | 37 +++++-------------- .../rapids/AccessibleArrowColumnVector.java | 6 +++ .../spark/rapids/HostColumnarToGpu.scala | 7 ++-- .../spark/rapids/RapidsHostMemoryStore.scala | 1 - 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 2c159dc2b90..1a961f8ff55 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -18,7 +18,7 @@ import ai.rapids.cudf.ColumnView; import ai.rapids.cudf.DType; -import ai.rapids.cudf.ArrowHostColumnVector; +import ai.rapids.cudf.ArrowColumnBuilder; import ai.rapids.cudf.HostColumnVector; import ai.rapids.cudf.Scalar; import ai.rapids.cudf.Schema; @@ -134,7 +134,7 @@ public static abstract class GpuColumnarBatchBuilderBase implements AutoCloseabl public static final class GpuArrowColumnarBatchBuilder extends GpuColumnarBatchBuilderBase { private static final Logger logger = LoggerFactory.getLogger(GpuArrowColumnarBatchBuilder.class); - private final ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder[] builders; + private final ai.rapids.cudf.ArrowColumnBuilder[] builders; private final StructField[] fields; /** @@ -149,7 +149,7 @@ public static final class GpuArrowColumnarBatchBuilder extends GpuColumnarBatchB public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch batch) { fields = schema.fields(); int len = fields.length; - builders = new ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder[len]; + builders = new ai.rapids.cudf.ArrowColumnBuilder[len]; boolean success = false; if (batch.numRows() > rows) { @@ -164,12 +164,12 @@ public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch b logger.warn("field name: " + field.name() + " datatype: " + field.dataType() + " converted to: " + convertFrom(field.dataType(), field.nullable())); // TODO change batch.numRows() to rows if doing estimated and splitting - builders[i] = new ArrowHostColumnVector.ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), batch.numRows(), field.name()); + builders[i] = new ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), batch.numRows(), field.name()); } success = true; } finally { if (!success) { - for (ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder b: builders) { + for (ai.rapids.cudf.ArrowColumnBuilder b: builders) { if (b != null) { b.close(); } @@ -182,7 +182,7 @@ public void copyColumnar(ColumnVector cv, int colNum, boolean nullable, int rows HostColumnarToGpu.arrowColumnarCopy(cv, builder(colNum), nullable, rows); } - public ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder builder(int i) { + public ai.rapids.cudf.ArrowColumnBuilder builder(int i) { return builders[i]; } @@ -213,31 +213,12 @@ public ColumnarBatch build(int rows) { } } - public ArrowHostColumnVector[] buildHostColumns() { - ArrowHostColumnVector[] vectors = new ArrowHostColumnVector[builders.length]; - try { - for (int i = 0; i < builders.length; i++) { - vectors[i] = builders[i].build(); - builders[i] = null; - } - ArrowHostColumnVector[] result = vectors; - vectors = null; - return result; - } finally { - if (vectors != null) { - for (ArrowHostColumnVector v : vectors) { - if (v != null) { - v.close(); - } - } - } - } - } - @Override public void close() { - for (ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder b: builders) { + logger.warn(" in close arrow host column vector"); + for (ai.rapids.cudf.ArrowColumnBuilder b: builders) { if (b != null) { + logger.warn("closing builder"); b.close(); } } diff --git a/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java index 6ddfe930a31..1c6616e436a 100644 --- a/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java +++ b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java @@ -20,6 +20,8 @@ import org.apache.arrow.vector.*; import org.apache.arrow.vector.complex.*; import org.apache.arrow.vector.holders.NullableVarCharHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.spark.sql.util.ArrowUtils; import org.apache.spark.sql.types.*; @@ -33,6 +35,7 @@ * supported. */ public final class AccessibleArrowColumnVector extends ColumnVector { + private static final Logger logger = LoggerFactory.getLogger(AccessibleArrowColumnVector.class); private final AccessibleArrowVectorAccessor accessor; private AccessibleArrowColumnVector[] childColumns; @@ -53,6 +56,7 @@ public int numNulls() { @Override public void close() { + logger.warn("closing AccessibleArrowColumnVector"); if (childColumns != null) { for (int i = 0; i < childColumns.length; i++) { childColumns[i].close(); @@ -183,6 +187,7 @@ public AccessibleArrowColumnVector(ValueVector vector) { } private abstract static class AccessibleArrowVectorAccessor { + private static final Logger logger = LoggerFactory.getLogger(AccessibleArrowVectorAccessor.class); private final ValueVector vector; @@ -204,6 +209,7 @@ final int getNullCount() { } final void close() { + logger.warn("accessible arrow vector close()"); vector.close(); } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 3cb8dd6a6eb..0c2e100181d 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -35,7 +35,7 @@ object HostColumnarToGpu extends Logging { def arrowColumnarCopy( cv: ColumnVector, - ab: ai.rapids.cudf.ArrowHostColumnVector.ArrowColumnBuilder, + ab: ai.rapids.cudf.ArrowColumnBuilder, nullable: Boolean, rows: Int): Unit = { logWarning("host columnar to gpu cv is type: " + cv.getClass().toString()) @@ -57,12 +57,12 @@ object HostColumnarToGpu extends Logging { val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 val arrowDataVals = arrowVec.getArrowValueVector.getValueCount() // 20 logWarning(s"arrow data lenght is: $arrowDataLen capcity $arrowDataCap memory: $arrowDataMem num values $arrowDataVals") - val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem) + val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem, null) ab.setDataBuf(hostDataBuf) // TODO - need to check null count as validiting isn't required val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? - val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen) + val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen, null) ab.setValidityBuf(hostValidBuf) logWarning(s"buffer data is: $hostDataBuf validitiy buffer is: $hostValidBuf") try { @@ -303,6 +303,7 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], override def cleanupConcatIsDone(): Unit = { if (batchBuilder != null) { + logWarning("cleanup concat done") batchBuilder.close() batchBuilder = null } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala index b8d3208e14b..b23729083e0 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsHostMemoryStore.scala @@ -45,7 +45,6 @@ class RapidsHostMemoryStore( } var buffer: HostMemoryBuffer = null - buffer.getLength while (buffer == null) { buffer = PinnedMemoryPool.tryAllocate(size) if (buffer != null) { From ecc953ae88602c83524367bb4f29d34e0d459951 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Mon, 25 Jan 2021 17:51:17 -0600 Subject: [PATCH 20/49] remove use of HostmMeoryBuffer --- .../spark/rapids/HostColumnarToGpu.scala | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 0c2e100181d..962adc0c08b 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -57,22 +57,23 @@ object HostColumnarToGpu extends Logging { val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 val arrowDataVals = arrowVec.getArrowValueVector.getValueCount() // 20 logWarning(s"arrow data lenght is: $arrowDataLen capcity $arrowDataCap memory: $arrowDataMem num values $arrowDataVals") - val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem, null) - ab.setDataBuf(hostDataBuf) + // val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem, null) + ab.setDataBuf(arrowDataAddr, arrowDataMem) // TODO - need to check null count as validiting isn't required val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() - val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? - val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen, null) - ab.setValidityBuf(hostValidBuf) - logWarning(s"buffer data is: $hostDataBuf validitiy buffer is: $hostValidBuf") + + val arrowDataValidityLen = arrowVec.getArrowValueVector.getValidityBuffer.getActualMemoryConsumed() + // val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? + // val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen, null) + ab.setValidityBuf(arrowDataValidity, arrowDataValidityLen) + // logWarning(s"buffer data is: $hostDataBuf validitiy buffer is: $hostValidBuf") try { val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer if (arrowDataOffsetBuf != null) { logWarning("arrow data offset buffer addrs: " + arrowDataOffsetBuf.memoryAddress()) - val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.capacity() - val hostValidBuf = - new HostMemoryBuffer(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) - ab.setOffsetBuf(hostValidBuf) + val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.getActualMemoryConsumed() + // val hostValidBuf = new HostMemoryBuffer(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) + ab.setOffsetBuf(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) } else { logWarning("arrow data offset buffer is null") } From 016bad03c4adabdb6c8dd08fdb1b29966d06a2a0 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Mon, 25 Jan 2021 19:55:36 -0600 Subject: [PATCH 21/49] check null count --- .../com/nvidia/spark/rapids/GpuColumnVector.java | 2 -- .../com/nvidia/spark/rapids/HostColumnarToGpu.scala | 13 +++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 1a961f8ff55..d89ad9457ed 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -122,7 +122,6 @@ private static HostColumnVector.DataType convertFrom(DataType spark, boolean nul } } - // interface??? - then can't use autocloseable here public static abstract class GpuColumnarBatchBuilderBase implements AutoCloseable { public abstract ColumnarBatch build(int rows); @@ -131,7 +130,6 @@ public static abstract class GpuColumnarBatchBuilderBase implements AutoCloseabl public abstract void copyColumnar(ColumnVector cv, int colNum, boolean nullable, int rows); } - public static final class GpuArrowColumnarBatchBuilder extends GpuColumnarBatchBuilderBase { private static final Logger logger = LoggerFactory.getLogger(GpuArrowColumnarBatchBuilder.class); private final ai.rapids.cudf.ArrowColumnBuilder[] builders; diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 962adc0c08b..ab88d695b38 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -44,6 +44,7 @@ object HostColumnarToGpu extends Logging { // TODO - how make sure off heap? // could create HostMemoryBuffer(addr, length)' val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] + // TODO - accessor is private to ArrowColumnVector!!! // ValueVector => ArrowBuf @@ -60,13 +61,13 @@ object HostColumnarToGpu extends Logging { // val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem, null) ab.setDataBuf(arrowDataAddr, arrowDataMem) // TODO - need to check null count as validiting isn't required - val arrowDataValidity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() + val nullCount = arrowVec.getArrowValueVector.getNullCount() + if (nullCount > 0) { + val validity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() + val validityLen = arrowVec.getArrowValueVector.getValidityBuffer.getActualMemoryConsumed() + ab.setValidityBuf(validity, validityLen) + } - val arrowDataValidityLen = arrowVec.getArrowValueVector.getValidityBuffer.getActualMemoryConsumed() - // val arrowDataValidityLen = arrowVec.getArrowValueVector.getBufferSize() // ? - // val hostValidBuf = new HostMemoryBuffer(arrowDataValidity, arrowDataValidityLen, null) - ab.setValidityBuf(arrowDataValidity, arrowDataValidityLen) - // logWarning(s"buffer data is: $hostDataBuf validitiy buffer is: $hostValidBuf") try { val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer if (arrowDataOffsetBuf != null) { From a342c0596c17b9d1cf41f57641e23f02b7349df8 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Tue, 26 Jan 2021 10:30:48 -0600 Subject: [PATCH 22/49] changes --- .../nvidia/spark/rapids/GpuColumnVector.java | 2 ++ .../rapids/AccessibleArrowColumnVector.java | 2 -- .../spark/rapids/HostColumnarToGpu.scala | 36 +++++++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index d89ad9457ed..e1014f7c1b8 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -271,12 +271,14 @@ public ColumnarBatch build(int rows) { ColumnVector[] vectors = new ColumnVector[builders.length]; boolean success = false; try { + logger.warn(" column builders lenght: " + builders.length); for (int i = 0; i < builders.length; i++) { ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); logger.warn("done building and put not arrow"); vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); builders[i] = null; } + logger.warn("making column batch with vectors: " + vectors + " rows:" + rows); ColumnarBatch ret = new ColumnarBatch(vectors, rows); success = true; return ret; diff --git a/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java index 1c6616e436a..03da24f8488 100644 --- a/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java +++ b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java @@ -56,7 +56,6 @@ public int numNulls() { @Override public void close() { - logger.warn("closing AccessibleArrowColumnVector"); if (childColumns != null) { for (int i = 0; i < childColumns.length; i++) { childColumns[i].close(); @@ -209,7 +208,6 @@ final int getNullCount() { } final void close() { - logger.warn("accessible arrow vector close()"); vector.close(); } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index ab88d695b38..e691aaef356 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -38,7 +38,7 @@ object HostColumnarToGpu extends Logging { ab: ai.rapids.cudf.ArrowColumnBuilder, nullable: Boolean, rows: Int): Unit = { - logWarning("host columnar to gpu cv is type: " + cv.getClass().toString()) + logWarning("Arrow host columnar to gpu cv is type: " + cv.getClass().toString()) if (cv.isInstanceOf[AccessibleArrowColumnVector]) { logWarning("looking at arrow column vector") // TODO - how make sure off heap? @@ -62,13 +62,16 @@ object HostColumnarToGpu extends Logging { ab.setDataBuf(arrowDataAddr, arrowDataMem) // TODO - need to check null count as validiting isn't required val nullCount = arrowVec.getArrowValueVector.getNullCount() - if (nullCount > 0) { + ab.setNullCount(nullCount) + logWarning("null count is " + nullCount) + // if (nullCount > 0) { val validity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() val validityLen = arrowVec.getArrowValueVector.getValidityBuffer.getActualMemoryConsumed() ab.setValidityBuf(validity, validityLen) - } + // } try { + // TODO - should we chekc types first instead? val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer if (arrowDataOffsetBuf != null) { logWarning("arrow data offset buffer addrs: " + arrowDataOffsetBuf.memoryAddress()) @@ -83,7 +86,6 @@ object HostColumnarToGpu extends Logging { logWarning("unsupported op getOffsetBuffer") } - logWarning(s"lens data: ${arrowDataLen} validity: $arrowDataValidityLen ") } else { throw new Exception("not arrow data shouldn't be here!") @@ -92,6 +94,7 @@ object HostColumnarToGpu extends Logging { def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, nullable: Boolean, rows: Int): Unit = { + logWarning("Not Arrow host columnar to gpu cv is type: " + cv.getClass().toString()) (cv.dataType(), nullable) match { case (ByteType | BooleanType, true) => for (i <- 0 until rows) { @@ -252,7 +255,7 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], batchBuilder = null } - if (batch.numCols() > 0) { + logWarning(" in init new batch cols is:" + batch.numCols()) // when reading host batches it is essential to read the data immediately and pass to a // builder and we need to determine how many rows to allocate in the builder based on the // schema and desired batch size @@ -261,16 +264,23 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, GpuBatchUtils.estimateGpuMemory(schema, 512), 512) - if (batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { - if (batch.numRows() > batchRowLimit) { - logWarning(s"Arrow batch num rows: ${batch.numRows()} > then row limit: $batchRowLimit") - } + // if no columns then probably a count operation so doesn't matter which builder we use + val isArrow = if (batch.numCols() > 0 && batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { + true; + } else { + false + } + if (batch.numRows() > batchRowLimit) { + logWarning(s"batch num rows: ${batch.numRows()} > then row limit: $batchRowLimit") + } + if (isArrow) { + logWarning("arrow batch builder") batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { + logWarning("not an arrow batch builder") batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) - } - } + } totalRows = 0 } @@ -293,6 +303,10 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], // About to place data back on the GPU GpuSemaphore.acquireIfNecessary(TaskContext.get()) + logWarning("batch builder build total Rows: " + totalRows) + if (batchBuilder == null) { + logWarning("batch builder is null"); + } val ret = batchBuilder.build(totalRows) maxDeviceMemory = GpuColumnVector.getTotalDeviceMemoryUsed(ret) From 40e41150551158a33330e0f34e2f7ebe399ab4f0 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Tue, 26 Jan 2021 11:45:49 -0600 Subject: [PATCH 23/49] comment --- .../main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index e691aaef356..3fa5e007a1d 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -275,6 +275,7 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], } if (isArrow) { logWarning("arrow batch builder") + // todo - REMOVE BATCH ROWLIMIT batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { From 0e441ef864c3adefb766efbc83fd63ca3b3a8dad Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Tue, 26 Jan 2021 13:22:46 -0600 Subject: [PATCH 24/49] working --- .../com/nvidia/spark/rapids/HostColumnarToGpu.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 3fa5e007a1d..199ee25d8f1 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -70,14 +70,17 @@ object HostColumnarToGpu extends Logging { ab.setValidityBuf(validity, validityLen) // } + var offsets = 0L + var offsetsLen = 0L try { // TODO - should we chekc types first instead? val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer if (arrowDataOffsetBuf != null) { logWarning("arrow data offset buffer addrs: " + arrowDataOffsetBuf.memoryAddress()) - val arrowDataOffsetLen = arrowVec.getArrowValueVector.getOffsetBuffer.getActualMemoryConsumed() // val hostValidBuf = new HostMemoryBuffer(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) - ab.setOffsetBuf(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) + offsets = arrowDataOffsetBuf.memoryAddress() + offsetsLen = arrowVec.getArrowValueVector.getOffsetBuffer.getActualMemoryConsumed() + ab.setOffsetBuf(arrowDataOffsetBuf.memoryAddress(), offsetsLen) } else { logWarning("arrow data offset buffer is null") } @@ -85,7 +88,7 @@ object HostColumnarToGpu extends Logging { case e: UnsupportedOperationException => logWarning("unsupported op getOffsetBuffer") } - + ab.addBatch(rows, nullCount, arrowDataAddr, arrowDataMem, validity, validityLen, offsets, offsetsLen) } else { throw new Exception("not arrow data shouldn't be here!") From 7a7fc22af19c6727376bfc6dc0ea2490df09b11f Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 27 Jan 2021 09:54:41 -0600 Subject: [PATCH 25/49] working --- .../nvidia/spark/rapids/GpuColumnVector.java | 12 +---- .../spark/rapids/GpuCoalesceBatches.scala | 6 +++ .../spark/rapids/HostColumnarToGpu.scala | 47 ++++++++++++++++--- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index e1014f7c1b8..db3411ca842 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -136,7 +136,7 @@ public static final class GpuArrowColumnarBatchBuilder extends GpuColumnarBatchB private final StructField[] fields; /** - * A collection of builders for building up columnar data. + * A collection of builders for building up columnar data from Arrow data. * @param schema the schema of the batch. * @param rows the maximum number of rows in this batch. * @param batch if this is going to copy a ColumnarBatch in a non GPU format that batch @@ -150,19 +150,11 @@ public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch b builders = new ai.rapids.cudf.ArrowColumnBuilder[len]; boolean success = false; - if (batch.numRows() > rows) { - // todo - HOW DO WE SPLIT arrow batches here? address + X - // esimated Rows below is not actually used right now in ArrowColumnBuilder - } - try { - logger.warn("schema contains: " + schema.toString()); for (int i = 0; i < len; i++) { StructField field = fields[i]; logger.warn("field name: " + field.name() + " datatype: " + field.dataType() + " converted to: " + convertFrom(field.dataType(), field.nullable())); - - // TODO change batch.numRows() to rows if doing estimated and splitting - builders[i] = new ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), batch.numRows(), field.name()); + builders[i] = new ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), field.name()); } success = true; } finally { diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala index 20135a7c34c..76a3fb6d8e9 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala @@ -185,6 +185,7 @@ abstract class AbstractGpuCoalesceIterator( closeOnExcept(iterNext()) { cb => val numRows = cb.numRows() numInputBatches += 1 + logWarning("AbstractGpuCoalesceIterator num input batches " + numInputBatches) numInputRows += numRows if (numRows > 0) { saveOnDeck(cb) @@ -268,11 +269,14 @@ abstract class AbstractGpuCoalesceIterator( val batch = popOnDeck() numRows += batch.numRows() numBytes += getBatchDataSize(batch) + logWarning("add batch since on deck") addBatch(batch) } + logWarning(s"add batch before while rows: $numRows on deck $hasOnDeck") // there is a hard limit of 2^31 rows while (numRows < Int.MaxValue && !hasOnDeck && iterHasNext) { + logWarning(s"add batch inside while rows: $numRows on deck $hasOnDeck") closeOnExcept(iterNext()) { cb => val nextRows = cb.numRows() numInputBatches += 1 @@ -287,6 +291,7 @@ abstract class AbstractGpuCoalesceIterator( val wouldBeRows = numRows + nextRows val wouldBeBytes = numBytes + nextBytes + logWarning("add batch target: " + batchRowLimit + " would be is: " + wouldBeRows) if (wouldBeRows > Int.MaxValue) { if (goal == RequireSingleBatch) { throw new IllegalStateException("A single batch is required for this operation," + @@ -303,6 +308,7 @@ abstract class AbstractGpuCoalesceIterator( // 2GB data total to avoid hitting that error. saveOnDeck(cb) } else { + logWarning("add batch") addBatch(cb) numRows = wouldBeRows numBytes = wouldBeBytes diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 199ee25d8f1..65938a658f3 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -33,12 +33,42 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { + def getValueVector(cv: ColumnVector): ValueVector = { + val arrowCV = cv.asInstanceOf[ArrowColumnVector] + val fields = arrowCV.getClass.getDeclaredFields.toList + + val field = fields.filter(x => { + println(x.getName) + x.getName.contains("accessor") + }).head + + logWarning(" got accessor") + field.setAccessible(true) + val accessor = field.get(arrowCV) + val accessFields = accessor.getClass().getDeclaredFields().toList + + val valVecField = accessFields.filter(x => { + println(x.getName) + x.getName.contains("vector") + }).head + logWarning(" got vector") + + valVecField.setAccessible(true) + val res = valVecField.get(accessor).asInstanceOf[ValueVector] + logWarning(" got res " + res) + res + } + def arrowColumnarCopy( cv: ColumnVector, ab: ai.rapids.cudf.ArrowColumnBuilder, nullable: Boolean, rows: Int): Unit = { logWarning("Arrow host columnar to gpu cv is type: " + cv.getClass().toString()) + if (cv.isInstanceOf[ArrowColumnVector]) { + val res = getValueVector(cv) + logWarning("value vector is: " + res) + } if (cv.isInstanceOf[AccessibleArrowColumnVector]) { logWarning("looking at arrow column vector") // TODO - how make sure off heap? @@ -55,19 +85,19 @@ object HostColumnarToGpu extends Logging { val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? val arrowDataMem = arrowVec.getArrowValueVector.getDataBuffer.getActualMemoryConsumed() // ? - val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 + // val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 val arrowDataVals = arrowVec.getArrowValueVector.getValueCount() // 20 - logWarning(s"arrow data lenght is: $arrowDataLen capcity $arrowDataCap memory: $arrowDataMem num values $arrowDataVals") + logWarning(s"arrow data lenght is: $arrowDataLen memory: $arrowDataMem num values $arrowDataVals") // val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem, null) - ab.setDataBuf(arrowDataAddr, arrowDataMem) + // ab.setDataBuf(arrowDataAddr, arrowDataMem) // TODO - need to check null count as validiting isn't required val nullCount = arrowVec.getArrowValueVector.getNullCount() - ab.setNullCount(nullCount) + // ab.setNullCount(nullCount) logWarning("null count is " + nullCount) // if (nullCount > 0) { val validity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() val validityLen = arrowVec.getArrowValueVector.getValidityBuffer.getActualMemoryConsumed() - ab.setValidityBuf(validity, validityLen) + // ab.setValidityBuf(validity, validityLen) // } var offsets = 0L @@ -80,7 +110,7 @@ object HostColumnarToGpu extends Logging { // val hostValidBuf = new HostMemoryBuffer(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) offsets = arrowDataOffsetBuf.memoryAddress() offsetsLen = arrowVec.getArrowValueVector.getOffsetBuffer.getActualMemoryConsumed() - ab.setOffsetBuf(arrowDataOffsetBuf.memoryAddress(), offsetsLen) + // ab.setOffsetBuf(arrowDataOffsetBuf.memoryAddress(), offsetsLen) } else { logWarning("arrow data offset buffer is null") } @@ -89,6 +119,7 @@ object HostColumnarToGpu extends Logging { logWarning("unsupported op getOffsetBuffer") } ab.addBatch(rows, nullCount, arrowDataAddr, arrowDataMem, validity, validityLen, offsets, offsetsLen) + logWarning("builder arrow is: " + ab.toString()) } else { throw new Exception("not arrow data shouldn't be here!") @@ -268,7 +299,8 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], GpuBatchUtils.estimateGpuMemory(schema, 512), 512) // if no columns then probably a count operation so doesn't matter which builder we use - val isArrow = if (batch.numCols() > 0 && batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { + // val isArrow = if (batch.numCols() > 0 && batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { + val isArrow = if (batch.numCols() > 0 && (batch.column(0).isInstanceOf[ArrowColumnVector] || batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { true; } else { false @@ -400,6 +432,7 @@ case class HostColumnarToGpu(child: SparkPlan, goal: CoalesceGoal) val batches = child.executeColumnar() + logWarning(" num partitions is: " + batches.getNumPartitions) batches.mapPartitions { iter => new HostToGpuCoalesceIterator(iter, goal, outputSchema, numInputRows, numInputBatches, numOutputRows, numOutputBatches, collectTime, concatTime, From 2f42c08bf9e73e3d00eeb4cfbaba115f6d8deda3 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 27 Jan 2021 17:25:41 -0600 Subject: [PATCH 26/49] Update to the new CUDF code and add a config to toggle arrow copy on and off --- .../nvidia/spark/rapids/GpuColumnVector.java | 2 +- .../spark/rapids/HostColumnarToGpu.scala | 35 ++++++++++++++----- .../com/nvidia/spark/rapids/RapidsConf.scala | 6 ++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index db3411ca842..932b02cb53a 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -154,7 +154,7 @@ public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch b for (int i = 0; i < len; i++) { StructField field = fields[i]; logger.warn("field name: " + field.name() + " datatype: " + field.dataType() + " converted to: " + convertFrom(field.dataType(), field.nullable())); - builders[i] = new ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable()), field.name()); + builders[i] = new ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable())); } success = true; } finally { diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 65938a658f3..0a9295a4cab 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -16,6 +16,8 @@ package com.nvidia.spark.rapids +import java.nio.ByteBuffer + import ai.rapids.cudf._ import org.apache.arrow.vector.ValueVector import org.apache.arrow.vector.types.pojo.ArrowType @@ -81,13 +83,19 @@ object HostColumnarToGpu extends Logging { val buffers = arrowVec.getArrowValueVector.getBuffers(false) logWarning("num buffers is " + buffers.size) - val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.memoryAddress() + val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.nioBuffer() val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? val arrowDataMem = arrowVec.getArrowValueVector.getDataBuffer.getActualMemoryConsumed() // ? - // val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 + val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 val arrowDataVals = arrowVec.getArrowValueVector.getValueCount() // 20 + val arrowDataPos = arrowVec.getArrowValueVector.getDataBuffer.getPossibleMemoryConsumed() // 20 + val byteBuf = arrowVec.getArrowValueVector.getDataBuffer.nioBuffer() + logWarning(s" byte buffer position is: ${byteBuf.position()}") + logWarning(s"address of byte buffer is ${System.identityHashCode(byteBuf)}") + logWarning(s"arrow data lenght is: $arrowDataLen memory: $arrowDataMem num values $arrowDataVals") + logWarning(s"arrow data lenght is: $arrowDataLen memory: $arrowDataMem num values $arrowDataVals capcity: $arrowDataCap possible: $arrowDataPos") // val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem, null) // ab.setDataBuf(arrowDataAddr, arrowDataMem) // TODO - need to check null count as validiting isn't required @@ -95,21 +103,26 @@ object HostColumnarToGpu extends Logging { // ab.setNullCount(nullCount) logWarning("null count is " + nullCount) // if (nullCount > 0) { - val validity = arrowVec.getArrowValueVector.getValidityBuffer.memoryAddress() + val validity = arrowVec.getArrowValueVector.getValidityBuffer.nioBuffer() val validityLen = arrowVec.getArrowValueVector.getValidityBuffer.getActualMemoryConsumed() + val validityCap = arrowVec.getArrowValueVector.getValidityBuffer.capacity() + logWarning(s"arrow validity memory is: $validityLen length si ${arrowVec.getArrowValueVector.getValidityBuffer.capacity()}") // ab.setValidityBuf(validity, validityLen) // } - var offsets = 0L + var offsets:ByteBuffer = null var offsetsLen = 0L + var offsetsCap = 0L try { // TODO - should we chekc types first instead? val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer if (arrowDataOffsetBuf != null) { logWarning("arrow data offset buffer addrs: " + arrowDataOffsetBuf.memoryAddress()) // val hostValidBuf = new HostMemoryBuffer(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) - offsets = arrowDataOffsetBuf.memoryAddress() + offsets = arrowDataOffsetBuf.nioBuffer() offsetsLen = arrowVec.getArrowValueVector.getOffsetBuffer.getActualMemoryConsumed() + offsetsCap = arrowVec.getArrowValueVector.getOffsetBuffer.capacity() + logWarning(s"arrow offsets memory is: $offsetsLen length si ${arrowVec.getArrowValueVector.getOffsetBuffer.capacity()}") // ab.setOffsetBuf(arrowDataOffsetBuf.memoryAddress(), offsetsLen) } else { logWarning("arrow data offset buffer is null") @@ -118,7 +131,7 @@ object HostColumnarToGpu extends Logging { case e: UnsupportedOperationException => logWarning("unsupported op getOffsetBuffer") } - ab.addBatch(rows, nullCount, arrowDataAddr, arrowDataMem, validity, validityLen, offsets, offsetsLen) + ab.addBatch(rows, nullCount, arrowDataAddr, validity, offsets) logWarning("builder arrow is: " + ab.toString()) } else { @@ -260,7 +273,8 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], concatTime: SQLMetric, totalTime: SQLMetric, peakDevMemory: SQLMetric, - opName: String) + opName: String, + useArrowCopyOpt: Boolean) extends AbstractGpuCoalesceIterator(iter, goal, numInputRows, @@ -300,7 +314,9 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], // if no columns then probably a count operation so doesn't matter which builder we use // val isArrow = if (batch.numCols() > 0 && batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { - val isArrow = if (batch.numCols() > 0 && (batch.column(0).isInstanceOf[ArrowColumnVector] || batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { + val isArrow = if (useArrowCopyOpt && batch.numCols() > 0 && + (batch.column(0).isInstanceOf[ArrowColumnVector] || + batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { true; } else { false @@ -432,11 +448,12 @@ case class HostColumnarToGpu(child: SparkPlan, goal: CoalesceGoal) val batches = child.executeColumnar() + val confUseArrow = new RapidsConf(child.conf).useArrowCopyOptimization logWarning(" num partitions is: " + batches.getNumPartitions) batches.mapPartitions { iter => new HostToGpuCoalesceIterator(iter, goal, outputSchema, numInputRows, numInputBatches, numOutputRows, numOutputBatches, collectTime, concatTime, - totalTime, peakDevMemory, "HostColumnarToGpu") + totalTime, peakDevMemory, "HostColumnarToGpu", confUseArrow) } } } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala index ae1105eb1c2..5f5d35e4ec7 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala @@ -820,6 +820,10 @@ object RapidsConf { .booleanConf .createWithDefault(true) + val USE_ARROW_OPT = conf("spark.rapids.arrowCopyOptmizationEnabled") + .booleanConf + .createWithDefault(true) + private def printSectionHeader(category: String): Unit = println(s"\n### $category") @@ -1115,6 +1119,8 @@ class RapidsConf(conf: Map[String, String]) extends Logging { lazy val allowDisableEntirePlan: Boolean = get(ALLOW_DISABLE_ENTIRE_PLAN) + lazy val useArrowCopyOptimization: Boolean = get(USE_ARROW_OPT) + lazy val getCloudSchemes: Option[Seq[String]] = get(CLOUD_SCHEMES) def isOperatorEnabled(key: String, incompat: Boolean, isDisabledByDefault: Boolean): Boolean = { From 3bdf7f1b5a6f38ebfb8efc83204cfda7c0d420ba Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Wed, 27 Jan 2021 18:04:59 -0600 Subject: [PATCH 27/49] have the reflection code to access ArrowColumnVEctor working --- pom.xml | 2 +- .../spark/rapids/HostColumnarToGpu.scala | 105 +++++------------- 2 files changed, 28 insertions(+), 79 deletions(-) diff --git a/pom.xml b/pom.xml index 9d131cdd59b..66a83f48760 100644 --- a/pom.xml +++ b/pom.xml @@ -176,7 +176,7 @@ 3.0.2-SNAPSHOT 3.1.1-SNAPSHOT 3.6.0 - 4.3.0 + 3.4.4 diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 0a9295a4cab..af514cad50a 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -35,30 +35,21 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { + // use reflection to get value vector from ArrowColumnVector def getValueVector(cv: ColumnVector): ValueVector = { val arrowCV = cv.asInstanceOf[ArrowColumnVector] val fields = arrowCV.getClass.getDeclaredFields.toList - val field = fields.filter(x => { - println(x.getName) x.getName.contains("accessor") }).head - - logWarning(" got accessor") field.setAccessible(true) val accessor = field.get(arrowCV) - val accessFields = accessor.getClass().getDeclaredFields().toList - - val valVecField = accessFields.filter(x => { - println(x.getName) + val accessFields = accessor.getClass().getSuperclass().getDeclaredFields().toList + val valVecField = foosuper.filter( x => { x.getName.contains("vector") }).head - logWarning(" got vector") - valVecField.setAccessible(true) - val res = valVecField.get(accessor).asInstanceOf[ValueVector] - logWarning(" got res " + res) - res + valVecField.get(accessor).asInstanceOf[ValueVector] } def arrowColumnarCopy( @@ -67,76 +58,34 @@ object HostColumnarToGpu extends Logging { nullable: Boolean, rows: Int): Unit = { logWarning("Arrow host columnar to gpu cv is type: " + cv.getClass().toString()) - if (cv.isInstanceOf[ArrowColumnVector]) { - val res = getValueVector(cv) - logWarning("value vector is: " + res) - } - if (cv.isInstanceOf[AccessibleArrowColumnVector]) { + val valVector = if (cv.isInstanceOf[ArrowColumnVector]) { logWarning("looking at arrow column vector") - // TODO - how make sure off heap? - // could create HostMemoryBuffer(addr, length)' + getValueVector(cv) + } else if (cv.isInstanceOf[AccessibleArrowColumnVector]) { + logWarning("looking at accessible arrow column vector") val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] - - // TODO - accessor is private to ArrowColumnVector!!! - // ValueVector => ArrowBuf - - val buffers = arrowVec.getArrowValueVector.getBuffers(false) - logWarning("num buffers is " + buffers.size) - - val arrowDataAddr = arrowVec.getArrowValueVector.getDataBuffer.nioBuffer() - val arrowDataLen = arrowVec.getArrowValueVector.getBufferSize() // ? - val arrowDataMem = arrowVec.getArrowValueVector.getDataBuffer.getActualMemoryConsumed() // ? - - val arrowDataCap = arrowVec.getArrowValueVector.getDataBuffer.capacity() // ? 80 - val arrowDataVals = arrowVec.getArrowValueVector.getValueCount() // 20 - val arrowDataPos = arrowVec.getArrowValueVector.getDataBuffer.getPossibleMemoryConsumed() // 20 - val byteBuf = arrowVec.getArrowValueVector.getDataBuffer.nioBuffer() - logWarning(s" byte buffer position is: ${byteBuf.position()}") - logWarning(s"address of byte buffer is ${System.identityHashCode(byteBuf)}") - - logWarning(s"arrow data lenght is: $arrowDataLen memory: $arrowDataMem num values $arrowDataVals") - logWarning(s"arrow data lenght is: $arrowDataLen memory: $arrowDataMem num values $arrowDataVals capcity: $arrowDataCap possible: $arrowDataPos") - // val hostDataBuf = new HostMemoryBuffer(arrowDataAddr, arrowDataMem, null) - // ab.setDataBuf(arrowDataAddr, arrowDataMem) - // TODO - need to check null count as validiting isn't required - val nullCount = arrowVec.getArrowValueVector.getNullCount() - // ab.setNullCount(nullCount) - logWarning("null count is " + nullCount) - // if (nullCount > 0) { - val validity = arrowVec.getArrowValueVector.getValidityBuffer.nioBuffer() - val validityLen = arrowVec.getArrowValueVector.getValidityBuffer.getActualMemoryConsumed() - val validityCap = arrowVec.getArrowValueVector.getValidityBuffer.capacity() - logWarning(s"arrow validity memory is: $validityLen length si ${arrowVec.getArrowValueVector.getValidityBuffer.capacity()}") - // ab.setValidityBuf(validity, validityLen) - // } - - var offsets:ByteBuffer = null - var offsetsLen = 0L - var offsetsCap = 0L - try { - // TODO - should we chekc types first instead? - val arrowDataOffsetBuf = arrowVec.getArrowValueVector.getOffsetBuffer - if (arrowDataOffsetBuf != null) { - logWarning("arrow data offset buffer addrs: " + arrowDataOffsetBuf.memoryAddress()) - // val hostValidBuf = new HostMemoryBuffer(arrowDataOffsetBuf.memoryAddress(), arrowDataOffsetLen) - offsets = arrowDataOffsetBuf.nioBuffer() - offsetsLen = arrowVec.getArrowValueVector.getOffsetBuffer.getActualMemoryConsumed() - offsetsCap = arrowVec.getArrowValueVector.getOffsetBuffer.capacity() - logWarning(s"arrow offsets memory is: $offsetsLen length si ${arrowVec.getArrowValueVector.getOffsetBuffer.capacity()}") - // ab.setOffsetBuf(arrowDataOffsetBuf.memoryAddress(), offsetsLen) - } else { - logWarning("arrow data offset buffer is null") - } - } catch { - case e: UnsupportedOperationException => - logWarning("unsupported op getOffsetBuffer") - } - ab.addBatch(rows, nullCount, arrowDataAddr, validity, offsets) - logWarning("builder arrow is: " + ab.toString()) - + arrowVec.getArrowValueVector() } else { throw new Exception("not arrow data shouldn't be here!") } + val arrowDataAddr = valVector.getDataBuffer.nioBuffer() + val byteBuf = valVector.getDataBuffer.nioBuffer() + val nullCount = valVector.getNullCount() + val validity = valVector.getValidityBuffer.nioBuffer() + + var offsets:ByteBuffer = null + try { + // TODO - should we chekc types first instead? + val arrowDataOffsetBuf = valVector.getOffsetBuffer + if (arrowDataOffsetBuf != null) { + offsets = arrowDataOffsetBuf.nioBuffer() + } + } catch { + case e: UnsupportedOperationException => + logWarning("unsupported op getOffsetBuffer") + } + ab.addBatch(rows, nullCount, arrowDataAddr, validity, offsets) + logWarning("builder arrow is: " + ab.toString()) } def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, From 5b7561cf5eeed282f493618d4f1c95ead7d990f7 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 10:42:23 -0600 Subject: [PATCH 28/49] remove logging and commonize --- .../nvidia/spark/rapids/GpuColumnVector.java | 110 ++++++++---------- .../spark/rapids/HostColumnarToGpu.scala | 23 +--- 2 files changed, 47 insertions(+), 86 deletions(-) diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index 932b02cb53a..e03d0c6cd34 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -24,9 +24,6 @@ import ai.rapids.cudf.Schema; import ai.rapids.cudf.Table; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import org.apache.spark.sql.catalyst.expressions.Attribute; import org.apache.spark.sql.types.*; import org.apache.spark.sql.vectorized.ColumnVector; @@ -123,17 +120,39 @@ private static HostColumnVector.DataType convertFrom(DataType spark, boolean nul } public static abstract class GpuColumnarBatchBuilderBase implements AutoCloseable { - public abstract ColumnarBatch build(int rows); + protected StructField[] fields; public abstract void close(); - public abstract void copyColumnar(ColumnVector cv, int colNum, boolean nullable, int rows); + + protected abstract ColumnVector buildAndPutOnDevice(int builderIndex); + protected abstract int buildersLength(); + + public ColumnarBatch build(int rows) { + int buildersLen = buildersLength(); + ColumnVector[] vectors = new ColumnVector[buildersLen]; + boolean success = false; + try { + for (int i = 0; i < buildersLen; i++) { + vectors[i] = buildAndPutOnDevice(i); + } + ColumnarBatch ret = new ColumnarBatch(vectors, rows); + success = true; + return ret; + } finally { + if (!success) { + for (ColumnVector vec: vectors) { + if (vec != null) { + vec.close(); + } + } + } + } + } } public static final class GpuArrowColumnarBatchBuilder extends GpuColumnarBatchBuilderBase { - private static final Logger logger = LoggerFactory.getLogger(GpuArrowColumnarBatchBuilder.class); private final ai.rapids.cudf.ArrowColumnBuilder[] builders; - private final StructField[] fields; /** * A collection of builders for building up columnar data from Arrow data. @@ -153,7 +172,6 @@ public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch b try { for (int i = 0; i < len; i++) { StructField field = fields[i]; - logger.warn("field name: " + field.name() + " datatype: " + field.dataType() + " converted to: " + convertFrom(field.dataType(), field.nullable())); builders[i] = new ArrowColumnBuilder(convertFrom(field.dataType(), field.nullable())); } success = true; @@ -168,6 +186,17 @@ public GpuArrowColumnarBatchBuilder(StructType schema, int rows, ColumnarBatch b } } + protected int buildersLength() { + return builders.length; + } + + protected ColumnVector buildAndPutOnDevice(int builderIndex) { + ai.rapids.cudf.ColumnVector cv = builders[builderIndex].buildAndPutOnDevice(); + GpuColumnVector gcv = new GpuColumnVector(fields[builderIndex].dataType(), cv); + builders[builderIndex] = null; + return gcv; + } + public void copyColumnar(ColumnVector cv, int colNum, boolean nullable, int rows) { HostColumnarToGpu.arrowColumnarCopy(cv, builder(colNum), nullable, rows); } @@ -176,39 +205,10 @@ public ai.rapids.cudf.ArrowColumnBuilder builder(int i) { return builders[i]; } - public ColumnarBatch build(int rows) { - ColumnVector[] vectors = new ColumnVector[builders.length]; - logger.warn(" in build column batch for arrow"); - boolean success = false; - try { - for (int i = 0; i < builders.length; i++) { - logger.warn(" calling build and put on device: " + i + " type is: " + fields[i].dataType()); - ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); - logger.warn(" createing gpu column vector"); - vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); - builders[i] = null; - } - logger.warn(" createing columnar batch"); - ColumnarBatch ret = new ColumnarBatch(vectors, rows); - success = true; - return ret; - } finally { - if (!success) { - for (ColumnVector vec: vectors) { - if (vec != null) { - vec.close(); - } - } - } - } - } - @Override public void close() { - logger.warn(" in close arrow host column vector"); for (ai.rapids.cudf.ArrowColumnBuilder b: builders) { if (b != null) { - logger.warn("closing builder"); b.close(); } } @@ -216,9 +216,7 @@ public void close() { } public static final class GpuColumnarBatchBuilder extends GpuColumnarBatchBuilderBase { - private static final Logger logger = LoggerFactory.getLogger(GpuColumnarBatchBuilder.class); private final ai.rapids.cudf.HostColumnVector.ColumnBuilder[] builders; - private final StructField[] fields; /** * A collection of builders for building up columnar data. @@ -259,30 +257,15 @@ public ai.rapids.cudf.HostColumnVector.ColumnBuilder builder(int i) { return builders[i]; } - public ColumnarBatch build(int rows) { - ColumnVector[] vectors = new ColumnVector[builders.length]; - boolean success = false; - try { - logger.warn(" column builders lenght: " + builders.length); - for (int i = 0; i < builders.length; i++) { - ai.rapids.cudf.ColumnVector cv = builders[i].buildAndPutOnDevice(); - logger.warn("done building and put not arrow"); - vectors[i] = new GpuColumnVector(fields[i].dataType(), cv); - builders[i] = null; - } - logger.warn("making column batch with vectors: " + vectors + " rows:" + rows); - ColumnarBatch ret = new ColumnarBatch(vectors, rows); - success = true; - return ret; - } finally { - if (!success) { - for (ColumnVector vec: vectors) { - if (vec != null) { - vec.close(); - } - } - } - } + protected int buildersLength() { + return builders.length; + } + + protected ColumnVector buildAndPutOnDevice(int builderIndex) { + ai.rapids.cudf.ColumnVector cv = builders[builderIndex].buildAndPutOnDevice(); + GpuColumnVector gcv = new GpuColumnVector(fields[builderIndex].dataType(), cv); + builders[builderIndex] = null; + return gcv; } public HostColumnVector[] buildHostColumns() { @@ -316,7 +299,6 @@ public void close() { } } - private static DType toRapidsOrNull(DataType type) { if (type instanceof LongType) { return DType.INT64; diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index af514cad50a..416a05a9842 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -45,7 +45,7 @@ object HostColumnarToGpu extends Logging { field.setAccessible(true) val accessor = field.get(arrowCV) val accessFields = accessor.getClass().getSuperclass().getDeclaredFields().toList - val valVecField = foosuper.filter( x => { + val valVecField = accessFields.filter( x => { x.getName.contains("vector") }).head valVecField.setAccessible(true) @@ -57,7 +57,6 @@ object HostColumnarToGpu extends Logging { ab: ai.rapids.cudf.ArrowColumnBuilder, nullable: Boolean, rows: Int): Unit = { - logWarning("Arrow host columnar to gpu cv is type: " + cv.getClass().toString()) val valVector = if (cv.isInstanceOf[ArrowColumnVector]) { logWarning("looking at arrow column vector") getValueVector(cv) @@ -85,12 +84,10 @@ object HostColumnarToGpu extends Logging { logWarning("unsupported op getOffsetBuffer") } ab.addBatch(rows, nullCount, arrowDataAddr, validity, offsets) - logWarning("builder arrow is: " + ab.toString()) } def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, nullable: Boolean, rows: Int): Unit = { - logWarning("Not Arrow host columnar to gpu cv is type: " + cv.getClass().toString()) (cv.dataType(), nullable) match { case (ByteType | BooleanType, true) => for (i <- 0 until rows) { @@ -252,12 +249,9 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], batchBuilder = null } - logWarning(" in init new batch cols is:" + batch.numCols()) // when reading host batches it is essential to read the data immediately and pass to a // builder and we need to determine how many rows to allocate in the builder based on the // schema and desired batch size - - // TODO - batch row limit right for arrow? Do we want to allow splitting or just single? batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, GpuBatchUtils.estimateGpuMemory(schema, 512), 512) @@ -270,28 +264,20 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], } else { false } - if (batch.numRows() > batchRowLimit) { - logWarning(s"batch num rows: ${batch.numRows()} > then row limit: $batchRowLimit") - } if (isArrow) { - logWarning("arrow batch builder") - // todo - REMOVE BATCH ROWLIMIT batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { - logWarning("not an arrow batch builder") batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) } totalRows = 0 } override def addBatchToConcat(batch: ColumnarBatch): Unit = { - logWarning("in add batch") val rows = batch.numRows() for (i <- 0 until batch.numCols()) { batchBuilder.copyColumnar(batch.column(i), i, schema.fields(i).nullable, rows) } - logWarning("done in add batch") totalRows += rows } @@ -300,14 +286,9 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], } override def concatAllAndPutOnGPU(): ColumnarBatch = { - logWarning("concatallandput on gpu") // About to place data back on the GPU GpuSemaphore.acquireIfNecessary(TaskContext.get()) - logWarning("batch builder build total Rows: " + totalRows) - if (batchBuilder == null) { - logWarning("batch builder is null"); - } val ret = batchBuilder.build(totalRows) maxDeviceMemory = GpuColumnVector.getTotalDeviceMemoryUsed(ret) @@ -320,7 +301,6 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], override def cleanupConcatIsDone(): Unit = { if (batchBuilder != null) { - logWarning("cleanup concat done") batchBuilder.close() batchBuilder = null } @@ -398,7 +378,6 @@ case class HostColumnarToGpu(child: SparkPlan, goal: CoalesceGoal) val batches = child.executeColumnar() val confUseArrow = new RapidsConf(child.conf).useArrowCopyOptimization - logWarning(" num partitions is: " + batches.getNumPartitions) batches.mapPartitions { iter => new HostToGpuCoalesceIterator(iter, goal, outputSchema, numInputRows, numInputBatches, numOutputRows, numOutputBatches, collectTime, concatTime, From 95a9e637307010f9b47aaa6c17e7e4d258c25af9 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 13:32:50 -0600 Subject: [PATCH 29/49] cleanup --- .../rapids/AccessibleArrowColumnVector.java | 28 ++++------- .../spark/rapids/GpuCoalesceBatches.scala | 6 --- .../spark/rapids/HostColumnarToGpu.scala | 46 +++++++++---------- .../com/nvidia/spark/rapids/RapidsConf.scala | 4 ++ 4 files changed, 35 insertions(+), 49 deletions(-) diff --git a/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java index 03da24f8488..514f11316af 100644 --- a/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java +++ b/sql-plugin/src/main/java/org/apache/spark/sql/vectorized/rapids/AccessibleArrowColumnVector.java @@ -1,12 +1,11 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at + * Copyright (c) 2021, NVIDIA CORPORATION. * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -20,8 +19,6 @@ import org.apache.arrow.vector.*; import org.apache.arrow.vector.complex.*; import org.apache.arrow.vector.holders.NullableVarCharHolder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.spark.sql.util.ArrowUtils; import org.apache.spark.sql.types.*; @@ -31,12 +28,11 @@ import org.apache.spark.unsafe.types.UTF8String; /** - * A column vector backed by Apache Arrow. Currently calendar interval type and map type are not - * supported. + * A column vector backed by Apache Arrow that adds API to get to the Arrow ValueVector. + * Currently calendar interval type and map type are not supported. + * Original code copied from Spark ArrowColumnVector. */ public final class AccessibleArrowColumnVector extends ColumnVector { - private static final Logger logger = LoggerFactory.getLogger(AccessibleArrowColumnVector.class); - private final AccessibleArrowVectorAccessor accessor; private AccessibleArrowColumnVector[] childColumns; @@ -186,18 +182,12 @@ public AccessibleArrowColumnVector(ValueVector vector) { } private abstract static class AccessibleArrowVectorAccessor { - private static final Logger logger = LoggerFactory.getLogger(AccessibleArrowVectorAccessor.class); - private final ValueVector vector; AccessibleArrowVectorAccessor(ValueVector vector) { this.vector = vector; } - ValueVector getVector() { - return vector; - } - // TODO: should be final after removing ArrayAccessor workaround boolean isNullAt(int rowId) { return vector.isNull(rowId); diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala index 76a3fb6d8e9..20135a7c34c 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala @@ -185,7 +185,6 @@ abstract class AbstractGpuCoalesceIterator( closeOnExcept(iterNext()) { cb => val numRows = cb.numRows() numInputBatches += 1 - logWarning("AbstractGpuCoalesceIterator num input batches " + numInputBatches) numInputRows += numRows if (numRows > 0) { saveOnDeck(cb) @@ -269,14 +268,11 @@ abstract class AbstractGpuCoalesceIterator( val batch = popOnDeck() numRows += batch.numRows() numBytes += getBatchDataSize(batch) - logWarning("add batch since on deck") addBatch(batch) } - logWarning(s"add batch before while rows: $numRows on deck $hasOnDeck") // there is a hard limit of 2^31 rows while (numRows < Int.MaxValue && !hasOnDeck && iterHasNext) { - logWarning(s"add batch inside while rows: $numRows on deck $hasOnDeck") closeOnExcept(iterNext()) { cb => val nextRows = cb.numRows() numInputBatches += 1 @@ -291,7 +287,6 @@ abstract class AbstractGpuCoalesceIterator( val wouldBeRows = numRows + nextRows val wouldBeBytes = numBytes + nextBytes - logWarning("add batch target: " + batchRowLimit + " would be is: " + wouldBeRows) if (wouldBeRows > Int.MaxValue) { if (goal == RequireSingleBatch) { throw new IllegalStateException("A single batch is required for this operation," + @@ -308,7 +303,6 @@ abstract class AbstractGpuCoalesceIterator( // 2GB data total to avoid hitting that error. saveOnDeck(cb) } else { - logWarning("add batch") addBatch(cb) numRows = wouldBeRows numBytes = wouldBeBytes diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 416a05a9842..cd1dabbe96b 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -20,7 +20,6 @@ import java.nio.ByteBuffer import ai.rapids.cudf._ import org.apache.arrow.vector.ValueVector -import org.apache.arrow.vector.types.pojo.ArrowType import org.apache.spark.TaskContext import org.apache.spark.internal.Logging @@ -36,10 +35,10 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { // use reflection to get value vector from ArrowColumnVector - def getValueVector(cv: ColumnVector): ValueVector = { + private def getArrowValueVector(cv: ColumnVector): ValueVector = { val arrowCV = cv.asInstanceOf[ArrowColumnVector] val fields = arrowCV.getClass.getDeclaredFields.toList - val field = fields.filter(x => { + val field = fields.filter( x => { x.getName.contains("accessor") }).head field.setAccessible(true) @@ -59,7 +58,7 @@ object HostColumnarToGpu extends Logging { rows: Int): Unit = { val valVector = if (cv.isInstanceOf[ArrowColumnVector]) { logWarning("looking at arrow column vector") - getValueVector(cv) + getArrowValueVector(cv) } else if (cv.isInstanceOf[AccessibleArrowColumnVector]) { logWarning("looking at accessible arrow column vector") val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] @@ -249,27 +248,26 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], batchBuilder = null } - // when reading host batches it is essential to read the data immediately and pass to a - // builder and we need to determine how many rows to allocate in the builder based on the - // schema and desired batch size - batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, - GpuBatchUtils.estimateGpuMemory(schema, 512), 512) - - // if no columns then probably a count operation so doesn't matter which builder we use - // val isArrow = if (batch.numCols() > 0 && batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { - val isArrow = if (useArrowCopyOpt && batch.numCols() > 0 && - (batch.column(0).isInstanceOf[ArrowColumnVector] || - batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { + // when reading host batches it is essential to read the data immediately and pass to a + // builder and we need to determine how many rows to allocate in the builder based on the + // schema and desired batch size + batchRowLimit = GpuBatchUtils.estimateRowCount(goal.targetSizeBytes, + GpuBatchUtils.estimateGpuMemory(schema, 512), 512) + + // if no columns then probably a count operation so doesn't matter which builder we use + // val isArrow = if (batch.numCols() > 0 && batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { + val isArrow = if (useArrowCopyOpt && batch.numCols() > 0 && + (batch.column(0).isInstanceOf[ArrowColumnVector] || + batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { true; - } else { - false - } - if (isArrow) { - batchBuilder = - new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) - } else { - batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) - } + } else { + false + } + if (isArrow) { + batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) + } else { + batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) + } totalRows = 0 } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala index 5f5d35e4ec7..006c2f68399 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/RapidsConf.scala @@ -821,6 +821,10 @@ object RapidsConf { .createWithDefault(true) val USE_ARROW_OPT = conf("spark.rapids.arrowCopyOptmizationEnabled") + .doc("Option to turn off using the optimized Arrow copy code when reading from " + + "ArrowColumnVector in HostColumnarToGpu. Left as internal as user shouldn't " + + "have to turn it off, but its convenient for testing.") + .internal() .booleanConf .createWithDefault(true) From 5527531cd5ecda0a5fc841584b957e23127c1924 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 13:36:04 -0600 Subject: [PATCH 30/49] formatting --- .../scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index cd1dabbe96b..dfa8378446a 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -255,11 +255,10 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], GpuBatchUtils.estimateGpuMemory(schema, 512), 512) // if no columns then probably a count operation so doesn't matter which builder we use - // val isArrow = if (batch.numCols() > 0 && batch.column(0).isInstanceOf[AccessibleArrowColumnVector]) { val isArrow = if (useArrowCopyOpt && batch.numCols() > 0 && - (batch.column(0).isInstanceOf[ArrowColumnVector] || - batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { - true; + (batch.column(0).isInstanceOf[ArrowColumnVector] || + batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { + true } else { false } From ce1d7b0b3d18d41c1d57b6e9b2a8ae5c67f6fa9f Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 13:38:11 -0600 Subject: [PATCH 31/49] formatting --- .../com/nvidia/spark/rapids/HostColumnarToGpu.scala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index dfa8378446a..0be455e673f 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -255,14 +255,9 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], GpuBatchUtils.estimateGpuMemory(schema, 512), 512) // if no columns then probably a count operation so doesn't matter which builder we use - val isArrow = if (useArrowCopyOpt && batch.numCols() > 0 && - (batch.column(0).isInstanceOf[ArrowColumnVector] || - batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { - true - } else { - false - } - if (isArrow) { + if (useArrowCopyOpt && batch.numCols() > 0 && + (batch.column(0).isInstanceOf[ArrowColumnVector] || + batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) From 393ff6f3889add81ca4517a1a89d0725705730b8 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 13:46:18 -0600 Subject: [PATCH 32/49] expand comment --- .../main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 0be455e673f..03deb00311c 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -255,6 +255,8 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], GpuBatchUtils.estimateGpuMemory(schema, 512), 512) // if no columns then probably a count operation so doesn't matter which builder we use + // as we won't actually copy any data and we can't tell what type of data it is without + // having a column if (useArrowCopyOpt && batch.numCols() > 0 && (batch.column(0).isInstanceOf[ArrowColumnVector] || batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { From 1572afcceb7846ab4b048a780de9460d725f1293 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 15:59:03 -0600 Subject: [PATCH 33/49] move getting Arrow vectors into shims since Arrow versions changed between spark versions --- .../rapids/shims/spark300/Spark300Shims.scala | 15 +++++++++++++++ .../rapids/shims/spark311/Spark311Shims.scala | 16 ++++++++++++++++ .../nvidia/spark/rapids/HostColumnarToGpu.scala | 10 +++------- .../com/nvidia/spark/rapids/SparkShims.scala | 8 ++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala index 3cb5a7f9481..3a486148e13 100644 --- a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala +++ b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala @@ -16,10 +16,12 @@ package com.nvidia.spark.rapids.shims.spark300 +import java.nio.ByteBuffer import java.time.ZoneId import com.nvidia.spark.rapids._ import com.nvidia.spark.rapids.spark300.RapidsShuffleManager +import org.apache.arrow.vector.ValueVector import org.apache.spark.SparkEnv import org.apache.spark.rdd.RDD @@ -440,4 +442,17 @@ class Spark300Shims extends SparkShims { override def shouldIgnorePath(path: String): Boolean = { InMemoryFileIndex.shouldFilterOut(path) } + + // Arrow version changed between Spark versions + override def getArrowDataBuf(vec: ValueVector): ByteBuffer = { + vec.getDataBuffer.nioBuffer() + } + + override def getArrowValidityBuf(vec: ValueVector): ByteBuffer = { + vec.getValidityBuffer.nioBuffer() + } + + override def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer = { + vec.getOffsetBuffer.nioBuffer() + } } diff --git a/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala b/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala index a3945fcbefa..1b4b9103f7f 100644 --- a/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala +++ b/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala @@ -16,9 +16,12 @@ package com.nvidia.spark.rapids.shims.spark311 +import java.nio.ByteBuffer + import com.nvidia.spark.rapids._ import com.nvidia.spark.rapids.shims.spark301.Spark301Shims import com.nvidia.spark.rapids.spark311.RapidsShuffleManager +import org.apache.arrow.vector.ValueVector import org.apache.spark.SparkEnv import org.apache.spark.sql.SparkSession @@ -380,4 +383,17 @@ class Spark311Shims extends Spark301Shims { override def shouldIgnorePath(path: String): Boolean = { HadoopFSUtilsShim.shouldIgnorePath(path) } + + // Arrow version changed between Spark versions + override def getArrowDataBuf(vec: ValueVector): ByteBuffer = { + vec.getDataBuffer.nioBuffer() + } + + override def getArrowValidityBuf(vec: ValueVector): ByteBuffer = { + vec.getValidityBuffer.nioBuffer() + } + + override def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer = { + vec.getOffsetBuffer.nioBuffer() + } } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 03deb00311c..bdbae36bba2 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -18,7 +18,6 @@ package com.nvidia.spark.rapids import java.nio.ByteBuffer -import ai.rapids.cudf._ import org.apache.arrow.vector.ValueVector import org.apache.spark.TaskContext @@ -67,17 +66,14 @@ object HostColumnarToGpu extends Logging { throw new Exception("not arrow data shouldn't be here!") } val arrowDataAddr = valVector.getDataBuffer.nioBuffer() - val byteBuf = valVector.getDataBuffer.nioBuffer() + val byteBuf = ShimLoader.getSparkShims.getArrowDataBuf(valVector) val nullCount = valVector.getNullCount() - val validity = valVector.getValidityBuffer.nioBuffer() + val validity = ShimLoader.getSparkShims.getArrowValidityBuf(valVector) var offsets:ByteBuffer = null try { // TODO - should we chekc types first instead? - val arrowDataOffsetBuf = valVector.getOffsetBuffer - if (arrowDataOffsetBuf != null) { - offsets = arrowDataOffsetBuf.nioBuffer() - } + val arrowDataOffsetBuf = ShimLoader.getSparkShims.getArrowOffsetsBuf(valVector) } catch { case e: UnsupportedOperationException => logWarning("unsupported op getOffsetBuffer") diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala index 1fbc558573f..e9f8dca4088 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala @@ -16,6 +16,10 @@ package com.nvidia.spark.rapids +import java.nio.ByteBuffer + +import org.apache.arrow.vector.ValueVector + import org.apache.spark.rdd.RDD import org.apache.spark.sql.{SparkSession, SparkSessionExtensions} import org.apache.spark.sql.catalyst.InternalRow @@ -165,4 +169,8 @@ trait SparkShims { explicitMetadata: Option[Metadata] = None): Alias def shouldIgnorePath(path: String): Boolean + + def getArrowDataBuf(vec: ValueVector): ByteBuffer + def getArrowValidityBuf(vec: ValueVector): ByteBuffer + def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer } From d1ba0a390e2745255587d250b91af016032fa7a0 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 16:11:34 -0600 Subject: [PATCH 34/49] Revert "move getting Arrow vectors into shims since Arrow versions changed between spark versions" This reverts commit 1572afcceb7846ab4b048a780de9460d725f1293. --- .../rapids/shims/spark300/Spark300Shims.scala | 15 --------------- .../rapids/shims/spark311/Spark311Shims.scala | 16 ---------------- .../nvidia/spark/rapids/HostColumnarToGpu.scala | 10 +++++++--- .../com/nvidia/spark/rapids/SparkShims.scala | 8 -------- 4 files changed, 7 insertions(+), 42 deletions(-) diff --git a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala index 3a486148e13..3cb5a7f9481 100644 --- a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala +++ b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala @@ -16,12 +16,10 @@ package com.nvidia.spark.rapids.shims.spark300 -import java.nio.ByteBuffer import java.time.ZoneId import com.nvidia.spark.rapids._ import com.nvidia.spark.rapids.spark300.RapidsShuffleManager -import org.apache.arrow.vector.ValueVector import org.apache.spark.SparkEnv import org.apache.spark.rdd.RDD @@ -442,17 +440,4 @@ class Spark300Shims extends SparkShims { override def shouldIgnorePath(path: String): Boolean = { InMemoryFileIndex.shouldFilterOut(path) } - - // Arrow version changed between Spark versions - override def getArrowDataBuf(vec: ValueVector): ByteBuffer = { - vec.getDataBuffer.nioBuffer() - } - - override def getArrowValidityBuf(vec: ValueVector): ByteBuffer = { - vec.getValidityBuffer.nioBuffer() - } - - override def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer = { - vec.getOffsetBuffer.nioBuffer() - } } diff --git a/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala b/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala index 1b4b9103f7f..a3945fcbefa 100644 --- a/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala +++ b/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala @@ -16,12 +16,9 @@ package com.nvidia.spark.rapids.shims.spark311 -import java.nio.ByteBuffer - import com.nvidia.spark.rapids._ import com.nvidia.spark.rapids.shims.spark301.Spark301Shims import com.nvidia.spark.rapids.spark311.RapidsShuffleManager -import org.apache.arrow.vector.ValueVector import org.apache.spark.SparkEnv import org.apache.spark.sql.SparkSession @@ -383,17 +380,4 @@ class Spark311Shims extends Spark301Shims { override def shouldIgnorePath(path: String): Boolean = { HadoopFSUtilsShim.shouldIgnorePath(path) } - - // Arrow version changed between Spark versions - override def getArrowDataBuf(vec: ValueVector): ByteBuffer = { - vec.getDataBuffer.nioBuffer() - } - - override def getArrowValidityBuf(vec: ValueVector): ByteBuffer = { - vec.getValidityBuffer.nioBuffer() - } - - override def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer = { - vec.getOffsetBuffer.nioBuffer() - } } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index bdbae36bba2..03deb00311c 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -18,6 +18,7 @@ package com.nvidia.spark.rapids import java.nio.ByteBuffer +import ai.rapids.cudf._ import org.apache.arrow.vector.ValueVector import org.apache.spark.TaskContext @@ -66,14 +67,17 @@ object HostColumnarToGpu extends Logging { throw new Exception("not arrow data shouldn't be here!") } val arrowDataAddr = valVector.getDataBuffer.nioBuffer() - val byteBuf = ShimLoader.getSparkShims.getArrowDataBuf(valVector) + val byteBuf = valVector.getDataBuffer.nioBuffer() val nullCount = valVector.getNullCount() - val validity = ShimLoader.getSparkShims.getArrowValidityBuf(valVector) + val validity = valVector.getValidityBuffer.nioBuffer() var offsets:ByteBuffer = null try { // TODO - should we chekc types first instead? - val arrowDataOffsetBuf = ShimLoader.getSparkShims.getArrowOffsetsBuf(valVector) + val arrowDataOffsetBuf = valVector.getOffsetBuffer + if (arrowDataOffsetBuf != null) { + offsets = arrowDataOffsetBuf.nioBuffer() + } } catch { case e: UnsupportedOperationException => logWarning("unsupported op getOffsetBuffer") diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala index e9f8dca4088..1fbc558573f 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala @@ -16,10 +16,6 @@ package com.nvidia.spark.rapids -import java.nio.ByteBuffer - -import org.apache.arrow.vector.ValueVector - import org.apache.spark.rdd.RDD import org.apache.spark.sql.{SparkSession, SparkSessionExtensions} import org.apache.spark.sql.catalyst.InternalRow @@ -169,8 +165,4 @@ trait SparkShims { explicitMetadata: Option[Metadata] = None): Alias def shouldIgnorePath(path: String): Boolean - - def getArrowDataBuf(vec: ValueVector): ByteBuffer - def getArrowValidityBuf(vec: ValueVector): ByteBuffer - def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer } From c4c1f616eecbea7cb5db82a5eefd547dd95607e3 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 16:19:09 -0600 Subject: [PATCH 35/49] shims working --- .../rapids/shims/spark300/Spark300Shims.scala | 15 +++++++++++++++ .../rapids/shims/spark311/Spark311Shims.scala | 16 ++++++++++++++++ .../nvidia/spark/rapids/HostColumnarToGpu.scala | 16 +++++++--------- .../com/nvidia/spark/rapids/SparkShims.scala | 8 ++++++++ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala index 3cb5a7f9481..5ba4d2da15b 100644 --- a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala +++ b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala @@ -16,10 +16,12 @@ package com.nvidia.spark.rapids.shims.spark300 +import java.nio.ByteBuffer import java.time.ZoneId import com.nvidia.spark.rapids._ import com.nvidia.spark.rapids.spark300.RapidsShuffleManager +import org.apache.arrow.vector.ValueVector import org.apache.spark.SparkEnv import org.apache.spark.rdd.RDD @@ -440,4 +442,17 @@ class Spark300Shims extends SparkShims { override def shouldIgnorePath(path: String): Boolean = { InMemoryFileIndex.shouldFilterOut(path) } + + // Arrow version changed between Spark versions + override def getArrowDataBuf(vec: ValueVector): ByteBuffer = { + vec.getDataBuffer().nioBuffer() + } + + override def getArrowValidityBuf(vec: ValueVector): ByteBuffer = { + vec.getValidityBuffer().nioBuffer() + } + + override def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer = { + vec.getOffsetBuffer().nioBuffer() + } } diff --git a/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala b/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala index a3945fcbefa..1b4b9103f7f 100644 --- a/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala +++ b/shims/spark311/src/main/scala/com/nvidia/spark/rapids/shims/spark311/Spark311Shims.scala @@ -16,9 +16,12 @@ package com.nvidia.spark.rapids.shims.spark311 +import java.nio.ByteBuffer + import com.nvidia.spark.rapids._ import com.nvidia.spark.rapids.shims.spark301.Spark301Shims import com.nvidia.spark.rapids.spark311.RapidsShuffleManager +import org.apache.arrow.vector.ValueVector import org.apache.spark.SparkEnv import org.apache.spark.sql.SparkSession @@ -380,4 +383,17 @@ class Spark311Shims extends Spark301Shims { override def shouldIgnorePath(path: String): Boolean = { HadoopFSUtilsShim.shouldIgnorePath(path) } + + // Arrow version changed between Spark versions + override def getArrowDataBuf(vec: ValueVector): ByteBuffer = { + vec.getDataBuffer.nioBuffer() + } + + override def getArrowValidityBuf(vec: ValueVector): ByteBuffer = { + vec.getValidityBuffer.nioBuffer() + } + + override def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer = { + vec.getOffsetBuffer.nioBuffer() + } } diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 03deb00311c..8773ba42971 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -18,7 +18,6 @@ package com.nvidia.spark.rapids import java.nio.ByteBuffer -import ai.rapids.cudf._ import org.apache.arrow.vector.ValueVector import org.apache.spark.TaskContext @@ -66,23 +65,22 @@ object HostColumnarToGpu extends Logging { } else { throw new Exception("not arrow data shouldn't be here!") } - val arrowDataAddr = valVector.getDataBuffer.nioBuffer() - val byteBuf = valVector.getDataBuffer.nioBuffer() + // val dataBuf = valVector.getDataBuffer.nioBuffer() val nullCount = valVector.getNullCount() - val validity = valVector.getValidityBuffer.nioBuffer() + // val validity = valVector.getValidityBuffer.nioBuffer() + val dataBuf = ShimLoader.getSparkShims.getArrowDataBuf(valVector) + val validity = ShimLoader.getSparkShims.getArrowValidityBuf(valVector) var offsets:ByteBuffer = null try { // TODO - should we chekc types first instead? - val arrowDataOffsetBuf = valVector.getOffsetBuffer - if (arrowDataOffsetBuf != null) { - offsets = arrowDataOffsetBuf.nioBuffer() - } + offsets = ShimLoader.getSparkShims.getArrowOffsetsBuf(valVector) } catch { case e: UnsupportedOperationException => logWarning("unsupported op getOffsetBuffer") } - ab.addBatch(rows, nullCount, arrowDataAddr, validity, offsets) + logWarning(s"data is $dataBuf valid: $validity offsets: $offsets") + ab.addBatch(rows, nullCount, dataBuf, validity, offsets) } def columnarCopy(cv: ColumnVector, b: ai.rapids.cudf.HostColumnVector.ColumnBuilder, diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala index 1fbc558573f..e9f8dca4088 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala @@ -16,6 +16,10 @@ package com.nvidia.spark.rapids +import java.nio.ByteBuffer + +import org.apache.arrow.vector.ValueVector + import org.apache.spark.rdd.RDD import org.apache.spark.sql.{SparkSession, SparkSessionExtensions} import org.apache.spark.sql.catalyst.InternalRow @@ -165,4 +169,8 @@ trait SparkShims { explicitMetadata: Option[Metadata] = None): Alias def shouldIgnorePath(path: String): Boolean + + def getArrowDataBuf(vec: ValueVector): ByteBuffer + def getArrowValidityBuf(vec: ValueVector): ByteBuffer + def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer } From 4e19741f1ef78dcffac5805a34ea56b13f60b7e5 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 19:31:33 -0600 Subject: [PATCH 36/49] fix test and check types --- .../src/main/python/datasourcev2_read.py | 17 +++++++++++++++-- .../spark/rapids/HostColumnarToGpu.scala | 18 +++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index cf92bf0b1c9..adf15acb0ae 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -49,8 +49,11 @@ def readTable(csvPath, tableToRead): .orderBy("name", "age") def createDatabase(spark): - spark.sql("create database IF NOT EXISTS " + catalogName) - spark.sql("use " + catalogName) + try: + spark.sql("create database IF NOT EXISTS " + catalogName) + spark.sql("use " + catalogName) + except Exception: + pytest.skip("Failed to load catalog for datasource v2 {}, jar is probably missing".format(columnarClass)) def cleanupDatabase(spark): spark.sql("drop table IF EXISTS " + tableName) @@ -80,3 +83,13 @@ def test_read_round_trip_no_partitioned(std_input_path, csv): conf={'spark.sql.catalog.columnar': columnarClass}) assert_gpu_and_cpu_are_equal_collect(readTable(csvPath, columnarTableNameNoPart), conf={'spark.sql.catalog.columnar': columnarClass}) + +@pytest.mark.parametrize('csv', ['people.csv']) +def test_read_round_trip_no_partitioned_arrow_off(std_input_path, csv): + csvPath = std_input_path + "/" + csv + with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, csvPath), + conf={'spark.sql.catalog.columnar': columnarClass, + 'spark.rapids.arrowCopyOptmizationEnabled': 'false'}) + assert_gpu_and_cpu_are_equal_collect(readTable(csvPath, columnarTableNameNoPart), + conf={'spark.sql.catalog.columnar': columnarClass, + 'spark.rapids.arrowCopyOptmizationEnabled': 'false'}) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 8773ba42971..fbbe76cfe23 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -56,10 +56,8 @@ object HostColumnarToGpu extends Logging { nullable: Boolean, rows: Int): Unit = { val valVector = if (cv.isInstanceOf[ArrowColumnVector]) { - logWarning("looking at arrow column vector") getArrowValueVector(cv) } else if (cv.isInstanceOf[AccessibleArrowColumnVector]) { - logWarning("looking at accessible arrow column vector") val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] arrowVec.getArrowValueVector() } else { @@ -72,14 +70,14 @@ object HostColumnarToGpu extends Logging { val validity = ShimLoader.getSparkShims.getArrowValidityBuf(valVector) var offsets:ByteBuffer = null + // this is a bit ugly, not all Arrow types need this so try and + // just catch it try { - // TODO - should we chekc types first instead? offsets = ShimLoader.getSparkShims.getArrowOffsetsBuf(valVector) } catch { case e: UnsupportedOperationException => - logWarning("unsupported op getOffsetBuffer") + // swallow the exception and assume no offsets buffer } - logWarning(s"data is $dataBuf valid: $validity offsets: $offsets") ab.addBatch(rows, nullCount, dataBuf, validity, offsets) } @@ -236,6 +234,13 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], var totalRows = 0 var maxDeviceMemory: Long = 0 + // the arrow cudf converter only supports primitive types and strings + // decimals and nested types aren't supported yet + private def arrowTypesSupported(schema: StructType): Boolean = { + val dataTypes = schema.fields.map(_.dataType) + dataTypes.forall(GpuOverrides.isSupportedType(_)) + } + /** * Initialize the builders using an estimated row count based on the schema and the desired * batch size defined by [[RapidsConf.GPU_BATCH_SIZE_BYTES]]. @@ -256,10 +261,13 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], // as we won't actually copy any data and we can't tell what type of data it is without // having a column if (useArrowCopyOpt && batch.numCols() > 0 && + arrowTypesSupported(schema) && (batch.column(0).isInstanceOf[ArrowColumnVector] || batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { + logWarning("Using GpuArrowColumnarBatchBuilder") batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { + logWarning("Using GpuColumnarBatchBuilder") batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) } totalRows = 0 From 624abbcff88747f5afc6f4876d50cf40e2891cef Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 19:46:03 -0600 Subject: [PATCH 37/49] Add test for host iterator --- .../rapids/GpuCoalesceBatchesSuite.scala | 81 ++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala index 455c37ba8e3..a55399ed985 100644 --- a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala +++ b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala @@ -21,11 +21,15 @@ import java.nio.file.Files import ai.rapids.cudf.{ContiguousTable, Cuda, HostColumnVector, Table} import com.nvidia.spark.rapids.format.CodecType +import org.apache.arrow.memory.RootAllocator +import org.apache.arrow.vector.IntVector +import org.apache.arrow.vector.complex.MapVector +import org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType} import org.apache.spark.sql.execution.metric.SQLMetric import org.apache.spark.sql.rapids.metrics.source.MockTaskContext -import org.apache.spark.sql.types.{DataType, DataTypes, DecimalType, LongType, StructField, StructType} -import org.apache.spark.sql.vectorized.ColumnarBatch +import org.apache.spark.sql.types._ +import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch} class GpuCoalesceBatchesSuite extends SparkQueryCompareTestSuite { @@ -94,6 +98,79 @@ class GpuCoalesceBatchesSuite extends SparkQueryCompareTestSuite { }, conf) } + /** Maps field from Spark to Arrow. NOTE: timeZoneId required for TimestampType */ + private def toArrowField( + name: String, dt: DataType, nullable: Boolean, timeZoneId: String): Field = { + dt match { + case ArrayType(elementType, containsNull) => + val fieldType = new FieldType(nullable, ArrowType.List.INSTANCE, null) + new Field(name, fieldType, + Seq(toArrowField("element", elementType, containsNull, timeZoneId)).asJava) + case StructType(fields) => + val fieldType = new FieldType(nullable, ArrowType.Struct.INSTANCE, null) + new Field(name, fieldType, + fields.map { field => + toArrowField(field.name, field.dataType, field.nullable, timeZoneId) + }.toSeq.asJava) + case MapType(keyType, valueType, valueContainsNull) => + val mapType = new FieldType(nullable, new ArrowType.Map(false), null) + // Note: Map Type struct can not be null, Struct Type key field can not be null + new Field(name, mapType, + Seq(toArrowField(MapVector.DATA_VECTOR_NAME, + new StructType() + .add(MapVector.KEY_NAME, keyType, nullable = false) + .add(MapVector.VALUE_NAME, valueType, nullable = valueContainsNull), + nullable = false, + timeZoneId)).asJava) + case dataType => + val fieldType = new FieldType(nullable, toArrowType(dataType, timeZoneId), null) + new Field(name, fieldType, Seq.empty[Field].asJava) + } + } + + test("test HostToGpuCoalesceIterator with arrow") { + val rootAllocator = new RootAllocator(Long.MaxValue) + val allocator = rootAllocator.newChildAllocator("int", 0, Long.MaxValue) + val vector1 = toArrowField("int1", IntegerType, nullable = true, null) + .createVector(allocator).asInstanceOf[IntVector] + vector1.allocateNew() + val vector2 = toArrowField("int2", IntegerType, nullable = true, null) + .createVector(allocator).asInstanceOf[IntVector] + vector2.allocateNew() + + (0 until 10).foreach { i => + vector1.setSafe(i, i) + vector2.setSafe(i + 1, i) + } + vector1.setNull(10) + vector1.setValueCount(11) + vector2.setNull(0) + vector2.setValueCount(11) + + val columnVectors = Seq(new ArrowColumnVector(vector1), new ArrowColumnVector(vector2)) + val schema = StructType(Seq(StructField("int1", IntegerType), StructField("int2", IntegerType))) + val batch = new ColumnarBatch(columnVectors.toArray) + val iter = Iterator.single(batch) + + val hostToGpuCoalesceIterator = new HostToGpuCoalesceIterator(iter, + TargetSize(1024), + schema: StructType, + new SQLMetric("t1", 0), + new SQLMetric("t2", 0), + new SQLMetric("t3", 0), + new SQLMetric("t4", 0), + new SQLMetric("t5", 0), + new SQLMetric("t6", 0), + new SQLMetric("t7", 0), + new SQLMetric("t8", 0), + "testcoalesce", + useArrowCopyOpt = true) + + hostToGpuCoalesceIterator.initNewBatch(batch) + assert(hostToGpuCoalesceIterator.batchBuilder. + isInstanceOf[GpuColumnVector.GpuColumnarBatchBuilder]) + } + test("coalesce HostColumnarToGpu") { val conf = makeBatchedBytes(1) From 765d36ac36dc166da294d3b2ac02f50142ee3f13 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 20:26:18 -0600 Subject: [PATCH 38/49] working tests --- .../rapids/GpuCoalesceBatchesSuite.scala | 111 ++++++++++++++++-- 1 file changed, 99 insertions(+), 12 deletions(-) diff --git a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala index a55399ed985..58502bcec9f 100644 --- a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala +++ b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala @@ -16,6 +16,8 @@ package com.nvidia.spark.rapids +import scala.collection.JavaConverters._ + import java.io.File import java.nio.file.Files @@ -23,7 +25,9 @@ import ai.rapids.cudf.{ContiguousTable, Cuda, HostColumnVector, Table} import com.nvidia.spark.rapids.format.CodecType import org.apache.arrow.memory.RootAllocator import org.apache.arrow.vector.IntVector +import org.apache.arrow.vector.complex.ListVector import org.apache.arrow.vector.complex.MapVector +import org.apache.arrow.vector.types.{DateUnit, FloatingPointPrecision, TimeUnit} import org.apache.arrow.vector.types.pojo.{ArrowType, Field, FieldType} import org.apache.spark.sql.execution.metric.SQLMetric @@ -98,6 +102,30 @@ class GpuCoalesceBatchesSuite extends SparkQueryCompareTestSuite { }, conf) } + /** Maps data type from Spark to Arrow. NOTE: timeZoneId required for TimestampTypes */ + private def toArrowType(dt: DataType, timeZoneId: String): ArrowType = dt match { + case BooleanType => ArrowType.Bool.INSTANCE + case ByteType => new ArrowType.Int(8, true) + case ShortType => new ArrowType.Int(8 * 2, true) + case IntegerType => new ArrowType.Int(8 * 4, true) + case LongType => new ArrowType.Int(8 * 8, true) + case FloatType => new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE) + case DoubleType => new ArrowType.FloatingPoint(FloatingPointPrecision.DOUBLE) + case StringType => ArrowType.Utf8.INSTANCE + case BinaryType => ArrowType.Binary.INSTANCE + // case DecimalType.Fixed(precision, scale) => new ArrowType.Decimal(precision, scale) + case DateType => new ArrowType.Date(DateUnit.DAY) + case TimestampType => + if (timeZoneId == null) { + throw new UnsupportedOperationException( + s"${TimestampType.catalogString} must supply timeZoneId parameter") + } else { + new ArrowType.Timestamp(TimeUnit.MICROSECOND, timeZoneId) + } + case _ => + throw new UnsupportedOperationException(s"Unsupported data type: ${dt.catalogString}") + } + /** Maps field from Spark to Arrow. NOTE: timeZoneId required for TimestampType */ private def toArrowField( name: String, dt: DataType, nullable: Boolean, timeZoneId: String): Field = { @@ -128,7 +156,7 @@ class GpuCoalesceBatchesSuite extends SparkQueryCompareTestSuite { } } - test("test HostToGpuCoalesceIterator with arrow") { + private def setupArrowBatch(withArrayType:Boolean = false): (ColumnarBatch, StructType) = { val rootAllocator = new RootAllocator(Long.MaxValue) val allocator = rootAllocator.newChildAllocator("int", 0, Long.MaxValue) val vector1 = toArrowField("int1", IntegerType, nullable = true, null) @@ -137,35 +165,94 @@ class GpuCoalesceBatchesSuite extends SparkQueryCompareTestSuite { val vector2 = toArrowField("int2", IntegerType, nullable = true, null) .createVector(allocator).asInstanceOf[IntVector] vector2.allocateNew() + val vector3 = toArrowField("array", ArrayType(IntegerType), nullable = true, null) + .createVector(allocator).asInstanceOf[ListVector] + vector3.allocateNew() + val elementVector = vector3.getDataVector().asInstanceOf[IntVector] (0 until 10).foreach { i => vector1.setSafe(i, i) vector2.setSafe(i + 1, i) + elementVector.setSafe(i, i) + vector3.startNewValue(i) + elementVector.setSafe(0, 1) + elementVector.setSafe(1, 2) + vector3.endValue(i, 2) } + elementVector.setValueCount(22) + vector3.setValueCount(11) + vector1.setNull(10) vector1.setValueCount(11) vector2.setNull(0) vector2.setValueCount(11) - val columnVectors = Seq(new ArrowColumnVector(vector1), new ArrowColumnVector(vector2)) - val schema = StructType(Seq(StructField("int1", IntegerType), StructField("int2", IntegerType))) - val batch = new ColumnarBatch(columnVectors.toArray) + val baseVectors = Seq(new ArrowColumnVector(vector1), new ArrowColumnVector(vector2)) + val columnVectors = if (withArrayType) { + baseVectors :+ new ArrowColumnVector(vector3) + } else { + baseVectors + } + val schemaBase = Seq(StructField("int1", IntegerType), StructField("int2", IntegerType)) + val schemaSeq = if (withArrayType) { + schemaBase :+ StructField("array", ArrayType(IntegerType)) + } else { + schemaBase + } + val schema = StructType(schemaSeq) + (new ColumnarBatch(columnVectors.toArray), schema) + } + + test("test HostToGpuCoalesceIterator with arrow valid") { + val (batch, schema) = setupArrowBatch(false) + val iter = Iterator.single(batch) + + val hostToGpuCoalesceIterator = new HostToGpuCoalesceIterator(iter, + TargetSize(1024), + schema: StructType, + new SQLMetric("t1", 0), new SQLMetric("t2", 0), new SQLMetric("t3", 0), + new SQLMetric("t4", 0), new SQLMetric("t5", 0), new SQLMetric("t6", 0), + new SQLMetric("t7", 0), new SQLMetric("t8", 0), + "testcoalesce", + useArrowCopyOpt = true) + + hostToGpuCoalesceIterator.initNewBatch(batch) + assert(hostToGpuCoalesceIterator.batchBuilder. + isInstanceOf[GpuColumnVector.GpuArrowColumnarBatchBuilder]) + } + + test("test HostToGpuCoalesceIterator with arrow array") { + val (batch, schema) = setupArrowBatch(true) val iter = Iterator.single(batch) val hostToGpuCoalesceIterator = new HostToGpuCoalesceIterator(iter, TargetSize(1024), schema: StructType, - new SQLMetric("t1", 0), - new SQLMetric("t2", 0), - new SQLMetric("t3", 0), - new SQLMetric("t4", 0), - new SQLMetric("t5", 0), - new SQLMetric("t6", 0), - new SQLMetric("t7", 0), - new SQLMetric("t8", 0), + new SQLMetric("t1", 0), new SQLMetric("t2", 0), new SQLMetric("t3", 0), + new SQLMetric("t4", 0), new SQLMetric("t5", 0), new SQLMetric("t6", 0), + new SQLMetric("t7", 0), new SQLMetric("t8", 0), "testcoalesce", useArrowCopyOpt = true) + hostToGpuCoalesceIterator.initNewBatch(batch) + // array isn't supported should fall back + assert(hostToGpuCoalesceIterator.batchBuilder. + isInstanceOf[GpuColumnVector.GpuColumnarBatchBuilder]) + } + + test("test HostToGpuCoalesceIterator with arrow config off") { + val (batch, schema) = setupArrowBatch() + val iter = Iterator.single(batch) + + val hostToGpuCoalesceIterator = new HostToGpuCoalesceIterator(iter, + TargetSize(1024), + schema: StructType, + new SQLMetric("t1", 0), new SQLMetric("t2", 0), new SQLMetric("t3", 0), + new SQLMetric("t4", 0), new SQLMetric("t5", 0), new SQLMetric("t6", 0), + new SQLMetric("t7", 0), new SQLMetric("t8", 0), + "testcoalesce", + useArrowCopyOpt = false) + hostToGpuCoalesceIterator.initNewBatch(batch) assert(hostToGpuCoalesceIterator.batchBuilder. isInstanceOf[GpuColumnVector.GpuColumnarBatchBuilder]) From b24d3d52b1cda72af5b48ddc980ca3187b90efa8 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 20:59:27 -0600 Subject: [PATCH 39/49] fix style --- .../com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala index 58502bcec9f..0596194312b 100644 --- a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala +++ b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala @@ -16,11 +16,11 @@ package com.nvidia.spark.rapids -import scala.collection.JavaConverters._ - import java.io.File import java.nio.file.Files +import scala.collection.JavaConverters._ + import ai.rapids.cudf.{ContiguousTable, Cuda, HostColumnVector, Table} import com.nvidia.spark.rapids.format.CodecType import org.apache.arrow.memory.RootAllocator From 11dc637d88219d96c84d946a63178a1c352d3b43 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 21:03:26 -0600 Subject: [PATCH 40/49] cleanup --- .../com/nvidia/spark/rapids/HostColumnarToGpu.scala | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index fbbe76cfe23..b29b7c16e92 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -63,15 +63,11 @@ object HostColumnarToGpu extends Logging { } else { throw new Exception("not arrow data shouldn't be here!") } - // val dataBuf = valVector.getDataBuffer.nioBuffer() val nullCount = valVector.getNullCount() - // val validity = valVector.getValidityBuffer.nioBuffer() val dataBuf = ShimLoader.getSparkShims.getArrowDataBuf(valVector) val validity = ShimLoader.getSparkShims.getArrowValidityBuf(valVector) - var offsets:ByteBuffer = null - // this is a bit ugly, not all Arrow types need this so try and - // just catch it + // this is a bit ugly, not all Arrow types need this so try and just catch it try { offsets = ShimLoader.getSparkShims.getArrowOffsetsBuf(valVector) } catch { @@ -264,10 +260,10 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], arrowTypesSupported(schema) && (batch.column(0).isInstanceOf[ArrowColumnVector] || batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { - logWarning("Using GpuArrowColumnarBatchBuilder") + logInfo("Using GpuArrowColumnarBatchBuilder") batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { - logWarning("Using GpuColumnarBatchBuilder") + logInfo("Using GpuColumnarBatchBuilder") batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) } totalRows = 0 From 5dd07bc057cb6b3e24fada19ac8bd023e0e6950c Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 21:06:08 -0600 Subject: [PATCH 41/49] comment test --- .../src/main/python/datasourcev2_read.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index adf15acb0ae..bbc6b4e4a83 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -18,6 +18,18 @@ from pyspark.sql.types import * from spark_session import with_cpu_session +# This test requires a datasource v2 jar containing the class +# org.apache.spark.sql.connector.InMemoryTableCatalog +# which returns ArrowColumnVectors be specified in order for it to run. +# If that class is not present it skips the tests. + +catalogName = "columnar" +tableName = "people" +tableNameNoPart = "peoplenopart" +columnarTableName = catalogName + "." + tableName +columnarTableNameNoPart = catalogName + "." + tableNameNoPart +columnarClass = 'org.apache.spark.sql.connector.InMemoryTableCatalog' + def createPeopleCSVDf(spark, peopleCSVLocation): return spark.read.format("csv")\ .option("header", "false")\ @@ -27,13 +39,6 @@ def createPeopleCSVDf(spark, peopleCSVLocation): .withColumnRenamed("_c1", "age")\ .withColumnRenamed("_c2", "job") -catalogName = "columnar" -tableName = "people" -tableNameNoPart = "peoplenopart" -columnarTableName = catalogName + "." + tableName -columnarTableNameNoPart = catalogName + "." + tableNameNoPart -columnarClass = 'org.apache.spark.sql.connector.InMemoryTableCatalog' - def setupInMemoryTableWithPartitioning(spark, csv): peopleCSVDf = createPeopleCSVDf(spark, csv) peopleCSVDf.createOrReplaceTempView("people_csv") From 5ba6a513ad3d5fc12f559100ba8757e082717305 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 21:30:18 -0600 Subject: [PATCH 42/49] update copyright and wrap exception --- ...rk-datasource-v2-columnar-1.0.0-SNAPSHOT.jar | Bin 76751 -> 0 bytes pom.xml | 2 +- .../nvidia/spark/rapids/GpuColumnVector.java | 2 +- .../spark/rapids/GpuCoalesceBatches.scala | 2 +- .../nvidia/spark/rapids/HostColumnarToGpu.scala | 16 +++++++++++----- .../spark/rapids/GpuCoalesceBatchesSuite.scala | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) delete mode 100644 integration_tests/src/test/resources/spark-datasource-v2-columnar-1.0.0-SNAPSHOT.jar diff --git a/integration_tests/src/test/resources/spark-datasource-v2-columnar-1.0.0-SNAPSHOT.jar b/integration_tests/src/test/resources/spark-datasource-v2-columnar-1.0.0-SNAPSHOT.jar deleted file mode 100644 index 1fe8f39e9772c30d107e894e16766fa761a8e3ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76751 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%0D0I|L(+R-RT@?^qXE;AaX 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 S2y_hi`3SUP69aI02>KsA+VKkj diff --git a/pom.xml b/pom.xml index 66a83f48760..9d131cdd59b 100644 --- a/pom.xml +++ b/pom.xml @@ -176,7 +176,7 @@ 3.0.2-SNAPSHOT 3.1.1-SNAPSHOT 3.6.0 - 3.4.4 + 4.3.0 diff --git a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java index e03d0c6cd34..b181c9d6e30 100644 --- a/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java +++ b/sql-plugin/src/main/java/com/nvidia/spark/rapids/GpuColumnVector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2019-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala index 20135a7c34c..a242dbb41e6 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuCoalesceBatches.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2019-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index b29b7c16e92..fbcdfb2681d 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2020, NVIDIA CORPORATION. + * Copyright (c) 2019-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,18 +56,24 @@ object HostColumnarToGpu extends Logging { nullable: Boolean, rows: Int): Unit = { val valVector = if (cv.isInstanceOf[ArrowColumnVector]) { - getArrowValueVector(cv) + try { + getArrowValueVector(cv) + } catch { + case e: Exception => + throw new IllegalStateException("Trying to read from a ArrowColumnVector but can't " + + "have access to its ValueVector", e) + } } else if (cv.isInstanceOf[AccessibleArrowColumnVector]) { val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] arrowVec.getArrowValueVector() } else { - throw new Exception("not arrow data shouldn't be here!") + throw new IllegalStateException(s"Illegal column vector type: ${cv.getClass}") } val nullCount = valVector.getNullCount() val dataBuf = ShimLoader.getSparkShims.getArrowDataBuf(valVector) val validity = ShimLoader.getSparkShims.getArrowValidityBuf(valVector) - var offsets:ByteBuffer = null - // this is a bit ugly, not all Arrow types need this so try and just catch it + // this is a bit ugly, not all Arrow types have the offsets buffer + var offsets: ByteBuffer = null try { offsets = ShimLoader.getSparkShims.getArrowOffsetsBuf(valVector) } catch { diff --git a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala index 0596194312b..21cfdcc39c1 100644 --- a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala +++ b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, NVIDIA CORPORATION. + * Copyright (c) 2020-2021, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From f5bfeaed153fc1fe516269858f503ea973750219 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 21:46:39 -0600 Subject: [PATCH 43/49] comment test Signed-off-by: Thomas Graves --- .../scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala index 21cfdcc39c1..e4d0a37086c 100644 --- a/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala +++ b/tests/src/test/scala/com/nvidia/spark/rapids/GpuCoalesceBatchesSuite.scala @@ -102,6 +102,7 @@ class GpuCoalesceBatchesSuite extends SparkQueryCompareTestSuite { }, conf) } + // this was copied from Spark ArrowUtils /** Maps data type from Spark to Arrow. NOTE: timeZoneId required for TimestampTypes */ private def toArrowType(dt: DataType, timeZoneId: String): ArrowType = dt match { case BooleanType => ArrowType.Bool.INSTANCE @@ -126,6 +127,7 @@ class GpuCoalesceBatchesSuite extends SparkQueryCompareTestSuite { throw new UnsupportedOperationException(s"Unsupported data type: ${dt.catalogString}") } + // this was copied from Spark ArrowUtils /** Maps field from Spark to Arrow. NOTE: timeZoneId required for TimestampType */ private def toArrowField( name: String, dt: DataType, nullable: Boolean, timeZoneId: String): Field = { From 56174211e5c957e437806cb80aff6f48906ae62e Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Thu, 28 Jan 2021 21:49:12 -0600 Subject: [PATCH 44/49] update comment in exception --- .../main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index fbcdfb2681d..bd47811fc97 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -61,7 +61,7 @@ object HostColumnarToGpu extends Logging { } catch { case e: Exception => throw new IllegalStateException("Trying to read from a ArrowColumnVector but can't " + - "have access to its ValueVector", e) + "access its Arrow ValueVector", e) } } else if (cv.isInstanceOf[AccessibleArrowColumnVector]) { val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] From c68c26d8f2d93c6c8ace2eefaa138bff653c1915 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 29 Jan 2021 08:30:56 -0600 Subject: [PATCH 45/49] fix merge conflicts --- .../nvidia/spark/rapids/shims/spark300/Spark300Shims.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala index 57245f5c46f..1bfe5f645a1 100644 --- a/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala +++ b/shims/spark300/src/main/scala/com/nvidia/spark/rapids/shims/spark300/Spark300Shims.scala @@ -21,11 +21,8 @@ import java.time.ZoneId import com.nvidia.spark.rapids._ import com.nvidia.spark.rapids.spark300.RapidsShuffleManager -<<<<<<< HEAD import org.apache.arrow.vector.ValueVector -======= import org.apache.hadoop.fs.Path ->>>>>>> origin/branch-0.4 import org.apache.spark.SparkEnv import org.apache.spark.rdd.RDD @@ -466,6 +463,7 @@ class Spark300Shims extends SparkShims { override def getArrowOffsetsBuf(vec: ValueVector): ByteBuffer = { vec.getOffsetBuffer().nioBuffer() + } override def replaceWithAlluxioPathIfNeeded( conf: RapidsConf, From 513cea04d983676d9e404d39d71faa4247a59ecc Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 29 Jan 2021 10:28:57 -0600 Subject: [PATCH 46/49] use case, logDebug, update test to use tmp table --- docs/configs.md | 1 + .../src/main/python/datasourcev2_read.py | 40 ++++++++++--------- .../spark/rapids/HostColumnarToGpu.scala | 31 +++++++------- 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/docs/configs.md b/docs/configs.md index 6ff58712230..501e5075ff0 100644 --- a/docs/configs.md +++ b/docs/configs.md @@ -29,6 +29,7 @@ scala> spark.conf.set("spark.rapids.sql.incompatibleOps.enabled", true) Name | Description | Default Value -----|-------------|-------------- +spark.rapids.alluxio.pathsToReplace|List of paths to be replaced with corresponding alluxio scheme. Eg, when configureis set to "s3:/foo->alluxio://0.1.2.3:19998/foo,gcs:/bar->alluxio://0.1.2.3:19998/bar", which means: s3:/foo/a.csv will be replaced to alluxio://0.1.2.3:19998/foo/a.csv and gcs:/bar/b.csv will be replaced to alluxio://0.1.2.3:19998/bar/b.csv|None spark.rapids.cloudSchemes|Comma separated list of additional URI schemes that are to be considered cloud based filesystems. Schemes already included: dbfs, s3, s3a, s3n, wasbs, gs. Cloud based stores generally would be total separate from the executors and likely have a higher I/O read cost. Many times the cloud filesystems also get better throughput when you have multiple readers in parallel. This is used with spark.rapids.sql.format.parquet.reader.type|None spark.rapids.memory.gpu.allocFraction|The fraction of total GPU memory that should be initially allocated for pooled memory. Extra memory will be allocated as needed, but it may result in more fragmentation. This must be less than or equal to the maximum limit configured via spark.rapids.memory.gpu.maxAllocFraction.|0.9 spark.rapids.memory.gpu.debug|Provides a log of GPU memory allocations and frees. If set to STDOUT or STDERR the logging will go there. Setting it to NONE disables logging. All other values are reserved for possible future expansion and in the mean time will disable logging.|NONE diff --git a/integration_tests/src/main/python/datasourcev2_read.py b/integration_tests/src/main/python/datasourcev2_read.py index bbc6b4e4a83..4a556369304 100644 --- a/integration_tests/src/main/python/datasourcev2_read.py +++ b/integration_tests/src/main/python/datasourcev2_read.py @@ -15,6 +15,7 @@ import pytest from asserts import assert_gpu_and_cpu_are_equal_collect +from marks import * from pyspark.sql.types import * from spark_session import with_cpu_session @@ -24,10 +25,6 @@ # If that class is not present it skips the tests. catalogName = "columnar" -tableName = "people" -tableNameNoPart = "peoplenopart" -columnarTableName = catalogName + "." + tableName -columnarTableNameNoPart = catalogName + "." + tableNameNoPart columnarClass = 'org.apache.spark.sql.connector.InMemoryTableCatalog' def createPeopleCSVDf(spark, peopleCSVLocation): @@ -39,15 +36,15 @@ def createPeopleCSVDf(spark, peopleCSVLocation): .withColumnRenamed("_c1", "age")\ .withColumnRenamed("_c2", "job") -def setupInMemoryTableWithPartitioning(spark, csv): +def setupInMemoryTableWithPartitioning(spark, csv, tname, column_and_table): peopleCSVDf = createPeopleCSVDf(spark, csv) - peopleCSVDf.createOrReplaceTempView("people_csv") - spark.table("people_csv").write.partitionBy("job").saveAsTable(columnarTableName) + peopleCSVDf.createOrReplaceTempView(tname) + spark.table(tname).write.partitionBy("job").saveAsTable(column_and_table) -def setupInMemoryTableNoPartitioning(spark, csv): +def setupInMemoryTableNoPartitioning(spark, csv, tname, column_and_table): peopleCSVDf = createPeopleCSVDf(spark, csv) - peopleCSVDf.createOrReplaceTempView("people_csv") - spark.table("people_csv").write.saveAsTable(columnarTableNameNoPart) + peopleCSVDf.createOrReplaceTempView(tname) + spark.table(tname).write.saveAsTable(column_and_table) def readTable(csvPath, tableToRead): return lambda spark: spark.table(tableToRead)\ @@ -61,8 +58,6 @@ def createDatabase(spark): pytest.skip("Failed to load catalog for datasource v2 {}, jar is probably missing".format(columnarClass)) def cleanupDatabase(spark): - spark.sql("drop table IF EXISTS " + tableName) - spark.sql("drop table IF EXISTS " + tableNameNoPart) spark.sql("drop database IF EXISTS " + catalogName) @pytest.fixture(autouse=True) @@ -73,26 +68,35 @@ def setupAndCleanUp(): with_cpu_session(lambda spark : cleanupDatabase(spark), conf={'spark.sql.catalog.columnar': columnarClass}) +@allow_non_gpu('ShowTablesExec', 'DropTableExec') @pytest.mark.parametrize('csv', ['people.csv']) -def test_read_round_trip_partitioned(std_input_path, csv): +def test_read_round_trip_partitioned(std_input_path, csv, spark_tmp_table_factory): csvPath = std_input_path + "/" + csv - with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark, csvPath), + tableName = spark_tmp_table_factory.get() + columnarTableName = catalogName + "." + tableName + with_cpu_session(lambda spark : setupInMemoryTableWithPartitioning(spark, csvPath, tableName, columnarTableName), conf={'spark.sql.catalog.columnar': columnarClass}) assert_gpu_and_cpu_are_equal_collect(readTable(csvPath, columnarTableName), conf={'spark.sql.catalog.columnar': columnarClass}) +@allow_non_gpu('ShowTablesExec', 'DropTableExec') @pytest.mark.parametrize('csv', ['people.csv']) -def test_read_round_trip_no_partitioned(std_input_path, csv): +def test_read_round_trip_no_partitioned(std_input_path, csv, spark_tmp_table_factory): csvPath = std_input_path + "/" + csv - with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, csvPath), + tableNameNoPart = spark_tmp_table_factory.get() + columnarTableNameNoPart = catalogName + "." + tableNameNoPart + with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, csvPath, tableNameNoPart, columnarTableNameNoPart), conf={'spark.sql.catalog.columnar': columnarClass}) assert_gpu_and_cpu_are_equal_collect(readTable(csvPath, columnarTableNameNoPart), conf={'spark.sql.catalog.columnar': columnarClass}) +@allow_non_gpu('ShowTablesExec', 'DropTableExec') @pytest.mark.parametrize('csv', ['people.csv']) -def test_read_round_trip_no_partitioned_arrow_off(std_input_path, csv): +def test_read_round_trip_no_partitioned_arrow_off(std_input_path, csv, spark_tmp_table_factory): csvPath = std_input_path + "/" + csv - with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, csvPath), + tableNameNoPart = spark_tmp_table_factory.get() + columnarTableNameNoPart = catalogName + "." + tableNameNoPart + with_cpu_session(lambda spark : setupInMemoryTableNoPartitioning(spark, csvPath, tableNameNoPart, columnarTableNameNoPart), conf={'spark.sql.catalog.columnar': columnarClass, 'spark.rapids.arrowCopyOptmizationEnabled': 'false'}) assert_gpu_and_cpu_are_equal_collect(readTable(csvPath, columnarTableNameNoPart), diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index bd47811fc97..6a118a282fc 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -55,19 +55,20 @@ object HostColumnarToGpu extends Logging { ab: ai.rapids.cudf.ArrowColumnBuilder, nullable: Boolean, rows: Int): Unit = { - val valVector = if (cv.isInstanceOf[ArrowColumnVector]) { - try { - getArrowValueVector(cv) - } catch { - case e: Exception => - throw new IllegalStateException("Trying to read from a ArrowColumnVector but can't " + - "access its Arrow ValueVector", e) - } - } else if (cv.isInstanceOf[AccessibleArrowColumnVector]) { - val arrowVec = cv.asInstanceOf[AccessibleArrowColumnVector] - arrowVec.getArrowValueVector() - } else { - throw new IllegalStateException(s"Illegal column vector type: ${cv.getClass}") + val valVector = cv match { + case v: ArrowColumnVector => + try { + getArrowValueVector(v) + } catch { + case e: Exception => + throw new IllegalStateException("Trying to read from a ArrowColumnVector but can't " + + "access its Arrow ValueVector", e) + } + case av: AccessibleArrowColumnVector => + // val arrowVec = av.asInstanceOf[AccessibleArrowColumnVector] + av.getArrowValueVector() + case _ => + throw new IllegalStateException(s"Illegal column vector type: ${cv.getClass}") } val nullCount = valVector.getNullCount() val dataBuf = ShimLoader.getSparkShims.getArrowDataBuf(valVector) @@ -266,10 +267,10 @@ class HostToGpuCoalesceIterator(iter: Iterator[ColumnarBatch], arrowTypesSupported(schema) && (batch.column(0).isInstanceOf[ArrowColumnVector] || batch.column(0).isInstanceOf[AccessibleArrowColumnVector])) { - logInfo("Using GpuArrowColumnarBatchBuilder") + logDebug("Using GpuArrowColumnarBatchBuilder") batchBuilder = new GpuColumnVector.GpuArrowColumnarBatchBuilder(schema, batchRowLimit, batch) } else { - logInfo("Using GpuColumnarBatchBuilder") + logDebug("Using GpuColumnarBatchBuilder") batchBuilder = new GpuColumnVector.GpuColumnarBatchBuilder(schema, batchRowLimit, null) } totalRows = 0 From 9b1a2fadca597be567c1e174be57fd35dc1c018b Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 29 Jan 2021 10:55:58 -0600 Subject: [PATCH 47/49] make some of the reflection lazy vals so it only happens once --- .../spark/rapids/HostColumnarToGpu.scala | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 6a118a282fc..afd4df3e4d1 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -33,21 +33,30 @@ import org.apache.spark.sql.vectorized.rapids.AccessibleArrowColumnVector object HostColumnarToGpu extends Logging { - // use reflection to get value vector from ArrowColumnVector - private def getArrowValueVector(cv: ColumnVector): ValueVector = { - val arrowCV = cv.asInstanceOf[ArrowColumnVector] - val fields = arrowCV.getClass.getDeclaredFields.toList + // use reflection to get access to a private field in a class + private def getClassFieldAccessible(className: String, fieldName: String) = { + val classObj = Class.forName(className) + val fields = classObj.getDeclaredFields.toList val field = fields.filter( x => { - x.getName.contains("accessor") + x.getName.contains(fieldName) }).head field.setAccessible(true) - val accessor = field.get(arrowCV) - val accessFields = accessor.getClass().getSuperclass().getDeclaredFields().toList - val valVecField = accessFields.filter( x => { - x.getName.contains("vector") - }).head - valVecField.setAccessible(true) - valVecField.get(accessor).asInstanceOf[ValueVector] + field + } + + private lazy val accessorField = { + getClassFieldAccessible("org.apache.spark.sql.vectorized.ArrowColumnVector", "accessor") + } + + private lazy val vecField = { + getClassFieldAccessible("org.apache.spark.sql.vectorized.ArrowColumnVector$ArrowVectorAccessor", "vector") + } + + // use reflection to get value vector from ArrowColumnVector + private def getArrowValueVector(cv: ColumnVector): ValueVector = { + val arrowCV = cv.asInstanceOf[ArrowColumnVector] + val accessor = accessorField.get(arrowCV) + vecField.get(accessor).asInstanceOf[ValueVector] } def arrowColumnarCopy( From 0c28106d8684552d0b401946bd628fb2b87ec872 Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 29 Jan 2021 11:01:17 -0600 Subject: [PATCH 48/49] fix line length --- .../main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index afd4df3e4d1..2610fee3486 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -49,7 +49,8 @@ object HostColumnarToGpu extends Logging { } private lazy val vecField = { - getClassFieldAccessible("org.apache.spark.sql.vectorized.ArrowColumnVector$ArrowVectorAccessor", "vector") + getClassFieldAccessible("org.apache.spark.sql.vectorized.ArrowColumnVector$ArrowVectorAccessor", + "vector") } // use reflection to get value vector from ArrowColumnVector From 7ce4bef1609ec672c4b7c7a47e808ec19951a7cf Mon Sep 17 00:00:00 2001 From: Thomas Graves Date: Fri, 29 Jan 2021 11:10:55 -0600 Subject: [PATCH 49/49] remove commented out line --- .../main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala index 2610fee3486..ba50bf6fe59 100644 --- a/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala +++ b/sql-plugin/src/main/scala/com/nvidia/spark/rapids/HostColumnarToGpu.scala @@ -75,7 +75,6 @@ object HostColumnarToGpu extends Logging { "access its Arrow ValueVector", e) } case av: AccessibleArrowColumnVector => - // val arrowVec = av.asInstanceOf[AccessibleArrowColumnVector] av.getArrowValueVector() case _ => throw new IllegalStateException(s"Illegal column vector type: ${cv.getClass}")