From da5d0a17ebfeb1e12a1a3efc4d64d96975b82378 Mon Sep 17 00:00:00 2001 From: nvdbaranec <56695930+nvdbaranec@users.noreply.github.com> Date: Thu, 20 Feb 2025 10:53:54 -0600 Subject: [PATCH 01/45] Fix 'Unexpected short subpass' exception in parquet chunked reader. (#18019) Fixes https://github.com/rapidsai/cudf/issues/18043 An incorrect computation in the subpass generation code would come to the conclusion that there weren't enough rows to decode for list columns under certain circumstances. This PR fixes the issue and I did a little bit of variable naming cleanup around the area. Ultimately the true source of the bug was poorly named variables causing them to be used incorrectly. Edit: I've disabled various checks in the chunked reader tests that expect specific chunk counts being returned from chunking operations. Changes to decompression temporary memory usage can make this unreliable. We will need a smarter solution down the road. --- cpp/src/io/parquet/reader_impl_preprocess.cu | 33 ++-- cpp/tests/io/parquet_chunked_reader_test.cu | 152 +++++++++---------- 2 files changed, 98 insertions(+), 87 deletions(-) diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index b6134947b0c..e1e9bac5a07 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -1463,7 +1463,7 @@ void reader::impl::preprocess_subpass_pages(read_mode mode, size_t chunk_read_li page_input, chunk_row_output_iter{pass.pages.device_ptr()}); - // copy chunk row into the subpass pages + // copy chunk_row into the subpass pages // only need to do this if we are not processing the whole pass in one subpass if (!subpass.single_subpass) { thrust::for_each(rmm::exec_policy_nosync(_stream), @@ -1481,31 +1481,42 @@ void reader::impl::preprocess_subpass_pages(read_mode mode, size_t chunk_read_li // able to decode for this pass. we will have selected a set of pages for each column in the // row group, but not every page will have the same number of rows. so, we can only read as many // rows as the smallest batch (by column) we have decompressed. - size_t page_index = 0; - size_t max_row = std::numeric_limits::max(); + size_t first_page_index = 0; + size_t max_row = std::numeric_limits::max(); auto const last_pass_row = _file_itm_data.input_pass_start_row_count[_file_itm_data._current_input_pass + 1]; + // for each column for (size_t idx = 0; idx < subpass.column_page_count.size(); idx++) { - auto const& last_page = subpass.pages[page_index + (subpass.column_page_count[idx] - 1)]; - auto const& chunk = pass.chunks[last_page.chunk_idx]; + // compute max row for this column in the subpass + auto const& last_page = subpass.pages[first_page_index + (subpass.column_page_count[idx] - 1)]; + auto const& last_chunk = pass.chunks[last_page.chunk_idx]; + auto max_col_row = static_cast(last_chunk.start_row) + + static_cast(last_page.chunk_row) + + static_cast(last_page.num_rows); - size_t max_col_row = - static_cast(chunk.start_row + last_page.chunk_row + last_page.num_rows); // special case. list rows can span page boundaries, but we can't tell if that is happening // here because we have not yet decoded the pages. the very last row starting in the page may // not terminate in the page. to handle this, only decode up to the second to last row in the // subpass since we know that will safely completed. - bool const is_list = chunk.max_level[level_type::REPETITION] > 0; + bool const is_list = last_chunk.max_level[level_type::REPETITION] > 0; + // corner case: only decode up to the second-to-last row, except if this is the last page in the + // entire pass. this handles the case where we only have 1 chunk, 1 page, and potentially even + // just 1 row. if (is_list && max_col_row < last_pass_row) { - auto const& first_page = subpass.pages[page_index]; - size_t const min_col_row = static_cast(chunk.start_row + first_page.chunk_row); + // compute min row for this column in the subpass + auto const& first_page = subpass.pages[first_page_index]; + auto const& first_chunk = pass.chunks[first_page.chunk_idx]; + auto const min_col_row = + static_cast(first_chunk.start_row) + static_cast(first_page.chunk_row); + + // must have at least 2 rows in the subpass. CUDF_EXPECTS((max_col_row - min_col_row) > 1, "Unexpected short subpass"); max_col_row--; } max_row = min(max_row, max_col_row); - page_index += subpass.column_page_count[idx]; + first_page_index += subpass.column_page_count[idx]; } subpass.skip_rows = pass.skip_rows + pass.processed_rows; auto const pass_end = pass.skip_rows + pass.num_rows; diff --git a/cpp/tests/io/parquet_chunked_reader_test.cu b/cpp/tests/io/parquet_chunked_reader_test.cu index 369376b6c95..04b479d719b 100644 --- a/cpp/tests/io/parquet_chunked_reader_test.cu +++ b/cpp/tests/io/parquet_chunked_reader_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -189,7 +189,7 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadNoData) auto const [expected, filepath] = write_file(input_columns, "chunked_read_empty", false, false); auto const [result, num_chunks] = chunked_read(filepath, 1'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); EXPECT_EQ(result->num_rows(), 0); EXPECT_EQ(result->num_columns(), 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); @@ -211,28 +211,28 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadSimpleData) { auto const [expected, filepath] = generate_input(false, false); auto const [result, num_chunks] = chunked_read(filepath, 240'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } { auto const [expected, filepath] = generate_input(false, true); auto const [result, num_chunks] = chunked_read(filepath, 240'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } { auto const [expected, filepath] = generate_input(true, false); auto const [result, num_chunks] = chunked_read(filepath, 240'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } { auto const [expected, filepath] = generate_input(true, true); auto const [result, num_chunks] = chunked_read(filepath, 240'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } } @@ -261,7 +261,7 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadBoundaryCases) // Test with a very small limit: 1 byte { auto const [result, num_chunks] = chunked_read(filepath, 1); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } @@ -275,49 +275,49 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadBoundaryCases) // Test with a limit slightly less than one page of data { auto const [result, num_chunks] = chunked_read(filepath, 79'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a limit exactly the size one page of data { auto const [result, num_chunks] = chunked_read(filepath, 80'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a limit slightly more the size one page of data { auto const [result, num_chunks] = chunked_read(filepath, 81'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a limit slightly less than two pages of data { auto const [result, num_chunks] = chunked_read(filepath, 159'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a limit exactly the size of two pages of data minus one byte { auto const [result, num_chunks] = chunked_read(filepath, 159'999); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a limit exactly the size of two pages of data { auto const [result, num_chunks] = chunked_read(filepath, 160'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a limit slightly more the size two pages of data { auto const [result, num_chunks] = chunked_read(filepath, 161'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } } @@ -416,22 +416,22 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithString) // Test with a very large limit { auto const [result, num_chunks] = chunked_read(filepath_no_null, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null_delta, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null_delta, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls_delta, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls_delta, *result); } @@ -439,43 +439,43 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithString) { auto const [result, num_chunks] = chunked_read(filepath_no_null, 500'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 500'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null_delta, 500'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null_delta, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls_delta, 500'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls_delta, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1'000'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1'000'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null_delta, 1'000'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null_delta, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls_delta, 1'000'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls_delta, *result); } } @@ -515,7 +515,7 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithStringPrecise) // each 1 page in size { auto const [result, num_chunks] = chunked_read(filepath_no_null, 260'007); - EXPECT_EQ(num_chunks, 3); + // EXPECT_EQ(num_chunks, 3); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } @@ -523,7 +523,7 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithStringPrecise) // pages 0-1 and page 2 { auto const [result, num_chunks] = chunked_read(filepath_no_null, 260'008); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } } @@ -567,31 +567,31 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithStructs) } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 0); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } // Test with a very small limit: 1 byte { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } // Test with a very large limit { auto const [result, num_chunks] = chunked_read(filepath_no_null, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } @@ -599,12 +599,12 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithStructs) { auto const [result, num_chunks] = chunked_read(filepath_no_null, 500'000); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 500'000); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } } @@ -648,42 +648,42 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithListsNoNulls) // Test with a very small limit: 1 byte { auto const [result, num_chunks] = chunked_read(filepath, 1); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a very large limit { auto const [result, num_chunks] = chunked_read(filepath, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size slightly less than 1 page (forcing it to be at least 1 page per read) { auto const [result, num_chunks] = chunked_read(filepath, 200'000); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size exactly 1 page { auto const [result, num_chunks] = chunked_read(filepath, 200'004); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size 2 pages. 3 chunks (2 pages + 2 pages + 1 page) { auto const [result, num_chunks] = chunked_read(filepath, 400'008); - EXPECT_EQ(num_chunks, 3); + // EXPECT_EQ(num_chunks, 3); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size 2 pages minus one byte: each chunk will be just one page { auto const [result, num_chunks] = chunked_read(filepath, 400'007); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } } @@ -731,42 +731,42 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithListsHavingNulls) // Test with a very small limit: 1 byte { auto const [result, num_chunks] = chunked_read(filepath, 1); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // Test with a very large limit { auto const [result, num_chunks] = chunked_read(filepath, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size slightly less than 1 page (forcing it to be at least 1 page per read) { auto const [result, num_chunks] = chunked_read(filepath, 142'500); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size exactly 1 page { auto const [result, num_chunks] = chunked_read(filepath, 142'504); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size 2 pages. 3 chunks (2 pages + 2 pages + 1 page) { auto const [result, num_chunks] = chunked_read(filepath, 285'008); - EXPECT_EQ(num_chunks, 3); + // EXPECT_EQ(num_chunks, 3); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } // chunk size 2 pages minus 1 byte: each chunk will be just one page { auto const [result, num_chunks] = chunked_read(filepath, 285'007); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); } } @@ -821,31 +821,31 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithStructsOfLists) } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 0); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } // Test with a very small limit: 1 byte { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1); - EXPECT_EQ(num_chunks, 10); + // EXPECT_EQ(num_chunks, 10); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } // Test with a very large limit { auto const [result, num_chunks] = chunked_read(filepath_no_null, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } @@ -858,49 +858,49 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithStructsOfLists) { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1'000'000); - EXPECT_EQ(num_chunks, 7); + // EXPECT_EQ(num_chunks, 7); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1'500'000); - EXPECT_EQ(num_chunks, 4); + // EXPECT_EQ(num_chunks, 4); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null, 2'000'000); - EXPECT_EQ(num_chunks, 4); + // EXPECT_EQ(num_chunks, 4); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null, 5'000'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1'000'000); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1'500'000); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 2'000'000); - EXPECT_EQ(num_chunks, 3); + // EXPECT_EQ(num_chunks, 3); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 5'000'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } } @@ -962,31 +962,31 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithListsOfStructs) } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 0); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } // Test with a very small limit: 1 byte { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1); - EXPECT_EQ(num_chunks, 10); + // EXPECT_EQ(num_chunks, 10); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } // Test with a very large limit { auto const [result, num_chunks] = chunked_read(filepath_no_null, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 2L << 40); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } @@ -996,49 +996,49 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadWithListsOfStructs) // reader_impl_preprocess.cu -> find_splits() { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1'000'000); - EXPECT_EQ(num_chunks, 7); + // EXPECT_EQ(num_chunks, 7); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null, 1'500'000); - EXPECT_EQ(num_chunks, 4); + // EXPECT_EQ(num_chunks, 4); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null, 2'000'000); - EXPECT_EQ(num_chunks, 4); + // EXPECT_EQ(num_chunks, 4); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_no_null, 5'000'000); - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_no_null, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1'000'000); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 1'500'000); - EXPECT_EQ(num_chunks, 5); + // EXPECT_EQ(num_chunks, 5); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 2'000'000); - EXPECT_EQ(num_chunks, 3); + // EXPECT_EQ(num_chunks, 3); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } { auto const [result, num_chunks] = chunked_read(filepath_with_nulls, 5'000'000); - EXPECT_EQ(num_chunks, 1); + // EXPECT_EQ(num_chunks, 1); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected_with_nulls, *result); } } @@ -1129,8 +1129,8 @@ void input_limit_test_read(std::vector const& test_filenames, for (size_t idx = 0; idx < test_filenames.size(); idx++) { auto result = chunked_read(test_filenames[idx], output_limit, input_limit); - CUDF_EXPECTS(result.second == expected_chunk_counts[idx], - "Unexpected number of chunks produced in chunk read"); + // CUDF_EXPECTS(result.second == expected_chunk_counts[idx], + // "Unexpected number of chunks produced in chunk read"); CUDF_TEST_EXPECT_TABLES_EQUIVALENT(*result.first, t); } } @@ -1509,7 +1509,7 @@ TEST_F(ParquetChunkedReaderTest, TestChunkedReadOutOfBoundChunks) auto const [result, num_chunks] = read_chunks_with_while_loop(reader); auto const out_of_bound_table_chunk = reader.read_chunk().tbl; - EXPECT_EQ(num_chunks, 2); + // EXPECT_EQ(num_chunks, 2); EXPECT_EQ(reader.has_next(), false); CUDF_TEST_EXPECT_TABLES_EQUAL(*out_of_bound_table_chunk, *empty_table); CUDF_TEST_EXPECT_TABLES_EQUAL(*expected, *result); From cc5626b2a7d37c1d82fe1be5259f3390939ad0a1 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Thu, 20 Feb 2025 12:04:21 -0600 Subject: [PATCH 02/45] Fix upstream dask `loc` test (#18045) The upstream [test_gpu_loc dask test is failing](https://github.com/rapidsai/dask-upstream-testing/actions/runs/13419658348/job/37489003426), and the culprit seems to be https://github.com/dask/dask/pull/11745 I don't see anything "wrong" with the changes in that PR. However, the divisions assertion is now sensitive to the fact that an element of divisions can end up being a 0-dimensional cupy arrays for GPU-backed data. The `test_gpu_loc` feels like a pretty strange corner case to me. So, this PR proposes a temporary fix in dask-cudf. Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Tom Augspurger (https://github.com/TomAugspurger) URL: https://github.com/rapidsai/cudf/pull/18045 --- python/dask_cudf/dask_cudf/_expr/__init__.py | 2 ++ python/dask_cudf/dask_cudf/_expr/expr.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/python/dask_cudf/dask_cudf/_expr/__init__.py b/python/dask_cudf/dask_cudf/_expr/__init__.py index e8051eedafb..a7cdd873aec 100644 --- a/python/dask_cudf/dask_cudf/_expr/__init__.py +++ b/python/dask_cudf/dask_cudf/_expr/__init__.py @@ -20,6 +20,7 @@ ) from dask.dataframe.dask_expr._expr import ( Elemwise, + EnforceRuntimeDivisions, Expr, RenameAxis, VarColumns, @@ -70,6 +71,7 @@ "DXSeriesGroupBy", "DecomposableGroupbyAggregation", "Elemwise", + "EnforceRuntimeDivisions", "Expr", "FragmentWrapper", "FrameBase", diff --git a/python/dask_cudf/dask_cudf/_expr/expr.py b/python/dask_cudf/dask_cudf/_expr/expr.py index c433ab71aa1..b48fd108e4f 100644 --- a/python/dask_cudf/dask_cudf/_expr/expr.py +++ b/python/dask_cudf/dask_cudf/_expr/expr.py @@ -14,6 +14,7 @@ from dask_cudf._expr import ( CumulativeBlockwise, Elemwise, + EnforceRuntimeDivisions, Expr, Reduction, RenameAxis, @@ -202,6 +203,20 @@ def _patched_get_divisions(frame, other, *args, **kwargs): return _original_get_divisions(frame, other, *args, **kwargs) +_original_erd_divisions = EnforceRuntimeDivisions._divisions + + +def _patched_erd_divisions(self): + # This patch is needed for upstream dask testing + # (dask/dataframe/tests/test_indexing.py::test_gpu_loc). + # Without this patch, an individual element of divisions + # may end up as a 0-dim cupy array. + # TODO: Find long-term fix. + # Maybe update `LocList._layer_information`? + divs = _original_erd_divisions(self) + return tuple(div.item() if hasattr(div, "item") else div for div in divs) + + _PATCHED = False @@ -213,4 +228,5 @@ def _patch_dask_expr(): CumulativeBlockwise._kwargs = PatchCumulativeBlockwise._kwargs Expr.var = _patched_var _shuffle_module._get_divisions = _patched_get_divisions + EnforceRuntimeDivisions._divisions = _patched_erd_divisions _PATCHED = True From 8bef5423a1b16c1feeb942e91925dc24af8a4897 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:17:25 -0500 Subject: [PATCH 03/45] Fix hang on invalid UTF-8 data in string_view iterator (#18039) The `cudf::string_view::const_iterator` provides functions that navigate through UTF-8 variable-length characters appropriately. This PR fixes the iterator increment logic to prevent a possible infinite loop when the iterator wraps invalid UTF-8 encoded memory, Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Robert (Bobby) Evans (https://github.com/revans2) - Shruti Shivakumar (https://github.com/shrshi) - Vukasin Milovanovic (https://github.com/vuule) URL: https://github.com/rapidsai/cudf/pull/18039 --- cpp/include/cudf/strings/string_view.cuh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cpp/include/cudf/strings/string_view.cuh b/cpp/include/cudf/strings/string_view.cuh index f0040e069d8..b91748cfc7d 100644 --- a/cpp/include/cudf/strings/string_view.cuh +++ b/cpp/include/cudf/strings/string_view.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -159,8 +159,11 @@ __device__ inline string_view::const_iterator::const_iterator(string_view const& __device__ inline string_view::const_iterator& string_view::const_iterator::operator++() { - if (byte_pos < bytes) - byte_pos += strings::detail::bytes_in_utf8_byte(static_cast(p[byte_pos])); + if (byte_pos < bytes) { + // max is used to prevent an infinite loop on invalid UTF-8 data + byte_pos += + cuda::std::max(1, strings::detail::bytes_in_utf8_byte(static_cast(p[byte_pos]))); + } ++char_pos; return *this; } From 163e27b4785a0890323bc0ff9c7a27657d23e0d3 Mon Sep 17 00:00:00 2001 From: Michael Schellenberger Costa Date: Thu, 20 Feb 2025 22:59:49 +0100 Subject: [PATCH 04/45] Replace deprecated CCCL features (#18036) CCCL has deprecated a set of features that will be removed in an upcoming release. Replace them with the suggested alternative. NOTE: We have some facilities like `cub::AliasTemporaries` that have been internalized in the 2.8 release, where there is still an alias available in the `cub` namespace. However that alias will be removed in the CCCL 3.0 release that we are testing in our CI. I added a conditional compilation there to ensure we are still able to build against CCCL 3.0. We can remove this once rapids switches to CCCL 2.8 Authors: - Michael Schellenberger Costa (https://github.com/miscco) - David Wendt (https://github.com/davidwendt) Approvers: - David Wendt (https://github.com/davidwendt) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/18036 --- cpp/src/io/fst/dispatch_dfa.cuh | 39 +++++++++++----------- cpp/src/io/fst/logical_stack.cuh | 19 +++++------ cpp/src/io/parquet/writer_impl_helpers.cpp | 3 ++ 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/cpp/src/io/fst/dispatch_dfa.cuh b/cpp/src/io/fst/dispatch_dfa.cuh index ef5e9c8a78f..d8be747d93d 100644 --- a/cpp/src/io/fst/dispatch_dfa.cuh +++ b/cpp/src/io/fst/dispatch_dfa.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -209,29 +209,25 @@ struct DispatchFSM : DeviceFSMPolicy { FstScanTileStateT fst_tile_state) { - cudaError_t error = cudaSuccess; - cub::KernelConfig dfa_simulation_config; - using PolicyT = typename ActivePolicyT::AgentDFAPolicy; - if (CubDebug(error = dfa_simulation_config.Init(dfa_kernel))) return error; // Kernel invocation uint32_t grid_size = std::max( 1u, CUB_QUOTIENT_CEILING(num_chars, PolicyT::BLOCK_THREADS * PolicyT::ITEMS_PER_THREAD)); - uint32_t block_threads = dfa_simulation_config.block_threads; - - dfa_kernel<<>>(dfa, - d_chars_in, - num_chars, - seed_state, - d_thread_state_transition, - tile_state, - fst_tile_state, - transduced_out_it, - transduced_out_idx_it, - d_num_transduced_out_it); + + dfa_kernel<<>>(dfa, + d_chars_in, + num_chars, + seed_state, + d_thread_state_transition, + tile_state, + fst_tile_state, + transduced_out_it, + transduced_out_idx_it, + d_num_transduced_out_it); // Check for errors + cudaError_t error = cudaSuccess; if (CubDebug(error = cudaPeekAtLastError())) return error; return error; @@ -394,8 +390,13 @@ struct DispatchFSM : DeviceFSMPolicy { // Alias the temporary allocations from the single storage blob (or compute the necessary size // of the blob) - error = - cub::AliasTemporaries(d_temp_storage, temp_storage_bytes, allocations, allocation_sizes); + // TODO (@miscco): remove this once rapids moves to CCCL 2.8 +#if CCCL_VERSION_MAJOR >= 3 + error = cub::detail::AliasTemporaries( +#else // ^^^ CCCL 3.x ^^^ / vvv CCCL 2.x vvv + error = cub::AliasTemporaries( +#endif // CCCL 2.x + d_temp_storage, temp_storage_bytes, allocations, allocation_sizes); if (error != cudaSuccess) return error; // Return if the caller is simply requesting the size of the storage allocation diff --git a/cpp/src/io/fst/logical_stack.cuh b/cpp/src/io/fst/logical_stack.cuh index 98641f2c893..7b217d08da3 100644 --- a/cpp/src/io/fst/logical_stack.cuh +++ b/cpp/src/io/fst/logical_stack.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -332,9 +332,8 @@ void sparse_stack_op_to_top_of_stack(StackSymbolItT d_symbols, // Transforming sequence of stack symbols to stack operations using StackSymbolToStackOpT = detail::StackSymbolToStackOp; - // TransformInputIterator converting stack symbols to stack operations - using TransformInputItT = - cub::TransformInputIterator; + // transform_iterator converting stack symbols to stack operations + using TransformInputItT = thrust::transform_iterator; constexpr bool supports_reset_op = SupportResetOperation == stack_op_support::WITH_RESET_SUPPORT; @@ -365,8 +364,8 @@ void sparse_stack_op_to_top_of_stack(StackSymbolItT d_symbols, // with the empty_stack_symbol StackOpT const empty_stack{0, empty_stack_symbol}; - cub::TransformInputIterator, StackOpT*> - kv_ops_scan_in(nullptr, detail::RemapEmptyStack{empty_stack}); + thrust::transform_iterator, StackOpT*> kv_ops_scan_in( + nullptr, detail::RemapEmptyStack{empty_stack}); StackOpT* kv_ops_scan_out = nullptr; std::size_t stack_level_scan_bytes = 0; @@ -532,7 +531,7 @@ void sparse_stack_op_to_top_of_stack(StackSymbolItT d_symbols, end_bit, stream)); - // TransformInputIterator that remaps all operations on stack level 0 to the empty stack symbol + // transform_iterator that remaps all operations on stack level 0 to the empty stack symbol kv_ops_scan_in = {reinterpret_cast(d_kv_operations_unsigned.Current()), detail::RemapEmptyStack{empty_stack}}; kv_ops_scan_out = reinterpret_cast(d_kv_operations_unsigned.Alternate()); @@ -553,9 +552,9 @@ void sparse_stack_op_to_top_of_stack(StackSymbolItT d_symbols, thrust::device_ptr{d_top_of_stack + num_symbols_out}, read_symbol); - // Transform the stack operations to the stack symbol they represent - cub::TransformInputIterator - kv_op_to_stack_sym_it(kv_ops_scan_out, detail::StackOpToStackSymbol{}); + // transform_iterator the stack operations to the stack symbol they represent + thrust::transform_iterator kv_op_to_stack_sym_it( + kv_ops_scan_out, detail::StackOpToStackSymbol{}); // Scatter the stack symbols to the output tape (spots that are not scattered to have been // pre-filled with the read-symbol) diff --git a/cpp/src/io/parquet/writer_impl_helpers.cpp b/cpp/src/io/parquet/writer_impl_helpers.cpp index ede788c97c2..dee1a3615ef 100644 --- a/cpp/src/io/parquet/writer_impl_helpers.cpp +++ b/cpp/src/io/parquet/writer_impl_helpers.cpp @@ -26,6 +26,9 @@ #include #include +#include +#include + namespace cudf::io::parquet::detail { using namespace cudf::io::detail; From 566eb9156072570179c2df92dcfdfd46e50d7c88 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Fri, 21 Feb 2025 01:21:45 -0600 Subject: [PATCH 05/45] Test narwhals in CI (#17884) Contributes to #17662. @MarcoGorelli provided very helpful instructions for running the narwhals test suite. We should examine and correct the failing tests. We are down to 39 failures, shown in [this Kaggle notebook](https://www.kaggle.com/code/marcogorelli/testing-cudf-in-narwhals?scriptVersionId=220053914). Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Matthew Murray (https://github.com/Matt711) - Gil Forsyth (https://github.com/gforsyth) URL: https://github.com/rapidsai/cudf/pull/17884 --- .github/workflows/pr.yaml | 15 +++++++++++++ ci/test_narwhals.sh | 44 +++++++++++++++++++++++++++++++++++++++ dependencies.yaml | 10 +++++++++ 3 files changed, 69 insertions(+) create mode 100755 ci/test_narwhals.sh diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index e7a37a477b7..38b890893d0 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -40,6 +40,7 @@ jobs: - unit-tests-cudf-pandas - pandas-tests - pandas-tests-diff + - narwhals-tests - telemetry-setup - third-party-integration-tests-cudf-pandas secrets: inherit @@ -358,6 +359,20 @@ jobs: node_type: "cpu4" build_type: pull-request run_script: "ci/cudf_pandas_scripts/pandas-tests/diff.sh" + narwhals-tests: + needs: [conda-python-build, changed-files] + secrets: inherit + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-25.04 + if: fromJSON(needs.changed-files.outputs.changed_file_groups).test_python + with: + build_type: pull-request + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + node_type: "gpu-l4-latest-1" + continue-on-error: true + container_image: "rapidsai/ci-conda:latest" + run_script: ci/test_narwhals.sh spark-rapids-jni: needs: changed-files uses: ./.github/workflows/spark-rapids-jni.yaml diff --git a/ci/test_narwhals.sh b/ci/test_narwhals.sh new file mode 100755 index 00000000000..4a32ff0b0fd --- /dev/null +++ b/ci/test_narwhals.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Copyright (c) 2025, NVIDIA CORPORATION. + +# Support invoking test_python_cudf.sh outside the script directory +cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../ || exit 1 + +# Common setup steps shared by Python test jobs +source ./ci/test_python_common.sh test_python_narwhals + +rapids-logger "Check GPU usage" +nvidia-smi +rapids-print-env +EXITCODE=0 +trap "EXITCODE=1" ERR +set +e + +rapids-logger "pytest narwhals" +git clone https://github.com/narwhals-dev/narwhals --depth=1 +pushd narwhals || exit 1 +rapids-pip-retry install -U -e ".[dev]" + +rapids-logger "Check narwhals versions" +python -c "import narwhals; print(narwhals.show_versions())" + +rapids-logger "Run narwhals tests for cuDF" +python -m pytest \ + --cache-clear \ + --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf-narwhals.xml" \ + --numprocesses=8 \ + --dist=worksteal \ + --constructors=cudf + +rapids-logger "Run narwhals tests for cuDF Polars" +NARWHALS_POLARS_GPU=1 python -m pytest \ + --cache-clear \ + --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf-polars-narwhals.xml" \ + --numprocesses=8 \ + --dist=worksteal \ + --constructors=polars[lazy] + +popd || exit 1 + +rapids-logger "Test script exiting with value: $EXITCODE" +exit ${EXITCODE} diff --git a/dependencies.yaml b/dependencies.yaml index c8893fc8b49..585144300af 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -379,6 +379,16 @@ files: includes: - test_python_common - test_python_cudf_common + test_python_narwhals: + output: none + includes: + - cuda_version + - py_version + - test_python_common + - test_python_cudf_common + - test_python_cudf + - depends_on_cudf + - depends_on_cudf_polars channels: - rapidsai - rapidsai-nightly From 1fe744fb43044e9beb728c244f3f4a0beac3588f Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Fri, 21 Feb 2025 07:46:39 -0600 Subject: [PATCH 06/45] Update to nvcomp 4.2.0.11 (#18042) This updates to use nvcomp 4.2.0.11. The version is updated in rapids-cmake in https://github.com/rapidsai/rapids-cmake/pull/780. --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-128_arch-x86_64.yaml | 2 +- conda/recipes/libcudf/conda_build_config.yaml | 2 +- dependencies.yaml | 8 ++++---- python/libcudf/pyproject.toml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index cc01f5286ef..4df04b61bae 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -60,7 +60,7 @@ dependencies: - numpy>=1.23,<3.0a0 - numpydoc - nvcc_linux-64=11.8 -- nvcomp==4.1.0.6 +- nvcomp==4.2.0.11 - nvtx>=0.2.1 - openpyxl - packaging diff --git a/conda/environments/all_cuda-128_arch-x86_64.yaml b/conda/environments/all_cuda-128_arch-x86_64.yaml index f4cdbed9be6..a32f8b2ee25 100644 --- a/conda/environments/all_cuda-128_arch-x86_64.yaml +++ b/conda/environments/all_cuda-128_arch-x86_64.yaml @@ -58,7 +58,7 @@ dependencies: - numba>=0.59.1,<0.61.0a0 - numpy>=1.23,<3.0a0 - numpydoc -- nvcomp==4.1.0.6 +- nvcomp==4.2.0.11 - nvtx>=0.2.1 - openpyxl - packaging diff --git a/conda/recipes/libcudf/conda_build_config.yaml b/conda/recipes/libcudf/conda_build_config.yaml index 181064465ef..7c3f5777782 100644 --- a/conda/recipes/libcudf/conda_build_config.yaml +++ b/conda/recipes/libcudf/conda_build_config.yaml @@ -32,7 +32,7 @@ flatbuffers_version: - "=24.3.25" nvcomp_version: - - "=4.1.0.6" + - "=4.2.0.11" zlib_version: - ">=1.2.13" diff --git a/dependencies.yaml b/dependencies.yaml index 501128d278e..c59643273aa 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -432,7 +432,7 @@ dependencies: - output_types: conda packages: # Align nvcomp version with rapids-cmake - - nvcomp==4.1.0.6 + - nvcomp==4.2.0.11 specific: - output_types: [requirements, pyproject] matrices: @@ -440,12 +440,12 @@ dependencies: cuda: "12.*" use_cuda_wheels: "true" packages: - - nvidia-nvcomp-cu12==4.1.0.6 + - nvidia-nvcomp-cu12==4.2.0.11 - matrix: cuda: "11.*" use_cuda_wheels: "true" packages: - - nvidia-nvcomp-cu11==4.1.0.6 + - nvidia-nvcomp-cu11==4.2.0.11 # if use_cuda_wheels=false is provided, do not add dependencies on any CUDA wheels # (e.g. for DLFW and pip devcontainers) - matrix: @@ -455,7 +455,7 @@ dependencies: # (just as a source of documentation, as this populates pyproject.toml in source control) - matrix: packages: - - nvidia-nvcomp==4.1.0.6 + - nvidia-nvcomp==4.2.0.11 rapids_build_skbuild: common: - output_types: [conda, requirements, pyproject] diff --git a/python/libcudf/pyproject.toml b/python/libcudf/pyproject.toml index d16ad97ec54..8c2d5a8288d 100644 --- a/python/libcudf/pyproject.toml +++ b/python/libcudf/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ ] dependencies = [ "libkvikio==25.2.*,>=0.0.0a0", - "nvidia-nvcomp==4.1.0.6", + "nvidia-nvcomp==4.2.0.11", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. [project.urls] From 024f52bfec467acdeea2741b7081ca9cf856334f Mon Sep 17 00:00:00 2001 From: Eyal Soha <69258779+esoha-nvidia@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:35:51 -0700 Subject: [PATCH 07/45] Allow cudf::type_to_id() (#17831) Currently, `cudf::type_to_id` where X is a valid type for converting to a `cudf::id` only works if X is neither `const` nor `volatile`. However, if it has either of those "cv" qualifiers, the function returns `type_id::EMPTY`. With this change, return the correct type id no matter the cv-qualifiers. Authors: - Eyal Soha (https://github.com/esoha-nvidia) - David Wendt (https://github.com/davidwendt) Approvers: - Karthikeyan (https://github.com/karthikeyann) - Bradley Dice (https://github.com/bdice) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/17831 --- .../cudf/utilities/type_dispatcher.hpp | 51 +++++++++++++------ cpp/tests/types/type_dispatcher_test.cu | 8 ++- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/cpp/include/cudf/utilities/type_dispatcher.hpp b/cpp/include/cudf/utilities/type_dispatcher.hpp index c1dd79ef14f..d0aabee6344 100644 --- a/cpp/include/cudf/utilities/type_dispatcher.hpp +++ b/cpp/include/cudf/utilities/type_dispatcher.hpp @@ -46,14 +46,14 @@ namespace CUDF_EXPORT cudf { * For example: * * ``` - * return cudf::type_to_id(); // Returns INT32 + * return cudf::base_type_to_id(); // Returns INT32 * ``` * - * @tparam T The type to map to a `cudf::type_id` + * @tparam T The non-cv type to map to a `cudf::type_id` * @return The `cudf::type_id` corresponding to the specified type */ template -CUDF_HOST_DEVICE inline constexpr type_id type_to_id() +CUDF_HOST_DEVICE inline constexpr type_id base_type_to_id() { return type_id::EMPTY; }; @@ -114,20 +114,24 @@ using device_storage_type_t = // clang-format on /** - * @brief Checks if `fixed_point`-like types have template type `T` matching the column's - * stored type id + * @brief Maps a C++ type to its corresponding `cudf::type_id` * - * @tparam T The type that is stored on the device - * @param id The `data_type::id` of the column - * @return `true` If T matches the stored column `type_id` - * @return `false` If T does not match the stored column `type_id` + * When explicitly passed a template argument of a given type, returns the + * appropriate `type_id` enum for the specified C++ type. + * + * For example: + * + * ``` + * return cudf::type_to_id(); // Returns INT32 + * ``` + * + * @tparam T The type to map to a `cudf::type_id` + * @return The `cudf::type_id` corresponding to the specified type */ template -constexpr bool type_id_matches_device_storage_type(type_id id) +constexpr inline type_id type_to_id() { - return (id == type_id::DECIMAL32 && std::is_same_v) || - (id == type_id::DECIMAL64 && std::is_same_v) || - (id == type_id::DECIMAL128 && std::is_same_v) || id == type_to_id(); + return base_type_to_id>(); } /** @@ -140,7 +144,7 @@ constexpr bool type_id_matches_device_storage_type(type_id id) #ifndef CUDF_TYPE_MAPPING #define CUDF_TYPE_MAPPING(Type, Id) \ template <> \ - constexpr inline type_id type_to_id() \ + constexpr inline type_id base_type_to_id() \ { \ return Id; \ } \ @@ -194,11 +198,28 @@ CUDF_TYPE_MAPPING(cudf::struct_view, type_id::STRUCT) * @return id for 'char' type */ template <> // CUDF_TYPE_MAPPING(char,INT8) causes duplicate id_to_type_impl definition -constexpr inline type_id type_to_id() +constexpr inline type_id base_type_to_id() { return type_id::INT8; } +/** + * @brief Checks if `fixed_point`-like types have template type `T` matching the column's + * stored type id + * + * @tparam T The type that is stored on the device + * @param id The `data_type::id` of the column + * @return `true` If T matches the stored column `type_id` + * @return `false` If T does not match the stored column `type_id` + */ +template +constexpr bool type_id_matches_device_storage_type(type_id id) +{ + return (id == type_id::DECIMAL32 && std::is_same_v) || + (id == type_id::DECIMAL64 && std::is_same_v) || + (id == type_id::DECIMAL128 && std::is_same_v) || id == type_to_id(); +} + /** * @brief Use this specialization on `type_dispatcher` whenever you only need to operate on the * underlying stored type. diff --git a/cpp/tests/types/type_dispatcher_test.cu b/cpp/tests/types/type_dispatcher_test.cu index f18e9afc09c..ddd318710a4 100644 --- a/cpp/tests/types/type_dispatcher_test.cu +++ b/cpp/tests/types/type_dispatcher_test.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,6 +50,12 @@ TYPED_TEST(TypedDispatcherTest, TypeToId) { EXPECT_TRUE(cudf::type_dispatcher(cudf::data_type{cudf::type_to_id()}, type_tester{})); + EXPECT_TRUE(cudf::type_dispatcher(cudf::data_type{cudf::type_to_id()}, + type_tester{})); + EXPECT_TRUE(cudf::type_dispatcher(cudf::data_type{cudf::type_to_id()}, + type_tester{})); + EXPECT_TRUE(cudf::type_dispatcher(cudf::data_type{cudf::type_to_id()}, + type_tester{})); } namespace { From 90dc38c84a5e712f60c5253c85f58ef508c0adcd Mon Sep 17 00:00:00 2001 From: Tanmay Gujar Date: Fri, 21 Feb 2025 10:28:58 -0800 Subject: [PATCH 08/45] Fix memcopy direction for concatenate (#18058) `cudaMemcpyAsync` can return a `cudaError_t` value which should be checked for runtime errors. We should preserve the behavior of `thrust::copy` which got replaced with the `cudaMemcpyAsync` call in #17584. The driver may do the right thing and infer the source and destination pointer location instead of using the `cudaMemcpyKind`, but this still leads to weird circumstances where the copy type in code is DtoD while the actual copy at runtime is HtoD Authors: - Tanmay Gujar (https://github.com/tgujar) Approvers: - Bradley Dice (https://github.com/bdice) - Karthikeyan (https://github.com/karthikeyann) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/18058 --- cpp/src/copying/concatenate.cu | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cpp/src/copying/concatenate.cu b/cpp/src/copying/concatenate.cu index 6fc49afd7ac..4237e3f0954 100644 --- a/cpp/src/copying/concatenate.cu +++ b/cpp/src/copying/concatenate.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024, NVIDIA CORPORATION. + * Copyright (c) 2020-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -308,11 +308,11 @@ std::unique_ptr for_each_concatenate(host_span views, auto count = 0; for (auto& v : views) { - cudaMemcpyAsync(m_view.begin() + count, - v.begin(), - v.size() * sizeof(T), - cudaMemcpyDeviceToDevice, - stream.value()); + CUDF_CUDA_TRY(cudaMemcpyAsync(m_view.begin() + count, + v.begin(), + v.size() * sizeof(T), + cudaMemcpyDefault, + stream.value())); count += v.size(); } From 1a95a0c8bbdc3164db8e76d0c1fc4b68424f41b2 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Fri, 21 Feb 2025 13:42:28 -0800 Subject: [PATCH 09/45] Host Snappy compression (#17824) Issue #17641 Simple implementation of the algorithm based on https://github.com/google/snappy/blob/main/snappy.cc. Other changes: - Limited the number of threads in the compression thread pool to 32, based on benchmark results; - Submit tasks to the thread pool starting from the largest buffers to improve CPU utilization (impact verified with benchmark results); - Changed the environment variable from `LIBCUDF_USE_HOST_COMPRESSION` to `LIBCUDF_HOST_COMPRESSION`, and its type to string, to allow potential future settings "OFF", "AUTO" and "ON". Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Basit Ayantunde (https://github.com/lamarrr) - Shruti Shivakumar (https://github.com/shrshi) - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cudf/pull/17824 --- cpp/src/io/comp/comp.cpp | 247 +++++++++++++++--- cpp/src/io/utilities/getenv_or.hpp | 2 +- cpp/tests/CMakeLists.txt | 2 +- .../comp/{decomp_test.cpp => comp_test.cpp} | 40 ++- 4 files changed, 247 insertions(+), 44 deletions(-) rename cpp/tests/io/comp/{decomp_test.cpp => comp_test.cpp} (86%) diff --git a/cpp/src/io/comp/comp.cpp b/cpp/src/io/comp/comp.cpp index 3800835eaf1..280c07a4ff1 100644 --- a/cpp/src/io/comp/comp.cpp +++ b/cpp/src/io/comp/comp.cpp @@ -18,7 +18,6 @@ #include "gpuinflate.hpp" #include "io/utilities/getenv_or.hpp" -#include "io/utilities/hostdevice_vector.hpp" #include "nvcomp_adapter.hpp" #include @@ -32,14 +31,17 @@ #include #include // GZIP compression +#include + namespace cudf::io::detail { namespace { auto& h_comp_pool() { - static std::size_t pool_size = - getenv_or("LIBCUDF_HOST_COMPRESSION_NUM_THREADS", std::thread::hardware_concurrency()); + static const std::size_t default_pool_size = std::min(32u, std::thread::hardware_concurrency()); + static const std::size_t pool_size = + getenv_or("LIBCUDF_HOST_COMPRESSION_NUM_THREADS", default_pool_size); static BS::thread_pool pool(pool_size); return pool; } @@ -92,35 +94,199 @@ std::vector compress_gzip(host_span src) return dst; } -/** - * @brief SNAPPY device compressor - */ -std::vector compress_snappy(host_span src, - rmm::cuda_stream_view stream) +namespace snappy { + +template +[[nodiscard]] T load(uint8_t const* ptr) +{ + T value; + std::memcpy(&value, ptr, sizeof(T)); + return value; +} + +class hash_table { + std::vector tbl; + static constexpr int hash_table_bits = 15; + + public: + hash_table() : tbl(1 << hash_table_bits, 0) {} + + void clear() { std::fill(tbl.begin(), tbl.end(), 0); } + + [[nodiscard]] uint16_t* entry(uint32_t bytes) + { + constexpr uint32_t multiplier = 0x1e35a7bd; + auto const hash = (bytes * multiplier) >> (31 - hash_table_bits); + return tbl.data() + hash / sizeof(uint16_t); + } +}; + +uint8_t* emit_literal(uint8_t* out_begin, uint8_t const* literal_begin, uint8_t const* literal_end) +{ + auto const literal_size = literal_end - literal_begin; + if (literal_size == 0) { return out_begin; } + auto const n = literal_size - 1; + + auto out_it = out_begin; + if (n < 60) { + // Fits into a single tag byte + *out_it++ = n << 2; + } else { + auto const log2_n = 31 - __builtin_clz(n); + auto const count = (log2_n >> 3) + 1; + *out_it++ = (59 + count) << 2; + std::memcpy(out_it, &n, count); + out_it += count; + } + std::memcpy(out_it, literal_begin, literal_size); + return out_it + literal_size; +} + +uint8_t* emit_copy(uint8_t* out_begin, size_t offset, size_t len) +{ + while (len > 0) { + auto const copy_len = std::min(len, 64ul); + auto const out_val = 2 + ((copy_len - 1) << 2) + (offset << 8); + std::memcpy(out_begin, &out_val, 3); + + out_begin += 3; + len -= copy_len; + } + return out_begin; +} + +size_t compress_block(host_span input, hash_table& table, host_span output) +{ + auto const [in_remain, out_remain] = [&]() -> std::pair { + auto in_it = input.begin(); + auto out_it = output.begin(); + + // The algorithm reads 8 bytes at a time, so we need to ensure there are at least 8 bytes + auto const input_max = input.end() - sizeof(uint64_t); + while (in_it < input_max) { + auto const next_emit = in_it++; + auto data = load(in_it); + uint32_t stride = 1; + uint8_t const* candidate = nullptr; + + auto word_match_found = [&]() { + if (input_max - in_it < 16) { return false; } + for (size_t word_idx = 0; word_idx < 4; ++word_idx) { + for (size_t byte_idx = 0; byte_idx < sizeof(uint32_t); ++byte_idx) { + auto const offset = sizeof(uint32_t) * word_idx + byte_idx; + auto* const entry = table.entry(static_cast(data)); + candidate = input.begin() + *entry; + *entry = in_it - input.data() + offset; + + if (load(candidate) == static_cast(data)) { + *(out_it++) = offset * sizeof(uint32_t); + std::memcpy(out_it, next_emit, offset + 1); + in_it += offset; + out_it += offset + 1; + stride = 1; + return true; + } + data >>= 8; + } + // Fetch the next eight bytes + data = load(in_it + sizeof(uint32_t) * (word_idx + 1)); + } + in_it += 16; + return false; + }(); + + if (not word_match_found) { + // keep looking for a match with increasing stride + while (true) { + auto* const entry = table.entry(static_cast(data)); + candidate = input.begin() + *entry; + *entry = in_it - input.begin(); + if (static_cast(data) == load(candidate)) { + stride = 1; + break; + } + + auto const next_input = in_it + stride; + if (next_input > input_max) { + // Reached the end of the input without finding a match + return {next_emit, out_it}; + } + + data = load(next_input); + in_it = next_input; + stride += 1; + } + + // Emit data prior to the match as literal + out_it = emit_literal(out_it, next_emit, in_it); + } + + // Emit match(es) + do { + auto const match_len = std::mismatch(in_it, input.end(), candidate).first - in_it; + out_it = emit_copy(out_it, in_it - candidate, match_len); + + in_it += match_len; + if (in_it >= input_max) { + // Reached the end of the input, no more matches to look for + return {in_it, out_it}; + } + data = load(in_it); + *table.entry(load(in_it - 1)) = in_it - input.begin() - 1; + auto* const entry = table.entry(data); + candidate = input.begin() + *entry; + *entry = in_it - input.begin(); + + } while (static_cast(data) == load(candidate)); + } + + return {in_it, out_it}; + }(); + + // Emit the remaining data as a literal + return emit_literal(out_remain, in_remain, input.end()) - output.begin(); +} + +void append_varint(std::vector& output, size_t v) +{ + while (v > 127) { + output.push_back((v & 0x7F) | 0x80); + v >>= 7; + } + output.push_back(v); +} + +[[nodiscard]] std::vector compress(host_span src) { - auto const d_src = - cudf::detail::make_device_uvector_async(src, stream, cudf::get_current_device_resource_ref()); - cudf::detail::hostdevice_vector> inputs(1, stream); - inputs[0] = d_src; - inputs.host_to_device_async(stream); - - auto dst_size = compress_max_output_chunk_size(nvcomp::compression_type::SNAPPY, src.size()); - rmm::device_uvector d_dst(dst_size, stream); - cudf::detail::hostdevice_vector> outputs(1, stream); - outputs[0] = d_dst; - outputs.host_to_device_async(stream); - - cudf::detail::hostdevice_vector hd_status(1, stream); - hd_status[0] = {}; - hd_status.host_to_device_async(stream); - - nvcomp::batched_compress(nvcomp::compression_type::SNAPPY, inputs, outputs, hd_status, stream); - - hd_status.device_to_host_sync(stream); - CUDF_EXPECTS(hd_status[0].status == compression_status::SUCCESS, "snappy compression failed"); - return cudf::detail::make_std_vector_sync(d_dst, stream); + std::vector dst; + append_varint(dst, src.size()); + dst.reserve(dst.size() + max_compressed_size(compression_type::SNAPPY, src.size())); + + hash_table table; // reuse hash table across blocks + constexpr size_t block_size = 1 << 16; + auto const block_max_compressed_size = max_compressed_size(compression_type::SNAPPY, block_size); + for (std::size_t src_offset = 0; src_offset < src.size(); src_offset += block_size) { + // Compress data in blocks of limited size + auto const block = src.subspan(src_offset, std::min(src.size() - src_offset, block_size)); + + auto const previous_size = dst.size(); + auto const curr_block_max_comp_size = + (block.size() == block_size) ? block_max_compressed_size + : max_compressed_size(compression_type::SNAPPY, block.size()); + dst.resize(previous_size + curr_block_max_comp_size); + auto const block_dst = + host_span{dst.data() + previous_size, dst.size() - previous_size}; + + table.clear(); + auto const comp_block_size = compress_block(block, table, block_dst); + dst.resize(previous_size + comp_block_size); + } + + return dst; } +} // namespace snappy + void device_compress(compression_type compression, device_span const> inputs, device_span const> outputs, @@ -156,6 +322,13 @@ void host_compress(compression_type compression, auto const h_outputs = cudf::detail::make_host_vector_async(outputs, stream); stream.synchronize(); + // Generate order vector to submit largest tasks first + std::vector task_order(num_chunks); + std::iota(task_order.begin(), task_order.end(), 0); + std::sort(task_order.begin(), task_order.end(), [&](size_t a, size_t b) { + return h_inputs[a].size() > h_inputs[b].size(); + }); + std::vector> tasks; auto const num_streams = std::min({num_chunks, @@ -163,9 +336,12 @@ void host_compress(compression_type compression, h_comp_pool().get_thread_count()}); auto const streams = cudf::detail::fork_streams(stream, num_streams); for (size_t i = 0; i < num_chunks; ++i) { + auto const idx = task_order[i]; auto const cur_stream = streams[i % streams.size()]; - auto task = [d_in = h_inputs[i], d_out = h_outputs[i], cur_stream, compression]() -> size_t { - auto const h_in = cudf::detail::make_host_vector_sync(d_in, cur_stream); + auto task = + [d_in = h_inputs[idx], d_out = h_outputs[idx], cur_stream, compression]() -> size_t { + auto h_in = cudf::detail::make_pinned_vector_async(d_in.size(), cur_stream); + cudf::detail::cuda_memcpy(h_in, d_in, cur_stream); auto const h_out = compress(compression, h_in, cur_stream); cudf::detail::cuda_memcpy(d_out.subspan(0, h_out.size()), h_out, cur_stream); return h_out.size(); @@ -174,7 +350,7 @@ void host_compress(compression_type compression, } for (auto i = 0ul; i < num_chunks; ++i) { - h_results[i] = {tasks[i].get(), compression_status::SUCCESS}; + h_results[task_order[i]] = {tasks[i].get(), compression_status::SUCCESS}; } cudf::detail::cuda_memcpy_async(results, h_results, stream); } @@ -183,6 +359,7 @@ void host_compress(compression_type compression, { switch (compression) { case compression_type::GZIP: + case compression_type::SNAPPY: case compression_type::NONE: return true; default: return false; } @@ -212,7 +389,7 @@ void host_compress(compression_type compression, if (not host_compression_supported(compression)) { return false; } if (not device_compression_supported(compression)) { return true; } // If both host and device compression are supported, use the host if the env var is set - return getenv_or("LIBCUDF_USE_HOST_COMPRESSION", 0); + return getenv_or("LIBCUDF_HOST_COMPRESSION", std::string{"OFF"}) == "ON"; } } // namespace @@ -249,12 +426,12 @@ std::optional compress_max_allowed_chunk_size(compression_type compressi std::vector compress(compression_type compression, host_span src, - rmm::cuda_stream_view stream) + rmm::cuda_stream_view) { CUDF_FUNC_RANGE(); switch (compression) { case compression_type::GZIP: return compress_gzip(src); - case compression_type::SNAPPY: return compress_snappy(src, stream); + case compression_type::SNAPPY: return snappy::compress(src); default: CUDF_FAIL("Unsupported compression type"); } } diff --git a/cpp/src/io/utilities/getenv_or.hpp b/cpp/src/io/utilities/getenv_or.hpp index acfd2221797..4d5c3ec6d22 100644 --- a/cpp/src/io/utilities/getenv_or.hpp +++ b/cpp/src/io/utilities/getenv_or.hpp @@ -45,7 +45,7 @@ T getenv_or(std::string_view env_var_name, T default_val) ss.str()); } - if (env_val == nullptr) { return default_val; } + if (env_val == nullptr) { return std::move(default_val); } std::stringstream sstream(env_val); T converted_val; diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index fd8cb3f22f2..cfc6a0dc425 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -298,7 +298,7 @@ ConfigureTest( # ################################################################################################## # * io tests -------------------------------------------------------------------------------------- -ConfigureTest(DECOMPRESSION_TEST io/comp/decomp_test.cpp) +ConfigureTest(COMPRESSION_TEST io/comp/comp_test.cpp) ConfigureTest(ROW_SELECTION_TEST io/row_selection_test.cpp) ConfigureTest( diff --git a/cpp/tests/io/comp/decomp_test.cpp b/cpp/tests/io/comp/comp_test.cpp similarity index 86% rename from cpp/tests/io/comp/decomp_test.cpp rename to cpp/tests/io/comp/comp_test.cpp index 5bbe8b63c47..e3bee708485 100644 --- a/cpp/tests/io/comp/decomp_test.cpp +++ b/cpp/tests/io/comp/comp_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ * limitations under the License. */ +#include "io/comp/comp.hpp" #include "io/comp/gpuinflate.hpp" +#include "io/comp/io_uncomp.hpp" #include "io/utilities/hostdevice_vector.hpp" #include @@ -34,6 +36,12 @@ using cudf::io::detail::compression_result; using cudf::io::detail::compression_status; namespace nvcomp = cudf::io::detail::nvcomp; +[[nodiscard]] std::vector vector_from_string(std::string const& str) +{ + return {reinterpret_cast(str.data()), + reinterpret_cast(str.data() + str.size())}; +} + /** * @brief Base test fixture for decompression * @@ -42,12 +50,6 @@ namespace nvcomp = cudf::io::detail::nvcomp; */ template struct DecompressTest : public cudf::test::BaseFixture { - [[nodiscard]] std::vector vector_from_string(std::string const str) const - { - return {reinterpret_cast(str.c_str()), - reinterpret_cast(str.c_str()) + strlen(str.c_str())}; - } - void Decompress(std::vector& decompressed, uint8_t const* compressed, size_t compressed_size) @@ -76,6 +78,11 @@ struct DecompressTest : public cudf::test::BaseFixture { } }; +struct HostCompressTest : public cudf::test::BaseFixture { + HostCompressTest() { setenv("LIBCUDF_HOST_COMPRESSION", "ON", 1); } + ~HostCompressTest() override { unsetenv("LIBCUDF_HOST_COMPRESSION"); } +}; + /** * @brief Derived fixture for GZIP decompression */ @@ -222,4 +229,23 @@ TEST_F(NvcompConfigTest, Decompression) EXPECT_TRUE(decomp_disabled(compression_type::SNAPPY, {false, false})); } +TEST_F(HostCompressTest, SnappyCompression) +{ + std::vector expected; + expected.reserve(8 * (32 << 20)); + for (size_t size = 1; size < 32 << 20; size *= 2) { + // Using number strings to generate data that is compressible, but not trivially so + for (size_t i = size / 2; i < size; ++i) { + auto const num_string = std::to_string(i); + // Keep adding to the test data + expected.insert(expected.end(), num_string.begin(), num_string.end()); + } + auto const compressed = cudf::io::detail::compress( + cudf::io::compression_type::SNAPPY, expected, cudf::get_default_stream()); + auto const decompressed = + cudf::io::detail::decompress(cudf::io::compression_type::SNAPPY, compressed); + EXPECT_EQ(expected, decompressed); + } +} + CUDF_TEST_PROGRAM_MAIN() From 4be30a1104b57726f50a35182f23de6285a245bf Mon Sep 17 00:00:00 2001 From: Robert Maynard Date: Sat, 22 Feb 2025 00:16:33 -0500 Subject: [PATCH 10/45] Require CMake 3.30.4 (#18007) Update CMake minimum required to 3.30.4 across all of RAPIDS Authors: - Robert Maynard (https://github.com/robertmaynard) - Nghia Truong (https://github.com/ttnghia) Approvers: - Nghia Truong (https://github.com/ttnghia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/18007 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 2 +- conda/environments/all_cuda-128_arch-x86_64.yaml | 2 +- conda/recipes/cudf/conda_build_config.yaml | 2 +- conda/recipes/cudf_kafka/conda_build_config.yaml | 2 +- conda/recipes/libcudf/conda_build_config.yaml | 2 +- conda/recipes/pylibcudf/conda_build_config.yaml | 2 +- cpp/CMakeLists.txt | 2 +- cpp/examples/basic/CMakeLists.txt | 2 +- cpp/examples/billion_rows/CMakeLists.txt | 2 +- cpp/examples/interop/CMakeLists.txt | 2 +- cpp/examples/nested_types/CMakeLists.txt | 2 +- cpp/examples/parquet_io/CMakeLists.txt | 2 +- cpp/examples/strings/CMakeLists.txt | 2 +- cpp/libcudf_kafka/CMakeLists.txt | 4 ++-- dependencies.yaml | 2 +- java/ci/Dockerfile.rocky | 2 +- java/src/main/native/CMakeLists.txt | 2 +- python/cudf/CMakeLists.txt | 4 ++-- python/cudf/pyproject.toml | 2 +- python/cudf/udf_cpp/CMakeLists.txt | 4 ++-- python/cudf_kafka/CMakeLists.txt | 4 ++-- python/cudf_kafka/pyproject.toml | 2 +- python/libcudf/CMakeLists.txt | 2 +- python/libcudf/pyproject.toml | 2 +- python/pylibcudf/CMakeLists.txt | 4 ++-- python/pylibcudf/pyproject.toml | 2 +- 26 files changed, 31 insertions(+), 31 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index 21cc9b8c9e2..cc674732ba4 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -15,7 +15,7 @@ dependencies: - cachetools - clang-tools=16.0.6 - clang==16.0.6 -- cmake>=3.26.4,!=3.30.0 +- cmake>=3.30.4 - cramjam - cubinlinker - cuda-nvtx=11.8 diff --git a/conda/environments/all_cuda-128_arch-x86_64.yaml b/conda/environments/all_cuda-128_arch-x86_64.yaml index 939d6ff9eb9..7593a72cc68 100644 --- a/conda/environments/all_cuda-128_arch-x86_64.yaml +++ b/conda/environments/all_cuda-128_arch-x86_64.yaml @@ -15,7 +15,7 @@ dependencies: - cachetools - clang-tools=16.0.6 - clang==16.0.6 -- cmake>=3.26.4,!=3.30.0 +- cmake>=3.30.4 - cramjam - cuda-cudart-dev - cuda-nvcc diff --git a/conda/recipes/cudf/conda_build_config.yaml b/conda/recipes/cudf/conda_build_config.yaml index a4a6a0910ce..bab277b8f60 100644 --- a/conda/recipes/cudf/conda_build_config.yaml +++ b/conda/recipes/cudf/conda_build_config.yaml @@ -13,7 +13,7 @@ c_stdlib_version: - "2.28" cmake_version: - - ">=3.26.4,!=3.30.0" + - ">=3.30.4" cuda_compiler: - cuda-nvcc # [not os.environ.get("RAPIDS_CUDA_VERSION", "").startswith("11")] diff --git a/conda/recipes/cudf_kafka/conda_build_config.yaml b/conda/recipes/cudf_kafka/conda_build_config.yaml index a4a6a0910ce..bab277b8f60 100644 --- a/conda/recipes/cudf_kafka/conda_build_config.yaml +++ b/conda/recipes/cudf_kafka/conda_build_config.yaml @@ -13,7 +13,7 @@ c_stdlib_version: - "2.28" cmake_version: - - ">=3.26.4,!=3.30.0" + - ">=3.30.4" cuda_compiler: - cuda-nvcc # [not os.environ.get("RAPIDS_CUDA_VERSION", "").startswith("11")] diff --git a/conda/recipes/libcudf/conda_build_config.yaml b/conda/recipes/libcudf/conda_build_config.yaml index 4d75646da78..48b2acf3a02 100644 --- a/conda/recipes/libcudf/conda_build_config.yaml +++ b/conda/recipes/libcudf/conda_build_config.yaml @@ -17,7 +17,7 @@ c_stdlib_version: - "2.28" cmake_version: - - ">=3.26.4,!=3.30.0" + - ">=3.30.4" dlpack_version: - ">=0.8,<1.0" diff --git a/conda/recipes/pylibcudf/conda_build_config.yaml b/conda/recipes/pylibcudf/conda_build_config.yaml index a4a6a0910ce..bab277b8f60 100644 --- a/conda/recipes/pylibcudf/conda_build_config.yaml +++ b/conda/recipes/pylibcudf/conda_build_config.yaml @@ -13,7 +13,7 @@ c_stdlib_version: - "2.28" cmake_version: - - ">=3.26.4,!=3.30.0" + - ">=3.30.4" cuda_compiler: - cuda-nvcc # [not os.environ.get("RAPIDS_CUDA_VERSION", "").startswith("11")] diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 2e4dd21667e..bb4d20f837c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../rapids_config.cmake) include(rapids-cmake) diff --git a/cpp/examples/basic/CMakeLists.txt b/cpp/examples/basic/CMakeLists.txt index 8e89b461e30..455494a40eb 100644 --- a/cpp/examples/basic/CMakeLists.txt +++ b/cpp/examples/basic/CMakeLists.txt @@ -1,6 +1,6 @@ # Copyright (c) 2020-2025, NVIDIA CORPORATION. -cmake_minimum_required(VERSION 3.26.4) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../set_cuda_architecture.cmake) diff --git a/cpp/examples/billion_rows/CMakeLists.txt b/cpp/examples/billion_rows/CMakeLists.txt index 603c8d0b457..f7dbd3e79b1 100644 --- a/cpp/examples/billion_rows/CMakeLists.txt +++ b/cpp/examples/billion_rows/CMakeLists.txt @@ -1,6 +1,6 @@ # Copyright (c) 2024-2025, NVIDIA CORPORATION. -cmake_minimum_required(VERSION 3.26.4) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../set_cuda_architecture.cmake) diff --git a/cpp/examples/interop/CMakeLists.txt b/cpp/examples/interop/CMakeLists.txt index 6f1249beaaa..37a55b98093 100644 --- a/cpp/examples/interop/CMakeLists.txt +++ b/cpp/examples/interop/CMakeLists.txt @@ -1,6 +1,6 @@ # Copyright (c) 2024-2025, NVIDIA CORPORATION. -cmake_minimum_required(VERSION 3.26.4) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../set_cuda_architecture.cmake) diff --git a/cpp/examples/nested_types/CMakeLists.txt b/cpp/examples/nested_types/CMakeLists.txt index e7972d1531b..4df41f2acd6 100644 --- a/cpp/examples/nested_types/CMakeLists.txt +++ b/cpp/examples/nested_types/CMakeLists.txt @@ -1,6 +1,6 @@ # Copyright (c) 2023-2025, NVIDIA CORPORATION. -cmake_minimum_required(VERSION 3.26.4) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../set_cuda_architecture.cmake) diff --git a/cpp/examples/parquet_io/CMakeLists.txt b/cpp/examples/parquet_io/CMakeLists.txt index 17f86fdf5e0..da12b7056fb 100644 --- a/cpp/examples/parquet_io/CMakeLists.txt +++ b/cpp/examples/parquet_io/CMakeLists.txt @@ -1,6 +1,6 @@ # Copyright (c) 2024-2025, NVIDIA CORPORATION. -cmake_minimum_required(VERSION 3.26.4) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../set_cuda_architecture.cmake) diff --git a/cpp/examples/strings/CMakeLists.txt b/cpp/examples/strings/CMakeLists.txt index 9010d495715..a0831488d60 100644 --- a/cpp/examples/strings/CMakeLists.txt +++ b/cpp/examples/strings/CMakeLists.txt @@ -1,6 +1,6 @@ # Copyright (c) 2022-2025, NVIDIA CORPORATION. -cmake_minimum_required(VERSION 3.26.4) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../set_cuda_architecture.cmake) diff --git a/cpp/libcudf_kafka/CMakeLists.txt b/cpp/libcudf_kafka/CMakeLists.txt index 9760ecfe067..26c81e7fd2f 100644 --- a/cpp/libcudf_kafka/CMakeLists.txt +++ b/cpp/libcudf_kafka/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2018-2024, NVIDIA CORPORATION. +# Copyright (c) 2018-2025, 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 @@ -11,7 +11,7 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../../rapids_config.cmake) include(rapids-cmake) diff --git a/dependencies.yaml b/dependencies.yaml index 83f4e96c748..e7840d56880 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -400,7 +400,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - &cmake_ver cmake>=3.26.4,!=3.30.0 + - &cmake_ver cmake>=3.30.4 - &ninja ninja build_all: common: diff --git a/java/ci/Dockerfile.rocky b/java/ci/Dockerfile.rocky index 9f3305278cb..277e33bb8eb 100644 --- a/java/ci/Dockerfile.rocky +++ b/java/ci/Dockerfile.rocky @@ -33,7 +33,7 @@ RUN dnf --enablerepo=powertools install -y scl-utils gcc-toolset-${TOOLSET_VERS RUN mkdir /usr/local/rapids /rapids && chmod 777 /usr/local/rapids /rapids # 3.22.3+: CUDA architecture 'native' support + flexible CMAKE__*_LAUNCHER for ccache -ARG CMAKE_VERSION=3.28.6 +ARG CMAKE_VERSION=3.30.7 # default x86_64 from x86 build, aarch64 cmake for arm build ARG CMAKE_ARCH=x86_64 RUN cd /usr/local && wget --quiet https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-${CMAKE_ARCH}.tar.gz && \ diff --git a/java/src/main/native/CMakeLists.txt b/java/src/main/native/CMakeLists.txt index 3923d8b45e3..1fa6f6d561f 100644 --- a/java/src/main/native/CMakeLists.txt +++ b/java/src/main/native/CMakeLists.txt @@ -11,7 +11,7 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../../../../rapids_config.cmake) include(rapids-cmake) diff --git a/python/cudf/CMakeLists.txt b/python/cudf/CMakeLists.txt index 7193ada5b93..2a17bc5dbb7 100644 --- a/python/cudf/CMakeLists.txt +++ b/python/cudf/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2022-2025, 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 @@ -12,7 +12,7 @@ # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../../rapids_config.cmake) include(rapids-cuda) diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index d716114cf7e..16cd97677ef 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -118,7 +118,7 @@ build-backend = "scikit_build_core.build" dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true" requires = [ - "cmake>=3.26.4,!=3.30.0", + "cmake>=3.30.4", "cython>=3.0.3", "libcudf==25.4.*,>=0.0.0a0", "librmm==25.4.*,>=0.0.0a0", diff --git a/python/cudf/udf_cpp/CMakeLists.txt b/python/cudf/udf_cpp/CMakeLists.txt index fa7855cfc65..9f6b67d0cdc 100644 --- a/python/cudf/udf_cpp/CMakeLists.txt +++ b/python/cudf/udf_cpp/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2022-2025, 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 @@ -12,7 +12,7 @@ # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(rapids-cmake) include(rapids-cpm) diff --git a/python/cudf_kafka/CMakeLists.txt b/python/cudf_kafka/CMakeLists.txt index fd835010c4e..3e12eb6aa41 100644 --- a/python/cudf_kafka/CMakeLists.txt +++ b/python/cudf_kafka/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2022-2025, 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 @@ -12,7 +12,7 @@ # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../../rapids_config.cmake) diff --git a/python/cudf_kafka/pyproject.toml b/python/cudf_kafka/pyproject.toml index 4a7143e1134..424010e632c 100644 --- a/python/cudf_kafka/pyproject.toml +++ b/python/cudf_kafka/pyproject.toml @@ -83,7 +83,7 @@ build-backend = "scikit_build_core.build" dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true" requires = [ - "cmake>=3.26.4,!=3.30.0", + "cmake>=3.30.4", "cython>=3.0.3", "ninja", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. diff --git a/python/libcudf/CMakeLists.txt b/python/libcudf/CMakeLists.txt index 259492b98d1..d5450639471 100644 --- a/python/libcudf/CMakeLists.txt +++ b/python/libcudf/CMakeLists.txt @@ -12,7 +12,7 @@ # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../../rapids_config.cmake) diff --git a/python/libcudf/pyproject.toml b/python/libcudf/pyproject.toml index 18aa824c6df..01fe6097936 100644 --- a/python/libcudf/pyproject.toml +++ b/python/libcudf/pyproject.toml @@ -79,7 +79,7 @@ build-backend = "scikit_build_core.build" dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true;use_cuda_wheels=true" requires = [ - "cmake>=3.26.4,!=3.30.0", + "cmake>=3.30.4", "libkvikio==25.4.*,>=0.0.0a0", "librmm==25.4.*,>=0.0.0a0", "ninja", diff --git a/python/pylibcudf/CMakeLists.txt b/python/pylibcudf/CMakeLists.txt index a4b831790fb..fe6e73a3f14 100644 --- a/python/pylibcudf/CMakeLists.txt +++ b/python/pylibcudf/CMakeLists.txt @@ -1,5 +1,5 @@ # ============================================================================= -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2022-2025, 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 @@ -12,7 +12,7 @@ # the License. # ============================================================================= -cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR) +cmake_minimum_required(VERSION 3.30.4 FATAL_ERROR) include(../../rapids_config.cmake) include(rapids-cuda) diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index 2f846b5f0b9..939da65c1ec 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -109,7 +109,7 @@ build-backend = "scikit_build_core.build" dependencies-file = "../../dependencies.yaml" matrix-entry = "cuda_suffixed=true" requires = [ - "cmake>=3.26.4,!=3.30.0", + "cmake>=3.30.4", "cython>=3.0.3", "libcudf==25.4.*,>=0.0.0a0", "librmm==25.4.*,>=0.0.0a0", From d0e219ef36e51e07573c7650819a26808b60c9a4 Mon Sep 17 00:00:00 2001 From: ustcfy <96854327+ustcfy@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:07:30 +0800 Subject: [PATCH 11/45] [FEA] Expose `stripe_size_rows` setting for `ORCWriterOptions` (#17927) closes https://github.com/rapidsai/cudf/issues/17785 This PR exposes the `stripe_size_rows` setting for the `ORCWriterOptions` Java interface. Exposing this interface is solely for the convenience of conducting some tests. Authors: - https://github.com/ustcfy - Nghia Truong (https://github.com/ttnghia) Approvers: - Nghia Truong (https://github.com/ttnghia) - Chong Gao (https://github.com/res-life) URL: https://github.com/rapidsai/cudf/pull/17927 --- .../java/ai/rapids/cudf/ORCWriterOptions.java | 19 ++++++++++++++++++- java/src/main/java/ai/rapids/cudf/Table.java | 4 ++++ java/src/main/native/src/TableJni.cpp | 4 ++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/java/src/main/java/ai/rapids/cudf/ORCWriterOptions.java b/java/src/main/java/ai/rapids/cudf/ORCWriterOptions.java index 372f919532e..009f5e12815 100644 --- a/java/src/main/java/ai/rapids/cudf/ORCWriterOptions.java +++ b/java/src/main/java/ai/rapids/cudf/ORCWriterOptions.java @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2019-2021, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,17 +23,34 @@ * that will be used by the ORC writer to write the file. */ public class ORCWriterOptions extends CompressionMetadataWriterOptions { + private int stripeSizeRows; private ORCWriterOptions(Builder builder) { super(builder); + this.stripeSizeRows = builder.stripeSizeRows; } public static Builder builder() { return new Builder(); } + public int getStripeSizeRows() { + return stripeSizeRows; + } + public static class Builder extends CompressionMetadataWriterOptions.Builder { + // < 1M rows default orc stripe rows, defined in cudf/cpp/include/cudf/io/orc.hpp + private int stripeSizeRows = 1000000; + + public Builder withStripeSizeRows(int stripeSizeRows) { + // maximum stripe size cannot be smaller than 512 + if (stripeSizeRows < 512) { + throw new IllegalArgumentException("Maximum stripe size cannot be smaller than 512"); + } + this.stripeSizeRows = stripeSizeRows; + return this; + } public ORCWriterOptions build() { return new ORCWriterOptions(this); diff --git a/java/src/main/java/ai/rapids/cudf/Table.java b/java/src/main/java/ai/rapids/cudf/Table.java index 298f2cff6f3..422989143c7 100644 --- a/java/src/main/java/ai/rapids/cudf/Table.java +++ b/java/src/main/java/ai/rapids/cudf/Table.java @@ -475,6 +475,7 @@ private static native long writeORCFileBegin(String[] columnNames, int compression, int[] precisions, boolean[] isMapValues, + int stripeSizeRows, String filename) throws CudfException; /** @@ -501,6 +502,7 @@ private static native long writeORCBufferBegin(String[] columnNames, int compression, int[] precisions, boolean[] isMapValues, + int stripeSizeRows, HostBufferConsumer consumer, HostMemoryAllocator hostMemoryAllocator ) throws CudfException; @@ -1823,6 +1825,7 @@ private ORCTableWriter(ORCWriterOptions options, File outputFile) { options.getCompressionType().nativeId, options.getFlatPrecision(), options.getFlatIsMap(), + options.getStripeSizeRows(), outputFile.getAbsolutePath())); this.consumer = null; } @@ -1838,6 +1841,7 @@ private ORCTableWriter(ORCWriterOptions options, HostBufferConsumer consumer, options.getCompressionType().nativeId, options.getFlatPrecision(), options.getFlatIsMap(), + options.getStripeSizeRows(), consumer, hostMemoryAllocator)); this.consumer = consumer; } diff --git a/java/src/main/native/src/TableJni.cpp b/java/src/main/native/src/TableJni.cpp index 50c6ae842f4..e1b487b1f7c 100644 --- a/java/src/main/native/src/TableJni.cpp +++ b/java/src/main/native/src/TableJni.cpp @@ -2480,6 +2480,7 @@ Java_ai_rapids_cudf_Table_writeORCBufferBegin(JNIEnv* env, jint j_compression, jintArray j_precisions, jbooleanArray j_is_map, + jint j_stripe_size_rows, jobject consumer, jobject host_memory_allocator) { @@ -2535,6 +2536,7 @@ Java_ai_rapids_cudf_Table_writeORCBufferBegin(JNIEnv* env, .enable_statistics(ORC_STATISTICS_ROW_GROUP) .key_value_metadata(kv_metadata) .compression_statistics(stats) + .stripe_size_rows(j_stripe_size_rows) .build(); auto writer_ptr = std::make_unique(opts); cudf::jni::native_orc_writer_handle* ret = new cudf::jni::native_orc_writer_handle( @@ -2555,6 +2557,7 @@ JNIEXPORT long JNICALL Java_ai_rapids_cudf_Table_writeORCFileBegin(JNIEnv* env, jint j_compression, jintArray j_precisions, jbooleanArray j_is_map, + jint j_stripe_size_rows, jstring j_output_path) { JNI_NULL_CHECK(env, j_col_names, "null columns", 0); @@ -2606,6 +2609,7 @@ JNIEXPORT long JNICALL Java_ai_rapids_cudf_Table_writeORCFileBegin(JNIEnv* env, .enable_statistics(ORC_STATISTICS_ROW_GROUP) .key_value_metadata(kv_metadata) .compression_statistics(stats) + .stripe_size_rows(j_stripe_size_rows) .build(); auto writer_ptr = std::make_unique(opts); cudf::jni::native_orc_writer_handle* ret = From d4c62efb3d326a71c1cd2504ee46817ba2d3975f Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Mon, 24 Feb 2025 08:46:10 -0600 Subject: [PATCH 12/45] Fix `dask_cudf.to_orc` deprecation (#18038) **TLDR**: This PR adds a trivial change to avoid multiple deprecation-warning stages when `import dask_cudf.to_orc` is called. Dask cuDF uses a `_deprecated_api` utility to discourage users from importing IO functions from `dask_cudf.io`. We also encourage users to leverage `DataFrame` methods in lieu of stand-alone functions for output IO. For example, we want people to call `df.to_orc(...)` (and to avoid patterns like `from dask_cudf import to_orc`). It turns out that the calling `import dask_cudf.to_orc` currently results in **two** deprecation warnings being raised before the "real" `dask_cudf.io.orc.to_orc` implementation is reached. This is because the first `dask_cudf.to_orc` import is incorrectly redirected to `dask_cudf.io.to_orc` (rather than `dask_cudf.io.orc.to_orc`). This subtle bug doesn't seem to cause problems in CI right now, but I do see local failures in `test_orc.py::test_deprecated_api_paths` with a slightly different environment (not sure of the specific differences atm). Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Murray (https://github.com/Matt711) - Tom Augspurger (https://github.com/TomAugspurger) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/18038 --- python/dask_cudf/dask_cudf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/dask_cudf/dask_cudf/__init__.py b/python/dask_cudf/dask_cudf/__init__.py index 9afe93a6e80..0cdb4525207 100644 --- a/python/dask_cudf/dask_cudf/__init__.py +++ b/python/dask_cudf/dask_cudf/__init__.py @@ -37,7 +37,7 @@ def read_parquet(*args, **kwargs): read_text = DataFrame.read_text to_orc = _deprecated_api( "dask_cudf.to_orc", - new_api="dask_cudf.io.to_orc", + new_api="dask_cudf.io.orc.to_orc", rec="Please use DataFrame.to_orc instead.", ) From 3c698d00c9e8b8b02f2ad596bcc757430a9a6916 Mon Sep 17 00:00:00 2001 From: Michael Schellenberger Costa Date: Mon, 24 Feb 2025 19:01:16 +0100 Subject: [PATCH 13/45] Use the right version macro `CCCL_MAJOR_VERSION` (#18073) I accidentally used `CCCL_VERSION_MAJOR`, which is used in our cmake scripts Authors: - Michael Schellenberger Costa (https://github.com/miscco) Approvers: - David Wendt (https://github.com/davidwendt) - Bradley Dice (https://github.com/bdice) - Yunsong Wang (https://github.com/PointKernel) URL: https://github.com/rapidsai/cudf/pull/18073 --- cpp/src/io/fst/dispatch_dfa.cuh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/io/fst/dispatch_dfa.cuh b/cpp/src/io/fst/dispatch_dfa.cuh index d8be747d93d..e8709b0d7bb 100644 --- a/cpp/src/io/fst/dispatch_dfa.cuh +++ b/cpp/src/io/fst/dispatch_dfa.cuh @@ -391,7 +391,7 @@ struct DispatchFSM : DeviceFSMPolicy { // Alias the temporary allocations from the single storage blob (or compute the necessary size // of the blob) // TODO (@miscco): remove this once rapids moves to CCCL 2.8 -#if CCCL_VERSION_MAJOR >= 3 +#if CCCL_MAJOR_VERSION >= 3 error = cub::detail::AliasTemporaries( #else // ^^^ CCCL 3.x ^^^ / vvv CCCL 2.x vvv error = cub::AliasTemporaries( From 2b6dcb0faa28a51989e32da6dd78378778b72198 Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Mon, 24 Feb 2025 12:05:26 -0600 Subject: [PATCH 14/45] Enable pytest-xdist runs for py-polars tests (#18016) This PR reduces CI time by parallelizing pytests execution for `py-polars` tests. This PR: ``` ========== 14748 passed, 28 skipped, 140 xfailed in 112.37s (0:01:52) ========== ``` On `branch-25.04`: ``` == 14748 passed, 28 skipped, 58 deselected, 140 xfailed in 529.08s (0:08:49) === ``` Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Gil Forsyth (https://github.com/gforsyth) URL: https://github.com/rapidsai/cudf/pull/18016 --- ci/run_cudf_polars_polars_tests.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/run_cudf_polars_polars_tests.sh b/ci/run_cudf_polars_polars_tests.sh index dfabe6093a9..757f4eb94c4 100755 --- a/ci/run_cudf_polars_polars_tests.sh +++ b/ci/run_cudf_polars_polars_tests.sh @@ -48,7 +48,9 @@ python -m pytest \ --cache-clear \ -m "" \ -p cudf_polars.testing.plugin \ - -v \ + -n 8 \ + --dist=worksteal \ + -vv \ --tb=native \ $DESELECTED_TESTS_STR \ "$@" \ From bcff1f7c55eb7077fa42e8e1ef231d0542fadd46 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:44:56 -0500 Subject: [PATCH 15/45] Add a slice expression to polars IR (#18050) Closes https://github.com/rapidsai/cudf/issues/18051 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/18050 --- .../cudf_polars/containers/dataframe.py | 4 +- python/cudf_polars/cudf_polars/dsl/expr.py | 4 +- .../cudf_polars/dsl/expressions/slicing.py | 51 +++++++++++++++++++ .../cudf_polars/cudf_polars/dsl/translate.py | 14 +++++ .../tests/expressions/test_slice.py | 24 +++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 python/cudf_polars/cudf_polars/dsl/expressions/slicing.py create mode 100644 python/cudf_polars/tests/expressions/test_slice.py diff --git a/python/cudf_polars/cudf_polars/containers/dataframe.py b/python/cudf_polars/cudf_polars/containers/dataframe.py index a605b476197..a2b496b8cfe 100644 --- a/python/cudf_polars/cudf_polars/containers/dataframe.py +++ b/python/cudf_polars/cudf_polars/containers/dataframe.py @@ -295,7 +295,7 @@ def filter(self, mask: Column) -> Self: table = plc.stream_compaction.apply_boolean_mask(self.table, mask.obj) return type(self).from_table(table, self.column_names).sorted_like(self) - def slice(self, zlice: tuple[int, int] | None) -> Self: + def slice(self, zlice: tuple[int, int | None] | None) -> Self: """ Slice a dataframe. @@ -312,6 +312,8 @@ def slice(self, zlice: tuple[int, int] | None) -> Self: if zlice is None: return self start, length = zlice + if length is None: + length = self.num_rows if start < 0: start += self.num_rows # Polars implementation wraps negative start by num_rows, then diff --git a/python/cudf_polars/cudf_polars/dsl/expr.py b/python/cudf_polars/cudf_polars/dsl/expr.py index 98d49e36fb1..3ba54543a3e 100644 --- a/python/cudf_polars/cudf_polars/dsl/expr.py +++ b/python/cudf_polars/cudf_polars/dsl/expr.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. # SPDX-License-Identifier: Apache-2.0 # TODO: remove need for this # ruff: noqa: D101 @@ -30,6 +30,7 @@ from cudf_polars.dsl.expressions.literal import Literal, LiteralColumn from cudf_polars.dsl.expressions.rolling import GroupedRollingWindow, RollingWindow from cudf_polars.dsl.expressions.selection import Filter, Gather +from cudf_polars.dsl.expressions.slicing import Slice from cudf_polars.dsl.expressions.sorting import Sort, SortBy from cudf_polars.dsl.expressions.string import StringFunction from cudf_polars.dsl.expressions.ternary import Ternary @@ -53,6 +54,7 @@ "LiteralColumn", "NamedExpr", "RollingWindow", + "Slice", "Sort", "SortBy", "StringFunction", diff --git a/python/cudf_polars/cudf_polars/dsl/expressions/slicing.py b/python/cudf_polars/cudf_polars/dsl/expressions/slicing.py new file mode 100644 index 00000000000..2d3640cce86 --- /dev/null +++ b/python/cudf_polars/cudf_polars/dsl/expressions/slicing.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +# TODO: remove need for this +# ruff: noqa: D101 +"""Slicing DSL nodes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from cudf_polars.dsl.expressions.base import ( + ExecutionContext, + Expr, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + + import pylibcudf as plc + + from cudf_polars.containers import Column, DataFrame + + +__all__ = ["Slice"] + + +class Slice(Expr): + __slots__ = ("length", "offset") + _non_child = ("dtype", "offset", "length") + + def __init__( + self, + dtype: plc.DataType, + offset: int, + length: int, + column: Expr, + ) -> None: + self.dtype = dtype + self.offset = offset + self.length = length + self.children = (column,) + + def do_evaluate( + self, + df: DataFrame, + *, + context: ExecutionContext = ExecutionContext.FRAME, + mapping: Mapping[Expr, Column] | None = None, + ) -> Column: + """Evaluate this expression given a dataframe for context.""" + return df.slice((self.offset, self.length)).columns[0] diff --git a/python/cudf_polars/cudf_polars/dsl/translate.py b/python/cudf_polars/cudf_polars/dsl/translate.py index 22f97f2bf52..369328d3a8c 100644 --- a/python/cudf_polars/cudf_polars/dsl/translate.py +++ b/python/cudf_polars/cudf_polars/dsl/translate.py @@ -690,6 +690,20 @@ def _(node: pl_expr.SortBy, translator: Translator, dtype: plc.DataType) -> expr ) +@_translate_expr.register +def _(node: pl_expr.Slice, translator: Translator, dtype: plc.DataType) -> expr.Expr: + offset = translator.translate_expr(n=node.offset) + length = translator.translate_expr(n=node.length) + assert isinstance(offset, expr.Literal) + assert isinstance(length, expr.Literal) + return expr.Slice( + dtype, + offset.value.as_py(), + length.value.as_py(), + translator.translate_expr(n=node.input), + ) + + @_translate_expr.register def _(node: pl_expr.Gather, translator: Translator, dtype: plc.DataType) -> expr.Expr: return expr.Gather( diff --git a/python/cudf_polars/tests/expressions/test_slice.py b/python/cudf_polars/tests/expressions/test_slice.py new file mode 100644 index 00000000000..9873be2455f --- /dev/null +++ b/python/cudf_polars/tests/expressions/test_slice.py @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + +import pytest + +import polars as pl + +from cudf_polars.testing.asserts import assert_gpu_result_equal + + +@pytest.mark.parametrize( + "zlice", + [ + (1,), + (1, 3), + (-1,), + ], +) +def test_slice(zlice): + df = pl.LazyFrame({"a": [0, 1, 2, 3], "b": [1, 2, 3, 4]}) + q = df.select(pl.col("a").slice(*zlice)) + + assert_gpu_result_equal(q) From 6590cc200652ef813dfdc8025e674a3570af6ae1 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:09:58 -0800 Subject: [PATCH 16/45] Support melt(ignore_index=False) (#18080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Additionally, refactors `melt` to avoid Series/DataFrame constructors by operating on columns and passing the result to `_from_data` ```python In [1]: import numpy as np, cudf In [2]: df = cudf.DataFrame(np.ones((1000, 1000))) In [3]: %timeit df.melt(id_vars=range(50, 300)) # this PR 1.35 s ± 12.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) In [3]: %timeit df.melt(id_vars=range(50, 300)) # branch-25.04 24.8 s ± 47 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) ``` Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/18080 --- python/cudf/cudf/core/reshape.py | 54 +++++++++++++++----------- python/cudf/cudf/tests/test_reshape.py | 25 +++++++++--- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index 21f8dc9bb8a..c5d2fd349e9 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -14,11 +14,18 @@ from cudf.api.extensions import no_default from cudf.api.types import is_scalar from cudf.core._compat import PANDAS_LT_300 -from cudf.core.column import ColumnBase, as_column, column_empty +from cudf.core.column import ( + ColumnBase, + as_column, + column_empty, + concat_columns, +) from cudf.core.column_accessor import ColumnAccessor from cudf.utils.dtypes import SIZE_TYPE_DTYPE, min_unsigned_type if TYPE_CHECKING: + from collections.abc import Hashable + from cudf._typing import DtypeObj _AXIS_MAP = {0: 0, 1: 1, "index": 0, "columns": 1} @@ -534,14 +541,14 @@ def concat( def melt( - frame, + frame: cudf.DataFrame, id_vars=None, value_vars=None, var_name=None, - value_name="value", + value_name: Hashable = "value", col_level=None, ignore_index: bool = True, -): +) -> cudf.DataFrame: """Unpivots a DataFrame from wide format to long format, optionally leaving identifier variables set. @@ -605,14 +612,12 @@ def melt( """ if col_level is not None: raise NotImplementedError("col_level != None is not supported yet.") - if ignore_index is not True: - raise NotImplementedError("ignore_index is currently not supported.") # Arg cleaning # id_vars if id_vars is not None: - if cudf.api.types.is_scalar(id_vars): + if is_scalar(id_vars): id_vars = [id_vars] id_vars = list(id_vars) missing = set(id_vars) - set(frame._column_names) @@ -626,7 +631,7 @@ def melt( # value_vars if value_vars is not None: - if cudf.api.types.is_scalar(value_vars): + if is_scalar(value_vars): value_vars = [value_vars] value_vars = list(value_vars) missing = set(value_vars) - set(frame._column_names) @@ -643,7 +648,7 @@ def melt( # Error for unimplemented support for datatype if any( isinstance(frame[col].dtype, cudf.CategoricalDtype) - for col in id_vars + value_vars + for col in itertools.chain(id_vars, value_vars) ): raise NotImplementedError( "Categorical columns are not yet supported for function" @@ -668,15 +673,14 @@ def melt( N = len(frame) K = len(value_vars) - def _tile(A, reps): - series_list = [A] * reps + def _tile(base_col: ColumnBase, reps: int) -> ColumnBase: if reps > 0: - return cudf.Series._concat(objs=series_list, index=False) + return concat_columns([base_col] * reps) else: - return cudf.Series([], dtype=A.dtype) + return column_empty(0, dtype=base_col.dtype) # Step 1: tile id_vars - mdata = {col: _tile(frame[col], K) for col in id_vars} + mdata = {col: _tile(frame[col]._column, K) for col in id_vars} # Step 2: add variable nval = len(value_vars) @@ -687,23 +691,27 @@ def _tile(A, reps): if not value_vars: # TODO: Use frame._data.label_dtype when it's more consistently set - var_data = cudf.Series( - value_vars, dtype=frame._data.to_pandas_index.dtype + var_data = column_empty( + 0, dtype=cudf.dtype(frame._data.to_pandas_index.dtype) ) else: - var_data = ( - cudf.Series(value_vars) - .take(np.repeat(np.arange(nval, dtype=dtype), N)) - .reset_index(drop=True) + var_data = as_column(value_vars).take( + as_column(np.repeat(np.arange(nval, dtype=dtype), N)), + check_bounds=False, ) mdata[var_name] = var_data # Step 3: add values - mdata[value_name] = cudf.Series._concat( - objs=[frame[val] for val in value_vars], index=False + mdata[value_name] = concat_columns( + [frame[val]._column for val in value_vars] ) - return cudf.DataFrame(mdata) + result = cudf.DataFrame._from_data(mdata) + if not ignore_index: + taker = np.tile(np.arange(len(frame)), frame.shape[1] - len(id_vars)) + result.index = frame.index.take(taker) + + return result def get_dummies( diff --git a/python/cudf/cudf/tests/test_reshape.py b/python/cudf/cudf/tests/test_reshape.py index 5cebdf37c9f..7fbe072dde7 100644 --- a/python/cudf/cudf/tests/test_reshape.py +++ b/python/cudf/cudf/tests/test_reshape.py @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2024, NVIDIA CORPORATION. +# Copyright (c) 2021-2025, NVIDIA CORPORATION. import re from itertools import chain @@ -40,7 +40,10 @@ @pytest.mark.parametrize("num_rows", [1, 2, 100]) @pytest.mark.parametrize("dtype", NUMERIC_TYPES + DATETIME_TYPES) @pytest.mark.parametrize("nulls", ["none", "some", "all"]) -def test_melt(nulls, num_id_vars, num_value_vars, num_rows, dtype): +@pytest.mark.parametrize("ignore_index", [True, False]) +def test_melt( + nulls, num_id_vars, num_value_vars, num_rows, dtype, ignore_index +): if dtype not in ["float32", "float64"] and nulls in ["some", "all"]: pytest.skip(reason="nulls not supported in dtype: " + dtype) @@ -72,10 +75,22 @@ def test_melt(nulls, num_id_vars, num_value_vars, num_rows, dtype): gdf = cudf.from_pandas(pdf) - got = cudf.melt(frame=gdf, id_vars=id_vars, value_vars=value_vars) - got_from_melt_method = gdf.melt(id_vars=id_vars, value_vars=value_vars) + got = cudf.melt( + frame=gdf, + id_vars=id_vars, + value_vars=value_vars, + ignore_index=ignore_index, + ) + got_from_melt_method = gdf.melt( + id_vars=id_vars, value_vars=value_vars, ignore_index=ignore_index + ) - expect = pd.melt(frame=pdf, id_vars=id_vars, value_vars=value_vars) + expect = pd.melt( + frame=pdf, + id_vars=id_vars, + value_vars=value_vars, + ignore_index=ignore_index, + ) assert_eq(expect, got) From 5c82909aa4f4f5030627b62e0d0587b0df7c1e44 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Mon, 24 Feb 2025 17:04:44 -0500 Subject: [PATCH 17/45] Remove `FindCUDAToolkit` backport (#18081) In 81cd4a00, `FindCUDAToolkit.cmake` was backported from CMake 3.31 to get https://gitlab.kitware.com/cmake/cmake/-/commit/b38a8e77cb3c8401b3022a68f07a4fd77b290524, which was introduced in CMake 3.28. Since we now require CMake 3.30, we no longer need the backport. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/18081 --- cpp/cmake/Modules/FindCUDAToolkit.cmake | 1437 ----------------------- 1 file changed, 1437 deletions(-) delete mode 100644 cpp/cmake/Modules/FindCUDAToolkit.cmake diff --git a/cpp/cmake/Modules/FindCUDAToolkit.cmake b/cpp/cmake/Modules/FindCUDAToolkit.cmake deleted file mode 100644 index 6f0272aa2d7..00000000000 --- a/cpp/cmake/Modules/FindCUDAToolkit.cmake +++ /dev/null @@ -1,1437 +0,0 @@ -# CMake - Cross Platform Makefile Generator -# Copyright 2000-2024 Kitware, Inc. and Contributors -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# * Neither the name of Kitware, Inc. nor the names of Contributors -# may be used to endorse or promote products derived from this -# software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#[=======================================================================[.rst: -FindCUDAToolkit ---------------- - -.. versionadded:: 3.17 - -This script locates the NVIDIA CUDA toolkit and the associated libraries, but -does not require the ``CUDA`` language be enabled for a given project. This -module does not search for the NVIDIA CUDA Samples. - -.. versionadded:: 3.19 - QNX support. - -Search Behavior -^^^^^^^^^^^^^^^ - -The CUDA Toolkit search behavior uses the following order: - -1. If the ``CUDA`` language has been enabled we will use the directory - containing the compiler as the first search location for ``nvcc``. - -2. If the variable :variable:`CMAKE_CUDA_COMPILER _COMPILER>` or - the environment variable :envvar:`CUDACXX` is defined, it will be used - as the path to the ``nvcc`` executable. - -3. If the ``CUDAToolkit_ROOT`` cmake configuration variable (e.g., - ``-DCUDAToolkit_ROOT=/some/path``) *or* environment variable is defined, it - will be searched. If both an environment variable **and** a - configuration variable are specified, the *configuration* variable takes - precedence. - - The directory specified here must be such that the executable ``nvcc`` or - the appropriate ``version.txt`` or ``version.json`` file can be found - underneath the specified directory. - -4. If the CUDA_PATH environment variable is defined, it will be searched - for ``nvcc``. - -5. The user's path is searched for ``nvcc`` using :command:`find_program`. If - this is found, no subsequent search attempts are performed. Users are - responsible for ensuring that the first ``nvcc`` to show up in the path is - the desired path in the event that multiple CUDA Toolkits are installed. - -6. On Unix systems, if the symbolic link ``/usr/local/cuda`` exists, this is - used. No subsequent search attempts are performed. No default symbolic link - location exists for the Windows platform. - -7. The platform specific default install locations are searched. If exactly one - candidate is found, this is used. The default CUDA Toolkit install locations - searched are: - - +-------------+-------------------------------------------------------------+ - | Platform | Search Pattern | - +=============+=============================================================+ - | macOS | ``/Developer/NVIDIA/CUDA-X.Y`` | - +-------------+-------------------------------------------------------------+ - | Other Unix | ``/usr/local/cuda-X.Y`` | - +-------------+-------------------------------------------------------------+ - | Windows | ``C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y`` | - +-------------+-------------------------------------------------------------+ - - Where ``X.Y`` would be a specific version of the CUDA Toolkit, such as - ``/usr/local/cuda-9.0`` or - ``C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0`` - - .. note:: - - When multiple CUDA Toolkits are installed in the default location of a - system (e.g., both ``/usr/local/cuda-9.0`` and ``/usr/local/cuda-10.0`` - exist but the ``/usr/local/cuda`` symbolic link does **not** exist), this - package is marked as **not** found. - - There are too many factors involved in making an automatic decision in - the presence of multiple CUDA Toolkits being installed. In this - situation, users are encouraged to either (1) set ``CUDAToolkit_ROOT`` or - (2) ensure that the correct ``nvcc`` executable shows up in ``$PATH`` for - :command:`find_program` to find. - -Arguments -^^^^^^^^^ - -``[]`` - The ``[]`` argument requests a version with which the package found - should be compatible. See :ref:`find_package version format ` - for more details. - -Options -^^^^^^^ - -``REQUIRED`` - If specified, configuration will error if a suitable CUDA Toolkit is not - found. - -``QUIET`` - If specified, the search for a suitable CUDA Toolkit will not produce any - messages. - -``EXACT`` - If specified, the CUDA Toolkit is considered found only if the exact - ``VERSION`` specified is recovered. - -Imported targets -^^^^^^^^^^^^^^^^ - -An :ref:`imported target ` named ``CUDA::toolkit`` is provided. - -This module defines :prop_tgt:`IMPORTED` targets for each -of the following libraries that are part of the CUDAToolkit: - -- :ref:`CUDA Runtime Library` -- :ref:`CUDA Driver Library` -- :ref:`cuBLAS` -- :ref:`cuDLA` -- :ref:`cuFile` -- :ref:`cuFFT` -- :ref:`cuRAND` -- :ref:`cuSOLVER` -- :ref:`cuSPARSE` -- :ref:`cuPTI` -- :ref:`NPP` -- :ref:`nvBLAS` -- :ref:`nvGRAPH` -- :ref:`nvJPEG` -- :ref:`nvidia-ML` -- :ref:`nvPTX Compiler` -- :ref:`nvRTC` -- :ref:`nvJitLink` -- :ref:`nvFatBin` -- :ref:`nvToolsExt` -- :ref:`nvtx3` -- :ref:`OpenCL` -- :ref:`cuLIBOS` - -.. _`cuda_toolkit_rt_lib`: - -CUDA Runtime Library -"""""""""""""""""""" - -The CUDA Runtime library (cudart) are what most applications will typically -need to link against to make any calls such as `cudaMalloc`, and `cudaFree`. - -Targets Created: - -- ``CUDA::cudart`` -- ``CUDA::cudart_static`` - -.. _`cuda_toolkit_driver_lib`: - -CUDA Driver Library -"""""""""""""""""""" - -The CUDA Driver library (cuda) are used by applications that use calls -such as `cuMemAlloc`, and `cuMemFree`. - -Targets Created: - -- ``CUDA::cuda_driver`` - -.. _`cuda_toolkit_cuBLAS`: - -cuBLAS -"""""" - -The `cuBLAS `_ library. - -Targets Created: - -- ``CUDA::cublas`` -- ``CUDA::cublas_static`` -- ``CUDA::cublasLt`` starting in CUDA 10.1 -- ``CUDA::cublasLt_static`` starting in CUDA 10.1 - -.. _`cuda_toolkit_cuDLA`: - -cuDLA -"""""" - -.. versionadded:: 3.27 - -The NVIDIA Tegra Deep Learning Accelerator `cuDLA `_ library. - -Targets Created: - -- ``CUDA::cudla`` starting in CUDA 11.6 - -.. _`cuda_toolkit_cuFile`: - -cuFile -"""""" - -.. versionadded:: 3.25 - -The NVIDIA GPUDirect Storage `cuFile `_ library. - -Targets Created: - -- ``CUDA::cuFile`` starting in CUDA 11.4 -- ``CUDA::cuFile_static`` starting in CUDA 11.4 -- ``CUDA::cuFile_rdma`` starting in CUDA 11.4 -- ``CUDA::cuFile_rdma_static`` starting in CUDA 11.4 - -.. _`cuda_toolkit_cuFFT`: - -cuFFT -""""" - -The `cuFFT `_ library. - -Targets Created: - -- ``CUDA::cufft`` -- ``CUDA::cufftw`` -- ``CUDA::cufft_static`` -- ``CUDA::cufft_static_nocallback`` starting in CUDA 9.2, requires CMake 3.23+ -- ``CUDA::cufftw_static`` - -cuRAND -"""""" - -The `cuRAND `_ library. - -Targets Created: - -- ``CUDA::curand`` -- ``CUDA::curand_static`` - -.. _`cuda_toolkit_cuSOLVER`: - -cuSOLVER -"""""""" - -The `cuSOLVER `_ library. - -Targets Created: - -- ``CUDA::cusolver`` -- ``CUDA::cusolver_static`` - -.. _`cuda_toolkit_cuSPARSE`: - -cuSPARSE -"""""""" - -The `cuSPARSE `_ library. - -Targets Created: - -- ``CUDA::cusparse`` -- ``CUDA::cusparse_static`` - -.. _`cuda_toolkit_cupti`: - -cupti -""""" - -The `NVIDIA CUDA Profiling Tools Interface `_. - -Targets Created: - -- ``CUDA::cupti`` -- ``CUDA::cupti_static`` - -.. versionadded:: 3.27 - - - ``CUDA::nvperf_host`` starting in CUDA 10.2 - - ``CUDA::nvperf_host_static`` starting in CUDA 10.2 - - ``CUDA::nvperf_target`` starting in CUDA 10.2 - - ``CUDA::pcsamplingutil`` starting in CUDA 11.3 - -.. _`cuda_toolkit_NPP`: - -NPP -""" - -The `NPP `_ libraries. - -Targets Created: - -- `nppc`: - - - ``CUDA::nppc`` - - ``CUDA::nppc_static`` - -- `nppial`: Arithmetic and logical operation functions in `nppi_arithmetic_and_logical_operations.h` - - - ``CUDA::nppial`` - - ``CUDA::nppial_static`` - -- `nppicc`: Color conversion and sampling functions in `nppi_color_conversion.h` - - - ``CUDA::nppicc`` - - ``CUDA::nppicc_static`` - -- `nppicom`: JPEG compression and decompression functions in `nppi_compression_functions.h` - Removed starting in CUDA 11.0, use :ref:`nvJPEG` instead. - - - ``CUDA::nppicom`` - - ``CUDA::nppicom_static`` - -- `nppidei`: Data exchange and initialization functions in `nppi_data_exchange_and_initialization.h` - - - ``CUDA::nppidei`` - - ``CUDA::nppidei_static`` - -- `nppif`: Filtering and computer vision functions in `nppi_filter_functions.h` - - - ``CUDA::nppif`` - - ``CUDA::nppif_static`` - -- `nppig`: Geometry transformation functions found in `nppi_geometry_transforms.h` - - - ``CUDA::nppig`` - - ``CUDA::nppig_static`` - -- `nppim`: Morphological operation functions found in `nppi_morphological_operations.h` - - - ``CUDA::nppim`` - - ``CUDA::nppim_static`` - -- `nppist`: Statistics and linear transform in `nppi_statistics_functions.h` and `nppi_linear_transforms.h` - - - ``CUDA::nppist`` - - ``CUDA::nppist_static`` - -- `nppisu`: Memory support functions in `nppi_support_functions.h` - - - ``CUDA::nppisu`` - - ``CUDA::nppisu_static`` - -- `nppitc`: Threshold and compare operation functions in `nppi_threshold_and_compare_operations.h` - - - ``CUDA::nppitc`` - - ``CUDA::nppitc_static`` - -- `npps`: - - - ``CUDA::npps`` - - ``CUDA::npps_static`` - -.. _`cuda_toolkit_nvBLAS`: - -nvBLAS -"""""" - -The `nvBLAS `_ libraries. -This is a shared library only. - -Targets Created: - -- ``CUDA::nvblas`` - -.. _`cuda_toolkit_nvGRAPH`: - -nvGRAPH -""""""" - -The `nvGRAPH `_ library. -Removed starting in CUDA 11.0 - -Targets Created: - -- ``CUDA::nvgraph`` -- ``CUDA::nvgraph_static`` - - -.. _`cuda_toolkit_nvJPEG`: - -nvJPEG -"""""" - -The `nvJPEG `_ library. -Introduced in CUDA 10. - -Targets Created: - -- ``CUDA::nvjpeg`` -- ``CUDA::nvjpeg_static`` - -.. _`cuda_toolkit_nvPTX`: - -nvPTX Compiler -"""""""""""""" - -.. versionadded:: 3.25 - -The `nvPTX `_ (PTX Compilation) library. -The PTX Compiler APIs are a set of APIs which can be used to compile a PTX program into GPU assembly code. -Introduced in CUDA 11.1 -This is a static library only. - -Targets Created: - -- ``CUDA::nvptxcompiler_static`` starting in CUDA 11.1 - -.. _`cuda_toolkit_nvRTC`: - -nvRTC -""""" - -The `nvRTC `_ (Runtime Compilation) library. - -Targets Created: - -- ``CUDA::nvrtc`` - -.. versionadded:: 3.26 - - - ``CUDA::nvrtc_builtins`` - - ``CUDA::nvrtc_static`` starting in CUDA 11.5 - - ``CUDA::nvrtc_builtins_static`` starting in CUDA 11.5 - -.. _`cuda_toolkit_nvjitlink`: - -nvJitLink -""""""""" - -The `nvJItLink `_ (Runtime LTO Linking) library. - -Targets Created: - -- ``CUDA::nvJitLink`` starting in CUDA 12.0 -- ``CUDA::nvJitLink_static`` starting in CUDA 12.0 - -.. _`cuda_toolkit_nvfatbin`: - -nvFatBin -""""""""" - -.. versionadded:: 3.30 - -The `nvFatBin `_ (Runtime fatbin creation) library. - -Targets Created: - -- ``CUDA::nvfatbin`` starting in CUDA 12.4 -- ``CUDA::nvfatbin_static`` starting in CUDA 12.4 - -.. _`cuda_toolkit_nvml`: - -nvidia-ML -""""""""" - -The `NVIDIA Management Library `_. - -Targets Created: - -- ``CUDA::nvml`` -- ``CUDA::nvml_static`` starting in CUDA 12.4 - -.. versionadded:: 3.31 - Added ``CUDA::nvml_static``. - -.. _`cuda_toolkit_nvToolsExt`: - -nvToolsExt -"""""""""" - -.. deprecated:: 3.25 With CUDA 10.0+, use :ref:`nvtx3 `. - -The `NVIDIA Tools Extension `_. -This is a shared library only. - -Targets Created: - -- ``CUDA::nvToolsExt`` - -.. _`cuda_toolkit_nvtx3`: - -nvtx3 -""""" - -.. versionadded:: 3.25 - -The header-only `NVIDIA Tools Extension Library `_. -Introduced in CUDA 10.0. - -Targets created: - -- ``CUDA::nvtx3`` - -.. _`cuda_toolkit_opencl`: - -OpenCL -"""""" - -The `NVIDIA OpenCL Library `_. -This is a shared library only. - -Targets Created: - -- ``CUDA::OpenCL`` - -.. _`cuda_toolkit_cuLIBOS`: - -cuLIBOS -""""""" - -The cuLIBOS library is a backend thread abstraction layer library which is -static only. The ``CUDA::cublas_static``, ``CUDA::cusparse_static``, -``CUDA::cufft_static``, ``CUDA::curand_static``, and (when implemented) NPP -libraries all automatically have this dependency linked. - -Target Created: - -- ``CUDA::culibos`` - -**Note**: direct usage of this target by consumers should not be necessary. - -.. _`cuda_toolkit_cuRAND`: - - - -Result variables -^^^^^^^^^^^^^^^^ - -``CUDAToolkit_FOUND`` - A boolean specifying whether or not the CUDA Toolkit was found. - -``CUDAToolkit_VERSION`` - The exact version of the CUDA Toolkit found (as reported by - ``nvcc --version``, ``version.txt``, or ``version.json``). - -``CUDAToolkit_VERSION_MAJOR`` - The major version of the CUDA Toolkit. - -``CUDAToolkit_VERSION_MINOR`` - The minor version of the CUDA Toolkit. - -``CUDAToolkit_VERSION_PATCH`` - The patch version of the CUDA Toolkit. - -``CUDAToolkit_BIN_DIR`` - The path to the CUDA Toolkit library directory that contains the CUDA - executable ``nvcc``. - -``CUDAToolkit_INCLUDE_DIRS`` - List of paths to all the CUDA Toolkit folders containing header files - required to compile a project linking against CUDA. - -``CUDAToolkit_LIBRARY_DIR`` - The path to the CUDA Toolkit library directory that contains the CUDA - Runtime library ``cudart``. - -``CUDAToolkit_LIBRARY_ROOT`` - .. versionadded:: 3.18 - - The path to the CUDA Toolkit directory containing the nvvm directory and - either version.txt or version.json. - -``CUDAToolkit_TARGET_DIR`` - The path to the CUDA Toolkit directory including the target architecture - when cross-compiling. When not cross-compiling this will be equivalent to - the parent directory of ``CUDAToolkit_BIN_DIR``. - -``CUDAToolkit_NVCC_EXECUTABLE`` - The path to the NVIDIA CUDA compiler ``nvcc``. Note that this path may - **not** be the same as - :variable:`CMAKE_CUDA_COMPILER _COMPILER>`. ``nvcc`` must be - found to determine the CUDA Toolkit version as well as determining other - features of the Toolkit. This variable is set for the convenience of - modules that depend on this one. - - -#]=======================================================================] - -# NOTE: much of this was simply extracted from FindCUDA.cmake. - -# James Bigler, NVIDIA Corp (nvidia.com - jbigler) -# Abe Stephens, SCI Institute -- http://www.sci.utah.edu/~abe/FindCuda.html -# -# Copyright (c) 2008 - 2009 NVIDIA Corporation. All rights reserved. -# -# Copyright (c) 2007-2009 -# Scientific Computing and Imaging Institute, University of Utah -# -# This code is licensed under the MIT License. See the FindCUDA.cmake script -# for the text of the license. - -# The MIT License -# -# License for the specific language governing rights and limitations under -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files (the "Software"), -# to deal in the Software without restriction, including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -# DEALINGS IN THE SOFTWARE. -# -############################################################################### - -function(_CUDAToolkit_build_include_dirs result_variable default_paths_variable) - set(content "${${default_paths_variable}}") - set(${result_variable} "${content}" PARENT_SCOPE) -endfunction() - -function(_CUDAToolkit_build_library_dirs result_variable default_paths_variable) - set(content "${${default_paths_variable}}") - set(${result_variable} "${content}" PARENT_SCOPE) -endfunction() - -# The toolkit is located during compiler detection for CUDA and stored in CMakeCUDACompiler.cmake as -# - CMAKE_CUDA_COMPILER_TOOLKIT_ROOT -# - CMAKE_CUDA_COMPILER_LIBRARY_ROOT -# - CMAKE_CUDA_COMPILER_LIBRARY_DIRECTORIES_FROM_IMPLICIT_LIBRARIES -# - CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES -# We compute the rest based on those here to avoid re-searching and to avoid finding a possibly -# different installation. -if(CMAKE_CUDA_COMPILER_TOOLKIT_ROOT) - set(CUDAToolkit_ROOT_DIR "${CMAKE_CUDA_COMPILER_TOOLKIT_ROOT}") - set(CUDAToolkit_LIBRARY_ROOT "${CMAKE_CUDA_COMPILER_LIBRARY_ROOT}") - _CUDAToolkit_build_library_dirs(CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES CMAKE_CUDA_HOST_IMPLICIT_LINK_DIRECTORIES) - _CUDAToolkit_build_include_dirs(CUDAToolkit_INCLUDE_DIRECTORIES CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES) - set(CUDAToolkit_BIN_DIR "${CUDAToolkit_ROOT_DIR}/bin") - set(CUDAToolkit_NVCC_EXECUTABLE "${CUDAToolkit_BIN_DIR}/nvcc${CMAKE_EXECUTABLE_SUFFIX}") - set(CUDAToolkit_VERSION "${CMAKE_CUDA_COMPILER_TOOLKIT_VERSION}") - - if(CUDAToolkit_VERSION MATCHES [=[([0-9]+)\.([0-9]+)\.([0-9]+)]=]) - set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}") - set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}") - set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}") - endif() -else() - function(_CUDAToolkit_find_root_dir ) - cmake_parse_arguments(arg "COMPILER_PATHS" "" "SEARCH_PATHS;FIND_FLAGS" ${ARGN}) - - if(NOT CUDAToolkit_BIN_DIR) - if(arg_COMPILER_PATHS) - # need to find parent dir, since this could clang and not nvcc - if(EXISTS "${CMAKE_CUDA_COMPILER}") - get_filename_component(possible_nvcc_path "${CMAKE_CUDA_COMPILER}" PROGRAM PROGRAM_ARGS CUDAToolkit_compiler_args) - get_filename_component(possible_nvcc_path "${possible_nvcc_path}" DIRECTORY) - elseif(EXISTS "$ENV{CUDACXX}") - get_filename_component(possible_nvcc_path "$ENV{CUDACXX}" PROGRAM PROGRAM_ARGS CUDAToolkit_compiler_args) - get_filename_component(possible_nvcc_path "${possible_nvcc_path}" DIRECTORY) - endif() - if(possible_nvcc_path) - find_program(CUDAToolkit_NVCC_EXECUTABLE - NAMES nvcc nvcc.exe - NO_DEFAULT_PATH - PATHS ${possible_nvcc_path} - ) - endif() - endif() - - if(NOT CUDAToolkit_SENTINEL_FILE) - find_program(CUDAToolkit_NVCC_EXECUTABLE - NAMES nvcc nvcc.exe - PATHS ${arg_SEARCH_PATHS} - ${arg_FIND_FLAGS} - ) - endif() - - if(NOT CUDAToolkit_NVCC_EXECUTABLE) - find_file(CUDAToolkit_SENTINEL_FILE - NAMES version.txt version.json - PATHS ${arg_SEARCH_PATHS} - NO_DEFAULT_PATH - ) - endif() - - if(EXISTS "${CUDAToolkit_NVCC_EXECUTABLE}") - # If NVCC exists then invoke it to find the toolkit location. - # This allows us to support wrapper scripts (e.g. ccache or colornvcc), CUDA Toolkit, - # NVIDIA HPC SDK, and distro's splayed layouts - execute_process(COMMAND ${CUDAToolkit_NVCC_EXECUTABLE} "-v" "__cmake_determine_cuda" - OUTPUT_VARIABLE _CUDA_NVCC_OUT ERROR_VARIABLE _CUDA_NVCC_OUT) - message(CONFIGURE_LOG - "Executed nvcc to extract CUDAToolkit information:\n${_CUDA_NVCC_OUT}\n\n") - if(_CUDA_NVCC_OUT MATCHES "\\#\\$ TOP=([^\r\n]*)") - get_filename_component(CUDAToolkit_BIN_DIR "${CMAKE_MATCH_1}/bin" ABSOLUTE) - message(CONFIGURE_LOG - "Parsed CUDAToolkit nvcc location:\n${CUDAToolkit_BIN_DIR}\n\n") - else() - get_filename_component(CUDAToolkit_BIN_DIR "${CUDAToolkit_NVCC_EXECUTABLE}" DIRECTORY) - endif() - if(_CUDA_NVCC_OUT MATCHES "\\#\\$ INCLUDES=([^\r\n]*)") - separate_arguments(_nvcc_output NATIVE_COMMAND "${CMAKE_MATCH_1}") - foreach(line IN LISTS _nvcc_output) - string(REGEX REPLACE "^-I" "" line "${line}") - get_filename_component(line "${line}" ABSOLUTE) - list(APPEND _cmake_CUDAToolkit_include_directories "${line}") - endforeach() - message(CONFIGURE_LOG - "Parsed CUDAToolkit nvcc implicit include information:\n${_cmake_CUDAToolkit_include_directories}\n\n") - - set(_cmake_CUDAToolkit_include_directories "${_cmake_CUDAToolkit_include_directories}" CACHE INTERNAL "CUDAToolkit internal list of include directories") - endif() - if(_CUDA_NVCC_OUT MATCHES "\\#\\$ LIBRARIES=([^\r\n]*)") - include(${CMAKE_ROOT}/Modules/CMakeParseImplicitLinkInfo.cmake) - set(_nvcc_link_line "cuda-fake-ld ${CMAKE_MATCH_1}") - CMAKE_PARSE_IMPLICIT_LINK_INFO("${_nvcc_link_line}" - _cmake_CUDAToolkit_implicit_link_libs - _cmake_CUDAToolkit_implicit_link_directories - _cmake_CUDAToolkit_implicit_frameworks - _nvcc_log - "${CMAKE_CUDA_IMPLICIT_OBJECT_REGEX}" - LANGUAGE CUDA) - message(CONFIGURE_LOG - "Parsed CUDAToolkit nvcc implicit link information:\n${_nvcc_log}\n${_cmake_CUDAToolkit_implicit_link_directories}\n\n") - unset(_nvcc_link_line) - unset(_cmake_CUDAToolkit_implicit_link_libs) - unset(_cmake_CUDAToolkit_implicit_frameworks) - - set(_cmake_CUDAToolkit_implicit_link_directories "${_cmake_CUDAToolkit_implicit_link_directories}" CACHE INTERNAL "CUDAToolkit internal list of implicit link directories") - endif() - unset(_CUDA_NVCC_OUT) - - set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}" CACHE PATH "" FORCE) - mark_as_advanced(CUDAToolkit_BIN_DIR) - endif() - - if(CUDAToolkit_SENTINEL_FILE) - get_filename_component(CUDAToolkit_BIN_DIR ${CUDAToolkit_SENTINEL_FILE} DIRECTORY ABSOLUTE) - set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}/bin") - - set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}" CACHE PATH "" FORCE) - mark_as_advanced(CUDAToolkit_BIN_DIR) - endif() - endif() - - if(DEFINED _cmake_CUDAToolkit_include_directories) - _CUDAToolkit_build_include_dirs(_cmake_CUDAToolkit_contents _cmake_CUDAToolkit_include_directories) - set(CUDAToolkit_INCLUDE_DIRECTORIES "${_cmake_CUDAToolkit_contents}" PARENT_SCOPE) - endif() - if(DEFINED _cmake_CUDAToolkit_implicit_link_directories) - _CUDAToolkit_build_library_dirs(_cmake_CUDAToolkit_contents _cmake_CUDAToolkit_implicit_link_directories) - set(CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES "${_cmake_CUDAToolkit_contents}" PARENT_SCOPE) - endif() - - if(CUDAToolkit_BIN_DIR) - get_filename_component(CUDAToolkit_ROOT_DIR ${CUDAToolkit_BIN_DIR} DIRECTORY ABSOLUTE) - set(CUDAToolkit_ROOT_DIR "${CUDAToolkit_ROOT_DIR}" PARENT_SCOPE) - endif() - - endfunction() - - function(_CUDAToolkit_find_version_file result_variable) - # We first check for a non-scattered installation to prefer it over a scattered installation. - set(version_files version.txt version.json) - foreach(vf IN LISTS version_files) - if(CUDAToolkit_ROOT AND EXISTS "${CUDAToolkit_ROOT}/${vf}") - set(${result_variable} "${CUDAToolkit_ROOT}/${vf}" PARENT_SCOPE) - break() - elseif(CUDAToolkit_ROOT_DIR AND EXISTS "${CUDAToolkit_ROOT_DIR}/${vf}") - set(${result_variable} "${CUDAToolkit_ROOT_DIR}/${vf}" PARENT_SCOPE) - break() - elseif(CMAKE_SYSROOT_LINK AND EXISTS "${CMAKE_SYSROOT_LINK}/usr/lib/cuda/${vf}") - set(${result_variable} "${CMAKE_SYSROOT_LINK}/usr/lib/cuda/${vf}" PARENT_SCOPE) - break() - elseif(EXISTS "${CMAKE_SYSROOT}/usr/lib/cuda/${vf}") - set(${result_variable} "${CMAKE_SYSROOT}/usr/lib/cuda/${vf}" PARENT_SCOPE) - break() - endif() - endforeach() - endfunction() - - function(_CUDAToolkit_parse_version_file version_file) - if(version_file) - file(READ "${version_file}" file_conents) - cmake_path(GET version_file EXTENSION LAST_ONLY version_ext) - if(version_ext STREQUAL ".json") - string(JSON cuda_version_info GET "${file_conents}" "cuda" "version") - set(cuda_version_match_regex [=[([0-9]+)\.([0-9]+)\.([0-9]+)]=]) - elseif(version_ext STREQUAL ".txt") - set(cuda_version_info "${file_conents}") - set(cuda_version_match_regex [=[CUDA Version ([0-9]+)\.([0-9]+)\.([0-9]+)]=]) - endif() - - if(cuda_version_info MATCHES "${cuda_version_match_regex}") - set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}" PARENT_SCOPE) - set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}" PARENT_SCOPE) - set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}" PARENT_SCOPE) - set(CUDAToolkit_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}" PARENT_SCOPE) - endif() - endif() - endfunction() - - # For NVCC we can easily deduce the SDK binary directory from the compiler path. - if(CMAKE_CUDA_COMPILER_LOADED AND NOT CUDAToolkit_BIN_DIR AND CMAKE_CUDA_COMPILER_ID STREQUAL "NVIDIA") - get_filename_component(CUDAToolkit_BIN_DIR "${CMAKE_CUDA_COMPILER}" DIRECTORY) - set(CUDAToolkit_BIN_DIR "${CUDAToolkit_BIN_DIR}" CACHE PATH "") - # Try language provided path first. - _CUDAToolkit_find_root_dir(SEARCH_PATHS "${CUDAToolkit_BIN_DIR}" FIND_FLAGS NO_DEFAULT_PATH) - mark_as_advanced(CUDAToolkit_BIN_DIR) - endif() - - # Try user provided path - _CUDAToolkit_find_root_dir(COMPILER_PATHS) - if(NOT CUDAToolkit_ROOT_DIR AND CUDAToolkit_ROOT) - _CUDAToolkit_find_root_dir(SEARCH_PATHS "${CUDAToolkit_ROOT}" FIND_FLAGS PATH_SUFFIXES bin NO_DEFAULT_PATH) - endif() - if(NOT CUDAToolkit_ROOT_DIR) - _CUDAToolkit_find_root_dir(FIND_FLAGS PATHS ENV CUDA_PATH PATH_SUFFIXES bin) - endif() - - # If the user specified CUDAToolkit_ROOT but the toolkit could not be found, this is an error. - if(NOT CUDAToolkit_ROOT_DIR AND (DEFINED CUDAToolkit_ROOT OR DEFINED ENV{CUDAToolkit_ROOT})) - # Declare error messages now, print later depending on find_package args. - set(fail_base "Could not find nvcc executable in path specified by") - set(cuda_root_fail "${fail_base} CUDAToolkit_ROOT=${CUDAToolkit_ROOT}") - set(env_cuda_root_fail "${fail_base} environment variable CUDAToolkit_ROOT=$ENV{CUDAToolkit_ROOT}") - - if(CUDAToolkit_FIND_REQUIRED) - if(DEFINED CUDAToolkit_ROOT) - message(FATAL_ERROR ${cuda_root_fail}) - elseif(DEFINED ENV{CUDAToolkit_ROOT}) - message(FATAL_ERROR ${env_cuda_root_fail}) - endif() - else() - if(NOT CUDAToolkit_FIND_QUIETLY) - if(DEFINED CUDAToolkit_ROOT) - message(STATUS ${cuda_root_fail}) - elseif(DEFINED ENV{CUDAToolkit_ROOT}) - message(STATUS ${env_cuda_root_fail}) - endif() - endif() - set(CUDAToolkit_FOUND FALSE) - unset(fail_base) - unset(cuda_root_fail) - unset(env_cuda_root_fail) - return() - endif() - endif() - - # CUDAToolkit_ROOT cmake / env variable not specified, try platform defaults. - # - # - Linux: /usr/local/cuda-X.Y - # - macOS: /Developer/NVIDIA/CUDA-X.Y - # - Windows: C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vX.Y - # - # We will also search the default symlink location /usr/local/cuda first since - # if CUDAToolkit_ROOT is not specified, it is assumed that the symlinked - # directory is the desired location. - if(NOT CUDAToolkit_ROOT_DIR) - if(UNIX) - if(NOT APPLE) - set(platform_base "/usr/local/cuda-") - else() - set(platform_base "/Developer/NVIDIA/CUDA-") - endif() - else() - set(platform_base "C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v") - endif() - - # Build out a descending list of possible cuda installations, e.g. - file(GLOB possible_paths "${platform_base}*") - # Iterate the glob results and create a descending list. - set(versions) - foreach(p ${possible_paths}) - # Extract version number from end of string - string(REGEX MATCH "[0-9][0-9]?\\.[0-9]$" p_version ${p}) - if(IS_DIRECTORY ${p} AND p_version) - list(APPEND versions ${p_version}) - endif() - endforeach() - - # Sort numerically in descending order, so we try the newest versions first. - list(SORT versions COMPARE NATURAL ORDER DESCENDING) - - # With a descending list of versions, populate possible paths to search. - set(search_paths) - foreach(v ${versions}) - list(APPEND search_paths "${platform_base}${v}") - endforeach() - - # Force the global default /usr/local/cuda to the front on Unix. - if(UNIX) - list(INSERT search_paths 0 "/usr/local/cuda") - endif() - - # Now search for the toolkit again using the platform default search paths. - _CUDAToolkit_find_root_dir(SEARCH_PATHS "${search_paths}" FIND_FLAGS PATH_SUFFIXES bin) - - # We are done with these variables now, cleanup for caller. - unset(platform_base) - unset(possible_paths) - unset(versions) - unset(search_paths) - - if(NOT CUDAToolkit_ROOT_DIR) - if(CUDAToolkit_FIND_REQUIRED) - message(FATAL_ERROR "Could not find nvcc, please set CUDAToolkit_ROOT.") - elseif(NOT CUDAToolkit_FIND_QUIETLY) - message(STATUS "Could not find nvcc, please set CUDAToolkit_ROOT.") - endif() - - set(CUDAToolkit_FOUND FALSE) - return() - endif() - endif() - - _CUDAToolkit_find_version_file( _CUDAToolkit_version_file ) - if(_CUDAToolkit_version_file) - # CUDAToolkit_LIBRARY_ROOT contains the device library and version file. - get_filename_component(CUDAToolkit_LIBRARY_ROOT "${_CUDAToolkit_version_file}" DIRECTORY ABSOLUTE) - endif() - unset(_CUDAToolkit_version_file) - - if(CUDAToolkit_NVCC_EXECUTABLE AND - CMAKE_CUDA_COMPILER_VERSION AND - CUDAToolkit_NVCC_EXECUTABLE STREQUAL CMAKE_CUDA_COMPILER) - # Need to set these based off the already computed CMAKE_CUDA_COMPILER_VERSION value - # This if statement will always match, but is used to provide variables for MATCH 1,2,3... - if(CMAKE_CUDA_COMPILER_VERSION MATCHES [=[([0-9]+)\.([0-9]+)\.([0-9]+)]=]) - set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}") - set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}") - set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}") - set(CUDAToolkit_VERSION "${CMAKE_CUDA_COMPILER_VERSION}") - endif() - elseif(CUDAToolkit_NVCC_EXECUTABLE) - # Compute the version by invoking nvcc - execute_process(COMMAND ${CUDAToolkit_NVCC_EXECUTABLE} "--version" OUTPUT_VARIABLE NVCC_OUT) - if(NVCC_OUT MATCHES [=[ V([0-9]+)\.([0-9]+)\.([0-9]+)]=]) - set(CUDAToolkit_VERSION_MAJOR "${CMAKE_MATCH_1}") - set(CUDAToolkit_VERSION_MINOR "${CMAKE_MATCH_2}") - set(CUDAToolkit_VERSION_PATCH "${CMAKE_MATCH_3}") - set(CUDAToolkit_VERSION "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") - endif() - unset(NVCC_OUT) - else() - _CUDAToolkit_find_version_file(version_file) - _CUDAToolkit_parse_version_file("${version_file}") - endif() -endif() - -# Find target directory when crosscompiling. -if(CMAKE_CROSSCOMPILING) - if(CMAKE_SYSTEM_PROCESSOR STREQUAL "armv7-a") - # Support for NVPACK - set(CUDAToolkit_TARGET_NAMES "armv7-linux-androideabi") - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm") - set(CUDAToolkit_TARGET_NAMES "armv7-linux-gnueabihf") - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") - if(ANDROID_ARCH_NAME STREQUAL "arm64") - set(CUDAToolkit_TARGET_NAMES "aarch64-linux-androideabi") - elseif (CMAKE_SYSTEM_NAME STREQUAL "QNX") - set(CUDAToolkit_TARGET_NAMES "aarch64-qnx") - else() - set(CUDAToolkit_TARGET_NAMES "aarch64-linux" "sbsa-linux") - endif() - elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") - set(CUDAToolkit_TARGET_NAMES "x86_64-linux") - endif() - - foreach(CUDAToolkit_TARGET_NAME IN LISTS CUDAToolkit_TARGET_NAMES) - if(EXISTS "${CUDAToolkit_ROOT_DIR}/targets/${CUDAToolkit_TARGET_NAME}") - set(CUDAToolkit_TARGET_DIR "${CUDAToolkit_ROOT_DIR}/targets/${CUDAToolkit_TARGET_NAME}") - # add known CUDA target root path to the set of directories we search for programs, libraries and headers - list(PREPEND CMAKE_FIND_ROOT_PATH "${CUDAToolkit_TARGET_DIR}") - - # Mark that we need to pop the root search path changes after we have - # found all cuda libraries so that searches for our cross-compilation - # libraries work when another cuda sdk is in CMAKE_PREFIX_PATH or - # PATh - set(_CUDAToolkit_Pop_ROOT_PATH True) - break() - endif() - endforeach() -endif() - -# Determine windows search path suffix for libraries -if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") - if(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64") - set(_CUDAToolkit_win_search_dirs lib/x64) - set(_CUDAToolkit_win_stub_search_dirs lib/x64/stubs) - endif() -endif() - -# If not already set we can simply use the toolkit root or it's a scattered installation. -if(NOT CUDAToolkit_TARGET_DIR) - # Not cross compiling - set(CUDAToolkit_TARGET_DIR "${CUDAToolkit_ROOT_DIR}") - # Now that we have the real ROOT_DIR, find components inside it. - list(APPEND CMAKE_PREFIX_PATH ${CUDAToolkit_ROOT_DIR}) - - # Mark that we need to pop the prefix path changes after we have - # found the cudart library. - set(_CUDAToolkit_Pop_Prefix True) -endif() - - -# We don't need to verify the cuda_runtime header when we are using `nvcc` include paths -# as the compiler being enabled means the header was found -if(NOT CUDAToolkit_INCLUDE_DIRECTORIES) - # Otherwise use CUDAToolkit_TARGET_DIR to guess where the `cuda_runtime.h` is located - # On a scattered installation /usr, on a non-scattered something like /usr/local/cuda or /usr/local/cuda-10.2/targets/aarch64-linux. - if(EXISTS "${CUDAToolkit_TARGET_DIR}/include/cuda_runtime.h") - set(CUDAToolkit_INCLUDE_DIRECTORIES "${CUDAToolkit_TARGET_DIR}/include") - else() - message(STATUS "Unable to find cuda_runtime.h in \"${CUDAToolkit_TARGET_DIR}/include\" for CUDAToolkit_INCLUDE_DIRECTORIES.") - endif() -endif() - -# The NVHPC layout moves math library headers and libraries to a sibling directory and it could be nested under -# the version of the CUDA toolchain -# Create a separate variable so this directory can be selectively added to math targets. -find_path(CUDAToolkit_CUBLAS_INCLUDE_DIR cublas_v2.h PATHS - ${CUDAToolkit_INCLUDE_DIRECTORIES} - NO_DEFAULT_PATH) - -if(NOT CUDAToolkit_CUBLAS_INCLUDE_DIR) - file(REAL_PATH "${CUDAToolkit_TARGET_DIR}" CUDAToolkit_MATH_INCLUDE_DIR) - cmake_path(APPEND CUDAToolkit_MATH_INCLUDE_DIR "../../math_libs/") - if(EXISTS "${CUDAToolkit_MATH_INCLUDE_DIR}/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/") - cmake_path(APPEND CUDAToolkit_MATH_INCLUDE_DIR "${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/") - endif() - cmake_path(APPEND CUDAToolkit_MATH_INCLUDE_DIR "include") - cmake_path(NORMAL_PATH CUDAToolkit_MATH_INCLUDE_DIR) - - find_path(CUDAToolkit_CUBLAS_INCLUDE_DIR cublas_v2.h PATHS - ${CUDAToolkit_INCLUDE_DIRECTORIES} - ) - if(CUDAToolkit_CUBLAS_INCLUDE_DIR) - list(APPEND CUDAToolkit_INCLUDE_DIRECTORIES "${CUDAToolkit_CUBLAS_INCLUDE_DIR}") - endif() -endif() -unset(CUDAToolkit_CUBLAS_INCLUDE_DIR CACHE) -unset(CUDAToolkit_CUBLAS_INCLUDE_DIR) - -# Find the CUDA Runtime Library libcudart -find_library(CUDA_CUDART - NAMES cudart - PATHS ${CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES} - PATH_SUFFIXES lib64 ${_CUDAToolkit_win_search_dirs} -) -find_library(CUDA_CUDART - NAMES cudart - PATHS ${CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES} - PATH_SUFFIXES lib64/stubs ${_CUDAToolkit_win_stub_search_dirs} lib/stubs stubs -) - -if(NOT CUDA_CUDART AND NOT CUDAToolkit_FIND_QUIETLY) - message(STATUS "Unable to find cudart library.") -endif() - -if(_CUDAToolkit_Pop_Prefix) - list(REMOVE_AT CMAKE_PREFIX_PATH -1) - unset(_CUDAToolkit_Pop_Prefix) -endif() - -#----------------------------------------------------------------------------- -# Perform version comparison and validate all required variables are set. -include(${CMAKE_ROOT}/Modules/FindPackageHandleStandardArgs.cmake) -find_package_handle_standard_args(CUDAToolkit - REQUIRED_VARS - CUDAToolkit_INCLUDE_DIRECTORIES - CUDA_CUDART - CUDAToolkit_BIN_DIR - VERSION_VAR - CUDAToolkit_VERSION -) - -unset(CUDAToolkit_ROOT_DIR) -mark_as_advanced(CUDA_CUDART - CUDAToolkit_NVCC_EXECUTABLE - CUDAToolkit_SENTINEL_FILE - ) - -#----------------------------------------------------------------------------- -# Construct result variables -if(CUDAToolkit_FOUND) - set(CUDAToolkit_INCLUDE_DIRS "${CUDAToolkit_INCLUDE_DIRECTORIES}") - get_filename_component(CUDAToolkit_LIBRARY_DIR ${CUDA_CUDART} DIRECTORY ABSOLUTE) - - # Build search paths without any symlinks - file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}" _cmake_search_dir) - set(CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}") - - # Detect we are in a splayed nvhpc toolkit layout and add extra - # search paths without symlinks - if(CUDAToolkit_LIBRARY_DIR MATCHES ".*/cuda/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/lib64$") - # Search location for math_libs/ - block(SCOPE_FOR POLICIES) - cmake_policy(SET CMP0152 NEW) - file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}/../../../../../" _cmake_search_dir) - list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}") - - # Search location for extras like cupti - file(REAL_PATH "${CUDAToolkit_LIBRARY_DIR}/../../../" _cmake_search_dir) - list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${_cmake_search_dir}") - endblock() - endif() - - if(DEFINED CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES) - list(APPEND CUDAToolkit_LIBRARY_SEARCH_DIRS "${CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES}") - endif() - - # If no `CUDAToolkit_LIBRARY_ROOT` exists set it based on CUDAToolkit_LIBRARY_DIR - if(NOT DEFINED CUDAToolkit_LIBRARY_ROOT) - foreach(CUDAToolkit_search_loc IN LISTS CUDAToolkit_LIBRARY_DIR CUDAToolkit_BIN_DIR) - get_filename_component(CUDAToolkit_possible_lib_root "${CUDAToolkit_search_loc}" DIRECTORY ABSOLUTE) - if(EXISTS "${CUDAToolkit_possible_lib_root}/nvvm/") - set(CUDAToolkit_LIBRARY_ROOT "${CUDAToolkit_possible_lib_root}") - break() - endif() - endforeach() - unset(CUDAToolkit_search_loc) - unset(CUDAToolkit_possible_lib_root) - endif() -else() - # clear cache results when we fail - unset(_cmake_CUDAToolkit_implicit_link_directories CACHE) - unset(_cmake_CUDAToolkit_include_directories CACHE) - unset(CUDA_CUDART CACHE) - unset(CUDAToolkit_BIN_DIR CACHE) - unset(CUDAToolkit_NVCC_EXECUTABLE CACHE) - unset(CUDAToolkit_SENTINEL_FILE CACHE) -endif() -unset(CUDAToolkit_IMPLICIT_LIBRARY_DIRECTORIES) -unset(CUDAToolkit_INCLUDE_DIRECTORIES) - -#----------------------------------------------------------------------------- -# Construct import targets -if(CUDAToolkit_FOUND) - - function(_CUDAToolkit_find_and_add_import_lib lib_name) - cmake_parse_arguments(arg "" "" "ALT;DEPS;EXTRA_PATH_SUFFIXES;EXTRA_INCLUDE_DIRS;ONLY_SEARCH_FOR" ${ARGN}) - - if(arg_ONLY_SEARCH_FOR) - set(search_names ${arg_ONLY_SEARCH_FOR}) - else() - set(search_names ${lib_name} ${arg_ALT}) - endif() - - find_library(CUDA_${lib_name}_LIBRARY - NAMES ${search_names} - HINTS ${CUDAToolkit_LIBRARY_SEARCH_DIRS} - ENV CUDA_PATH - PATH_SUFFIXES nvidia/current lib64 ${_CUDAToolkit_win_search_dirs} lib - # Support NVHPC splayed math library layout - math_libs/${CUDAToolkit_VERSION_MAJOR}.${CUDAToolkit_VERSION_MINOR}/lib64 - math_libs/lib64 - ${arg_EXTRA_PATH_SUFFIXES} - ) - # Don't try any stub directories until we have exhausted all other - # search locations. - set(CUDA_IMPORT_PROPERTY IMPORTED_LOCATION) - set(CUDA_IMPORT_TYPE UNKNOWN) - if(NOT CUDA_${lib_name}_LIBRARY) - find_library(CUDA_${lib_name}_LIBRARY - NAMES ${search_names} - HINTS ${CUDAToolkit_LIBRARY_SEARCH_DIRS} - ENV CUDA_PATH - PATH_SUFFIXES lib64/stubs ${_CUDAToolkit_win_stub_search_dirs} lib/stubs stubs - ) - endif() - if(CUDA_${lib_name}_LIBRARY MATCHES "/stubs/" AND NOT CUDA_${lib_name}_LIBRARY MATCHES "\\.a$" AND NOT WIN32) - # Use a SHARED library with IMPORTED_IMPLIB, but not IMPORTED_LOCATION, - # to indicate that the stub is for linkers but not dynamic loaders. - # It will not contribute any RPATH entry. When encountered as - # a private transitive dependency of another shared library, - # it will be passed explicitly to linkers so they can find it - # even when the runtime library file does not exist on disk. - set(CUDA_IMPORT_PROPERTY IMPORTED_IMPLIB) - set(CUDA_IMPORT_TYPE SHARED) - endif() - - mark_as_advanced(CUDA_${lib_name}_LIBRARY) - - if (NOT TARGET CUDA::${lib_name} AND CUDA_${lib_name}_LIBRARY) - add_library(CUDA::${lib_name} ${CUDA_IMPORT_TYPE} IMPORTED) - target_include_directories(CUDA::${lib_name} SYSTEM INTERFACE "${CUDAToolkit_INCLUDE_DIRS}") - if(DEFINED CUDAToolkit_MATH_INCLUDE_DIR) - string(FIND ${CUDA_${lib_name}_LIBRARY} "math_libs" math_libs) - if(NOT ${math_libs} EQUAL -1) - target_include_directories(CUDA::${lib_name} SYSTEM INTERFACE "${CUDAToolkit_MATH_INCLUDE_DIR}") - endif() - endif() - set_property(TARGET CUDA::${lib_name} PROPERTY ${CUDA_IMPORT_PROPERTY} "${CUDA_${lib_name}_LIBRARY}") - foreach(dep ${arg_DEPS}) - if(TARGET CUDA::${dep}) - target_link_libraries(CUDA::${lib_name} INTERFACE CUDA::${dep}) - endif() - endforeach() - if(arg_EXTRA_INCLUDE_DIRS) - target_include_directories(CUDA::${lib_name} SYSTEM INTERFACE "${arg_EXTRA_INCLUDE_DIRS}") - endif() - endif() - endfunction() - - if(NOT TARGET CUDA::toolkit) - add_library(CUDA::toolkit IMPORTED INTERFACE) - target_include_directories(CUDA::toolkit SYSTEM INTERFACE "${CUDAToolkit_INCLUDE_DIRS}") - target_link_directories(CUDA::toolkit INTERFACE "${CUDAToolkit_LIBRARY_DIR}") - endif() - - # setup dependencies that are required for cudart/cudart_static when building - # on linux. These are generally only required when using the CUDA toolkit - # when CUDA language is disabled - if(NOT TARGET CUDA::cudart_static_deps) - add_library(CUDA::cudart_static_deps IMPORTED INTERFACE) - if(UNIX AND (CMAKE_C_COMPILER OR CMAKE_CXX_COMPILER)) - find_package(Threads REQUIRED) - target_link_libraries(CUDA::cudart_static_deps INTERFACE Threads::Threads ${CMAKE_DL_LIBS}) - endif() - - if(UNIX AND NOT APPLE AND NOT (CMAKE_SYSTEM_NAME STREQUAL "QNX")) - # On Linux, you must link against librt when using the static cuda runtime. - find_library(CUDAToolkit_rt_LIBRARY rt) - mark_as_advanced(CUDAToolkit_rt_LIBRARY) - if(NOT CUDAToolkit_rt_LIBRARY) - message(WARNING "Could not find librt library, needed by CUDA::cudart_static") - else() - target_link_libraries(CUDA::cudart_static_deps INTERFACE ${CUDAToolkit_rt_LIBRARY}) - endif() - endif() - endif() - - _CUDAToolkit_find_and_add_import_lib(cuda_driver ALT cuda DEPS cudart_static_deps) - _CUDAToolkit_find_and_add_import_lib(cudart DEPS cudart_static_deps) - _CUDAToolkit_find_and_add_import_lib(cudart_static DEPS cudart_static_deps) - - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 12.0.0) - _CUDAToolkit_find_and_add_import_lib(nvJitLink) - _CUDAToolkit_find_and_add_import_lib(nvJitLink_static DEPS cudart_static_deps) - endif() - - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 12.4.0) - _CUDAToolkit_find_and_add_import_lib(nvfatbin DEPS cudart_static_deps) - _CUDAToolkit_find_and_add_import_lib(nvfatbin_static DEPS cudart_static_deps) - endif() - - _CUDAToolkit_find_and_add_import_lib(culibos) # it's a static library - foreach (cuda_lib cublasLt cufft nvjpeg) - _CUDAToolkit_find_and_add_import_lib(${cuda_lib}) - _CUDAToolkit_find_and_add_import_lib(${cuda_lib}_static DEPS cudart_static_deps culibos) - endforeach() - foreach (cuda_lib curand nppc) - _CUDAToolkit_find_and_add_import_lib(${cuda_lib}) - _CUDAToolkit_find_and_add_import_lib(${cuda_lib}_static DEPS culibos) - endforeach() - - _CUDAToolkit_find_and_add_import_lib(cusparse DEPS nvJitLink) - _CUDAToolkit_find_and_add_import_lib(cusparse_static DEPS nvJitLink_static culibos) - - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.0.0) - # cublas depends on cublasLt - # https://docs.nvidia.com/cuda/archive/11.0/cublas#static-library - _CUDAToolkit_find_and_add_import_lib(cublas DEPS cublasLt culibos) - _CUDAToolkit_find_and_add_import_lib(cublas_static DEPS cublasLt_static culibos) - else() - _CUDAToolkit_find_and_add_import_lib(cublas DEPS culibos) - _CUDAToolkit_find_and_add_import_lib(cublas_static DEPS culibos) - endif() - - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.4) - _CUDAToolkit_find_and_add_import_lib(cuFile ALT cufile DEPS culibos) - _CUDAToolkit_find_and_add_import_lib(cuFile_static ALT cufile_static DEPS culibos) - - _CUDAToolkit_find_and_add_import_lib(cuFile_rdma ALT cufile_rdma DEPS cuFile culibos) - _CUDAToolkit_find_and_add_import_lib(cuFile_rdma_static ALT cufile_rdma_static DEPS cuFile_static culibos) - endif() - - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.6) - _CUDAToolkit_find_and_add_import_lib(cudla) - endif() - - - # cuFFTW depends on cuFFT - _CUDAToolkit_find_and_add_import_lib(cufftw DEPS cufft) - _CUDAToolkit_find_and_add_import_lib(cufftw_static DEPS cufft_static) - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 9.2) - _CUDAToolkit_find_and_add_import_lib(cufft_static_nocallback DEPS culibos) - endif() - - # cuSOLVER depends on cuBLAS, and cuSPARSE - set(cusolver_deps cublas cusparse) - set(cusolver_static_deps cublas_static cusparse_static culibos) - if(CUDAToolkit_VERSION VERSION_GREATER 11.2.1) - # cusolver depends on libcusolver_metis and cublasLt - # https://docs.nvidia.com/cuda/archive/11.2.2/cusolver#link-dependency - list(APPEND cusolver_deps cublasLt) - _CUDAToolkit_find_and_add_import_lib(cusolver_metis_static ALT metis_static) # implementation detail static lib - list(APPEND cusolver_static_deps cusolver_metis_static cublasLt_static) - endif() - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 10.1.2) - # cusolver depends on liblapack_static.a starting with CUDA 10.1 update 2, - # https://docs.nvidia.com/cuda/archive/11.5.0/cusolver#static-link-lapack - _CUDAToolkit_find_and_add_import_lib(cusolver_lapack_static ALT lapack_static) # implementation detail static lib - list(APPEND cusolver_static_deps cusolver_lapack_static) - endif() - _CUDAToolkit_find_and_add_import_lib(cusolver DEPS ${cusolver_deps}) - _CUDAToolkit_find_and_add_import_lib(cusolver_static DEPS ${cusolver_static_deps}) - unset(cusolver_deps) - unset(cusolver_static_deps) - - # nvGRAPH depends on cuRAND, and cuSOLVER. - _CUDAToolkit_find_and_add_import_lib(nvgraph DEPS curand cusolver) - _CUDAToolkit_find_and_add_import_lib(nvgraph_static DEPS curand_static cusolver_static) - - # Process the majority of the NPP libraries. - foreach (cuda_lib nppial nppicc nppidei nppif nppig nppim nppist nppitc npps nppicom nppisu) - _CUDAToolkit_find_and_add_import_lib(${cuda_lib} DEPS nppc) - _CUDAToolkit_find_and_add_import_lib(${cuda_lib}_static DEPS nppc_static) - endforeach() - - find_path(CUDAToolkit_CUPTI_INCLUDE_DIR cupti.h PATHS - "${CUDAToolkit_ROOT_DIR}/extras/CUPTI/include" - ${CUDAToolkit_INCLUDE_DIRS} - PATH_SUFFIXES "../extras/CUPTI/include" - "../../../extras/CUPTI/include" - NO_DEFAULT_PATH) - mark_as_advanced(CUDAToolkit_CUPTI_INCLUDE_DIR) - - if(CUDAToolkit_CUPTI_INCLUDE_DIR) - set(_cmake_cupti_extra_paths extras/CUPTI/lib64/ - extras/CUPTI/lib/ - ../extras/CUPTI/lib64/ - ../extras/CUPTI/lib/) - _CUDAToolkit_find_and_add_import_lib(cupti - EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} - EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") - _CUDAToolkit_find_and_add_import_lib(cupti_static - EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} - EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 10.2.0) - _CUDAToolkit_find_and_add_import_lib(nvperf_host - EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} - EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") - _CUDAToolkit_find_and_add_import_lib(nvperf_host_static - EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} - EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") - _CUDAToolkit_find_and_add_import_lib(nvperf_target - EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} - EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") - endif() - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.3.0) - _CUDAToolkit_find_and_add_import_lib(pcsamplingutil - EXTRA_PATH_SUFFIXES ${_cmake_cupti_extra_paths} - EXTRA_INCLUDE_DIRS "${CUDAToolkit_CUPTI_INCLUDE_DIR}") - endif() - endif() - - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.1.0) - if(NOT TARGET CUDA::nvptxcompiler_static) - _CUDAToolkit_find_and_add_import_lib(nvptxcompiler_static) - if(TARGET CUDA::nvptxcompiler_static) - target_link_libraries(CUDA::nvptxcompiler_static INTERFACE CUDA::cudart_static_deps) - endif() - endif() - endif() - - _CUDAToolkit_find_and_add_import_lib(nvrtc_builtins ALT nvrtc-builtins) - _CUDAToolkit_find_and_add_import_lib(nvrtc) - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 11.5.0) - _CUDAToolkit_find_and_add_import_lib(nvrtc_builtins_static ALT nvrtc-builtins_static) - if(NOT TARGET CUDA::nvrtc_static) - _CUDAToolkit_find_and_add_import_lib(nvrtc_static DEPS nvrtc_builtins_static nvptxcompiler_static) - if(TARGET CUDA::nvrtc_static AND WIN32 AND NOT (BORLAND OR MINGW OR CYGWIN)) - target_link_libraries(CUDA::nvrtc_static INTERFACE Ws2_32.lib) - endif() - endif() - endif() - - _CUDAToolkit_find_and_add_import_lib(nvml ALT nvidia-ml nvml) - _CUDAToolkit_find_and_add_import_lib(nvml_static ONLY_SEARCH_FOR libnvidia-ml.a libnvml.a) - - if(WIN32) - # nvtools can be installed outside the CUDA toolkit directory - # so prefer the NVTOOLSEXT_PATH windows only environment variable - # In addition on windows the most common name is nvToolsExt64_1 - find_library(CUDA_nvToolsExt_LIBRARY - NAMES nvToolsExt64_1 nvToolsExt64 nvToolsExt - PATHS ENV NVTOOLSEXT_PATH - ENV CUDA_PATH - PATH_SUFFIXES lib/x64 lib - ) - endif() - _CUDAToolkit_find_and_add_import_lib(nvToolsExt ALT nvToolsExt64) - - if(CUDAToolkit_VERSION VERSION_GREATER_EQUAL 10.0) - # nvToolsExt is deprecated since nvtx3 introduction. - # Warn only if the project requires a sufficiently new CMake to make migration possible. - if(TARGET CUDA::nvToolsExt AND CMAKE_MINIMUM_REQUIRED_VERSION VERSION_GREATER_EQUAL 3.25) - set_property(TARGET CUDA::nvToolsExt PROPERTY DEPRECATION "nvToolsExt has been superseded by nvtx3 since CUDA 10.0 and CMake 3.25. Use CUDA::nvtx3 and include instead.") - endif() - - # Header-only variant. Uses dlopen(). - if(NOT TARGET CUDA::nvtx3) - add_library(CUDA::nvtx3 INTERFACE IMPORTED) - target_include_directories(CUDA::nvtx3 SYSTEM INTERFACE "${CUDAToolkit_INCLUDE_DIRS}") - target_link_libraries(CUDA::nvtx3 INTERFACE ${CMAKE_DL_LIBS}) - endif() - endif() - - _CUDAToolkit_find_and_add_import_lib(OpenCL) -endif() - -if(_CUDAToolkit_Pop_ROOT_PATH) - list(REMOVE_AT CMAKE_FIND_ROOT_PATH 0) - unset(_CUDAToolkit_Pop_ROOT_PATH) -endif() - -unset(_CUDAToolkit_win_search_dirs) -unset(_CUDAToolkit_win_stub_search_dirs) From 8c7eecfc55d8e3ed91ef327c75cce368ad5375b6 Mon Sep 17 00:00:00 2001 From: Vukasin Milovanovic Date: Mon, 24 Feb 2025 15:58:59 -0800 Subject: [PATCH 18/45] Read the footers in parallel when reading multiple Parquet files (#17957) Depends on https://github.com/rapidsai/cudf/pull/18018 When reading multiple files, all data(i.e. pages) IO is performed in the same "batch", allowing parallel IO operations (provided by kvikIO). However, footers are read serially, leading to poor performance when reading many files. This is especially pronounced for IO that benefits from high level of parallelism. This PR performs footer reading/parsing asynchronously using an internal thread pool. The pool size can be controlled with an environment variable `LIBCUDF_NUM_HOST_WORKERS`. Authors: - Vukasin Milovanovic (https://github.com/vuule) Approvers: - Nghia Truong (https://github.com/ttnghia) - Paul Mattione (https://github.com/pmattione-nvidia) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17957 --- cpp/CMakeLists.txt | 1 + .../detail/utilities/host_worker_pool.hpp | 32 +++++++++++++++++++ cpp/src/io/parquet/reader_impl_helpers.cpp | 19 ++++++++--- cpp/src/utilities/host_worker_pool.cpp | 32 +++++++++++++++++++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 cpp/include/cudf/detail/utilities/host_worker_pool.hpp create mode 100644 cpp/src/utilities/host_worker_pool.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index bb4d20f837c..0282282b5f3 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -773,6 +773,7 @@ add_library( src/utilities/cuda_memcpy.cu src/utilities/default_stream.cpp src/utilities/host_memory.cpp + src/utilities/host_worker_pool.cpp src/utilities/linked_column.cpp src/utilities/logger.cpp src/utilities/prefetch.cpp diff --git a/cpp/include/cudf/detail/utilities/host_worker_pool.hpp b/cpp/include/cudf/detail/utilities/host_worker_pool.hpp new file mode 100644 index 00000000000..7bd0cab76bc --- /dev/null +++ b/cpp/include/cudf/detail/utilities/host_worker_pool.hpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025, 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. + */ + +#include + +namespace cudf::detail { + +/** + * @brief Retrieves a reference to the global host worker thread pool. + * + * This function returns a reference to a thread pool that can be used for executing host-only + * tasks. The pool size is potentially not optimal for tasks that include device operations, like + * copies between host and device and kernel calls. + * + * @return A reference to the host worker thread pool. + */ +BS::thread_pool& host_worker_pool(); + +} // namespace cudf::detail diff --git a/cpp/src/io/parquet/reader_impl_helpers.cpp b/cpp/src/io/parquet/reader_impl_helpers.cpp index 768ca384352..ffc164964a5 100644 --- a/cpp/src/io/parquet/reader_impl_helpers.cpp +++ b/cpp/src/io/parquet/reader_impl_helpers.cpp @@ -23,6 +23,7 @@ #include "ipc/Message_generated.h" #include "ipc/Schema_generated.h" +#include #include #include @@ -352,11 +353,21 @@ metadata::metadata(datasource* source) std::vector aggregate_reader_metadata::metadatas_from_sources( host_span const> sources) { + // Avoid using the thread pool for a single source + if (sources.size() == 1) { return {metadata{sources[0].get()}}; } + + std::vector> metadata_ctor_tasks; + metadata_ctor_tasks.reserve(sources.size()); + for (auto const& source : sources) { + metadata_ctor_tasks.emplace_back(cudf::detail::host_worker_pool().submit_task( + [source = source.get()] { return metadata{source}; })); + } std::vector metadatas; - std::transform( - sources.begin(), sources.end(), std::back_inserter(metadatas), [](auto const& source) { - return metadata(source.get()); - }); + metadatas.reserve(sources.size()); + std::transform(metadata_ctor_tasks.begin(), + metadata_ctor_tasks.end(), + std::back_inserter(metadatas), + [](std::future& task) { return std::move(task).get(); }); return metadatas; } diff --git a/cpp/src/utilities/host_worker_pool.cpp b/cpp/src/utilities/host_worker_pool.cpp new file mode 100644 index 00000000000..fa0b8b6620d --- /dev/null +++ b/cpp/src/utilities/host_worker_pool.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025, 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. + */ + +#include "io/utilities/getenv_or.hpp" + +#include + +namespace cudf::detail { + +BS::thread_pool& host_worker_pool() +{ + static const std::size_t default_pool_size = + std::min(32u, std::thread::hardware_concurrency() / 2); + static const std::size_t pool_size = getenv_or("LIBCUDF_NUM_HOST_WORKERS", default_pool_size); + static BS::thread_pool pool(pool_size); + return pool; +} + +} // namespace cudf::detail From 5f71f7665977d3c976e37d30cc64adea49235776 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:50:35 -0800 Subject: [PATCH 19/45] Support IntervalDtype(subtype=None) (#18017) closes https://github.com/rapidsai/cudf/issues/17997 Will help unblock https://github.com/rapidsai/cudf/pull/17978 where we will need to interpret `dtype="interval"` as "interval without a subtype" instead of "interval with float64 subtype" Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/18017 --- docs/cudf/source/conf.py | 2 + python/cudf/cudf/core/column/interval.py | 8 +- python/cudf/cudf/core/dtypes.py | 131 ++++++++++++++--------- python/cudf/cudf/core/index.py | 2 +- python/cudf/cudf/tests/test_interval.py | 11 +- 5 files changed, 95 insertions(+), 59 deletions(-) diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index ac34c10d22f..c74da8d0ca9 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -593,6 +593,8 @@ def on_missing_reference(app, env, node, contnode): ("py:class", "pyarrow.lib.ChunkedArray"), ("py:class", "pyarrow.lib.Array"), ("py:class", "ColumnLike"), + ("py:class", "DtypeObj"), + ("py:class", "pa.StructType"), # TODO: Remove this when we figure out why typing_extensions doesn't seem # to map types correctly for intersphinx ("py:class", "typing_extensions.Self"), diff --git a/python/cudf/cudf/core/column/interval.py b/python/cudf/cudf/core/column/interval.py index dd8f58a118e..2be85fcaa83 100644 --- a/python/cudf/cudf/core/column/interval.py +++ b/python/cudf/cudf/core/column/interval.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018-2024, NVIDIA CORPORATION. +# Copyright (c) 2018-2025, NVIDIA CORPORATION. from __future__ import annotations from typing import TYPE_CHECKING, Literal @@ -105,9 +105,7 @@ def copy(self, deep: bool = True) -> Self: return IntervalColumn( # type: ignore[return-value] data=None, size=struct_copy.size, - dtype=IntervalDtype( - struct_copy.dtype.fields["left"], self.dtype.closed - ), + dtype=IntervalDtype(self.dtype.subtype, self.dtype.closed), mask=struct_copy.base_mask, offset=struct_copy.offset, null_count=struct_copy.null_count, @@ -163,7 +161,7 @@ def set_closed( return IntervalColumn( # type: ignore[return-value] data=None, size=self.size, - dtype=IntervalDtype(self.dtype.fields["left"], closed), + dtype=IntervalDtype(self.dtype.subtype, closed), mask=self.base_mask, offset=self.offset, null_count=self.null_count, diff --git a/python/cudf/cudf/core/dtypes.py b/python/cudf/cudf/core/dtypes.py index 12a9cce9f1c..977208f5eb4 100644 --- a/python/cudf/cudf/core/dtypes.py +++ b/python/cudf/cudf/core/dtypes.py @@ -6,7 +6,7 @@ import textwrap import warnings from functools import cached_property -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import numpy as np import pandas as pd @@ -19,7 +19,11 @@ from cudf.core._compat import PANDAS_GE_210, PANDAS_LT_300 from cudf.core.abc import Serializable from cudf.utils.docutils import doc_apply -from cudf.utils.dtypes import CUDF_STRING_DTYPE, cudf_dtype_from_pa_type +from cudf.utils.dtypes import ( + CUDF_STRING_DTYPE, + cudf_dtype_from_pa_type, + cudf_dtype_to_pa_type, +) if PANDAS_GE_210: PANDAS_NUMPY_DTYPE = pd.core.dtypes.dtypes.NumpyEADtype @@ -29,7 +33,9 @@ if TYPE_CHECKING: from collections.abc import Callable - from cudf._typing import Dtype + from typing_extension import Self + + from cudf._typing import Dtype, DtypeObj from cudf.core.buffer import Buffer @@ -573,15 +579,11 @@ class StructDtype(_BaseDtype): name = "struct" - def __init__(self, fields): - pa_fields = { - k: cudf.utils.dtypes.cudf_dtype_to_pa_type(cudf.dtype(v)) - for k, v in fields.items() - } - self._typ = pa.struct(pa_fields) + def __init__(self, fields: dict[str, Dtype]) -> None: + self._fields = {k: cudf.dtype(v) for k, v in fields.items()} @property - def fields(self): + def fields(self) -> dict[str, DtypeObj]: """ Returns an ordered dict of column name and dtype key-value. @@ -594,10 +596,7 @@ def fields(self): >>> struct_dtype.fields {'a': dtype('int64'), 'b': dtype('O')} """ - return { - field.name: cudf.utils.dtypes.cudf_dtype_from_pa_type(field.type) - for field in self._typ - } + return self._fields @property def type(self): @@ -606,7 +605,7 @@ def type(self): return dict @classmethod - def from_arrow(cls, typ): + def from_arrow(cls, typ: pa.StructType) -> Self: """ Convert a ``pyarrow.StructType`` to ``StructDtype``. @@ -620,11 +619,19 @@ def from_arrow(cls, typ): >>> cudf.StructDtype.from_arrow(pa_struct_type) StructDtype({'x': dtype('int32'), 'y': dtype('O')}) """ - obj = object.__new__(cls) - obj._typ = typ - return obj + return cls( + { + typ.field(i).name: cudf_dtype_from_pa_type(typ.field(i).type) + for i in range(typ.num_fields) + } + # Once pyarrow 18 is the min version, replace with this version + # { + # field.name: cudf_dtype_from_pa_type(field.type) + # for field in typ.fields + # } + ) - def to_arrow(self): + def to_arrow(self) -> pa.StructType: """ Convert a ``StructDtype`` to a ``pyarrow.StructType``. @@ -637,20 +644,25 @@ def to_arrow(self): >>> struct_type.to_arrow() StructType(struct) """ - return self._typ + return pa.struct( + { + k: cudf_dtype_to_pa_type(dtype) + for k, dtype in self.fields.items() + } + ) - def __eq__(self, other): + def __eq__(self, other) -> bool: if isinstance(other, str): return other == self.name if not isinstance(other, StructDtype): return False - return self._typ.equals(other._typ) + return self.to_arrow().equals(other.to_arrow()) - def __repr__(self): + def __repr__(self) -> str: return f"{type(self).__name__}({self.fields})" - def __hash__(self): - return hash(self._typ) + def __hash__(self) -> int: + return hash(self.to_arrow()) def serialize(self) -> tuple[dict, list]: header: dict[str, Any] = {} @@ -674,7 +686,7 @@ def serialize(self) -> tuple[dict, list]: return header, frames @classmethod - def deserialize(cls, header: dict, frames: list): + def deserialize(cls, header: dict, frames: list) -> Self: _check_type(cls, header, frames) fields = {} for k, dtype in header["fields"].items(): @@ -689,11 +701,8 @@ def deserialize(cls, header: dict, frames: list): return cls(fields) @cached_property - def itemsize(self): - return sum( - cudf.utils.dtypes.cudf_dtype_from_pa_type(field.type).itemsize - for field in self._typ - ) + def itemsize(self) -> int: + return sum(field.itemsize for field in self.fields.values()) def _recursively_replace_fields(self, result: dict) -> dict: """ @@ -926,6 +935,10 @@ class Decimal128Dtype(DecimalDtype): class IntervalDtype(StructDtype): """ + A data type for Interval data. + + Parameters + ---------- subtype: str, np.dtype The dtype of the Interval bounds. closed: {'right', 'left', 'both', 'neither'}, default 'right' @@ -935,43 +948,55 @@ class IntervalDtype(StructDtype): name = "interval" - def __init__(self, subtype, closed="right"): - super().__init__(fields={"left": subtype, "right": subtype}) - - if closed is None: - closed = "right" - if closed in ["left", "right", "neither", "both"]: + def __init__( + self, + subtype: None | Dtype = None, + closed: Literal["left", "right", "neither", "both"] = "right", + ) -> None: + if closed in {"left", "right", "neither", "both"}: self.closed = closed else: - raise ValueError("closed value is not valid") + raise ValueError(f"{closed=} is not valid") + if subtype is None: + self._subtype = None + dtypes = {} + else: + self._subtype = cudf.dtype(subtype) + dtypes = {"left": self._subtype, "right": self._subtype} + super().__init__(dtypes) @property - def subtype(self): - return self.fields["left"] + def subtype(self) -> DtypeObj | None: + return self._subtype def __repr__(self) -> str: + if self.subtype is None: + return "interval" return f"interval[{self.subtype}, {self.closed}]" def __str__(self) -> str: - return self.__repr__() + return repr(self) @classmethod - def from_arrow(cls, typ): - return IntervalDtype(typ.subtype.to_pandas_dtype(), typ.closed) + def from_arrow(cls, typ: ArrowIntervalType) -> Self: + return cls(typ.subtype.to_pandas_dtype(), typ.closed) - def to_arrow(self): + def to_arrow(self) -> ArrowIntervalType: return ArrowIntervalType( - pa.from_numpy_dtype(self.subtype), self.closed + cudf_dtype_to_pa_type(self.subtype), self.closed ) @classmethod - def from_pandas(cls, pd_dtype: pd.IntervalDtype) -> "IntervalDtype": - return cls(subtype=pd_dtype.subtype, closed=pd_dtype.closed) + def from_pandas(cls, pd_dtype: pd.IntervalDtype) -> Self: + return cls( + subtype=pd_dtype.subtype, + closed="right" if pd_dtype.closed is None else pd_dtype.closed, + ) def to_pandas(self) -> pd.IntervalDtype: return pd.IntervalDtype(subtype=self.subtype, closed=self.closed) - def __eq__(self, other): + def __eq__(self, other) -> bool: if isinstance(other, str): # This means equality isn't transitive but mimics pandas return other in (self.name, str(self)) @@ -981,21 +1006,23 @@ def __eq__(self, other): and self.closed == other.closed ) - def __hash__(self): + def __hash__(self) -> int: return hash((self.subtype, self.closed)) def serialize(self) -> tuple[dict, list]: header = { - "fields": (self.subtype.str, self.closed), + "fields": ( + self.subtype.str if self.subtype is not None else self.subtype, + self.closed, + ), "frame_count": 0, } return header, [] @classmethod - def deserialize(cls, header: dict, frames: list): + def deserialize(cls, header: dict, frames: list) -> Self: _check_type(cls, header, frames) subtype, closed = header["fields"] - subtype = np.dtype(subtype) return cls(subtype, closed=closed) diff --git a/python/cudf/cudf/core/index.py b/python/cudf/cudf/core/index.py index 8587bff2e32..1730a692dc1 100644 --- a/python/cudf/cudf/core/index.py +++ b/python/cudf/cudf/core/index.py @@ -3517,7 +3517,7 @@ def _from_column( def from_breaks( cls, breaks, - closed: Literal["left", "right", "neither", "both"] | None = "right", + closed: Literal["left", "right", "neither", "both"] = "right", name=None, copy: bool = False, dtype=None, diff --git a/python/cudf/cudf/tests/test_interval.py b/python/cudf/cudf/tests/test_interval.py index 5e1dd33fbf1..757eed0c9e3 100644 --- a/python/cudf/cudf/tests/test_interval.py +++ b/python/cudf/cudf/tests/test_interval.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. import numpy as np @@ -210,3 +210,12 @@ def test_reduction_return_interval_pandas_compatible(): result = cudf_ii.min() expected = ii.min() assert result == expected + + +def test_empty_intervaldtype(): + # "older pandas" supported closed=None, cudf chooses not to support that + pd_id = pd.IntervalDtype(closed="right") + cudf_id = cudf.IntervalDtype() + + assert str(pd_id) == str(cudf_id) + assert pd_id.subtype == cudf_id.subtype From 59d8f2697440caf79f90a332acbcde54ab4d4b3b Mon Sep 17 00:00:00 2001 From: Peter Andreas Entschev Date: Tue, 25 Feb 2025 16:30:28 +0100 Subject: [PATCH 20/45] Support Distributed in cudf-polars tests and IR evaluation (#17364) Support testing cudf-polars with the Distribute scheduler, and IR evaluation through serialization. Authors: - Peter Andreas Entschev (https://github.com/pentschev) - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Lawrence Mitchell (https://github.com/wence-) - Richard (Rick) Zamora (https://github.com/rjzamora) - Bradley Dice (https://github.com/bdice) - Kyle Edwards (https://github.com/KyleFromNVIDIA) URL: https://github.com/rapidsai/cudf/pull/17364 --- ci/run_cudf_polars_pytests.sh | 8 ++- .../cudf_polars/experimental/parallel.py | 54 +++++++++++++++++-- python/cudf_polars/tests/conftest.py | 46 +++++++++++++++- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/ci/run_cudf_polars_pytests.sh b/ci/run_cudf_polars_pytests.sh index bf5a3ccee8e..e881055e9e3 100755 --- a/ci/run_cudf_polars_pytests.sh +++ b/ci/run_cudf_polars_pytests.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. set -euo pipefail @@ -13,3 +13,9 @@ python -m pytest --cache-clear "$@" tests # Test the "dask-experimental" executor python -m pytest --cache-clear "$@" tests --executor dask-experimental + +# Test the "dask-experimental" executor with Distributed cluster +# Not all tests pass yet, deselecting by name those that are failing. +python -m pytest --cache-clear "$@" tests --executor dask-experimental --dask-cluster \ + -k "not test_groupby_maintain_order_random and not test_scan_csv_multi and not test_select_literal_series" \ + --cov-fail-under=89 # Override coverage, Distributed cluster coverage not yet 100% diff --git a/python/cudf_polars/cudf_polars/experimental/parallel.py b/python/cudf_polars/cudf_polars/experimental/parallel.py index 16290fdb663..e81866e68e4 100644 --- a/python/cudf_polars/cudf_polars/experimental/parallel.py +++ b/python/cudf_polars/cudf_polars/experimental/parallel.py @@ -7,7 +7,7 @@ import itertools import operator from functools import reduce -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar import cudf_polars.experimental.io import cudf_polars.experimental.join @@ -24,10 +24,38 @@ if TYPE_CHECKING: from collections.abc import MutableMapping + from distributed import Client + from cudf_polars.containers import DataFrame from cudf_polars.experimental.dispatch import LowerIRTransformer +class SerializerManager: + """Manager to ensure ensure serializer is only registered once.""" + + _serializer_registered: bool = False + _client_run_executed: ClassVar[set[str]] = set() + + @classmethod + def register_serialize(cls) -> None: + """Register Dask/cudf-polars serializers in calling process.""" + if not cls._serializer_registered: + from cudf_polars.experimental.dask_serialize import register + + register() + cls._serializer_registered = True + + @classmethod + def run_on_cluster(cls, client: Client) -> None: + """Run serializer registration on the workers and scheduler.""" + if ( + client.id not in cls._client_run_executed + ): # pragma: no cover; Only executes with Distributed scheduler + client.run(cls.register_serialize) + client.run_on_scheduler(cls.register_serialize) + cls._client_run_executed.add(client.id) + + @lower_ir_node.register(IR) def _(ir: IR, rec: LowerIRTransformer) -> tuple[IR, MutableMapping[IR, PartitionInfo]]: # Default logic - Requires single partition @@ -127,12 +155,32 @@ def task_graph( return graph, (key_name, 0) +def get_client(): + """Get appropriate Dask client or scheduler.""" + SerializerManager.register_serialize() + + try: # pragma: no cover; block depends on executor type and Distributed cluster + from distributed import get_client + + client = get_client() + SerializerManager.run_on_cluster(client) + except ( + ImportError, + ValueError, + ): # pragma: no cover; block depends on Dask local scheduler + from dask import get + + return get + else: # pragma: no cover; block depends on executor type and Distributed cluster + return client.get + + def evaluate_dask(ir: IR) -> DataFrame: """Evaluate an IR graph with Dask.""" - from dask import get - ir, partition_info = lower_ir_graph(ir) + get = get_client() + graph, key = task_graph(ir, partition_info) return get(graph, key) diff --git a/python/cudf_polars/tests/conftest.py b/python/cudf_polars/tests/conftest.py index 6338bf0cae1..dbd0989a8b2 100644 --- a/python/cudf_polars/tests/conftest.py +++ b/python/cudf_polars/tests/conftest.py @@ -1,9 +1,11 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import pytest +DISTRIBUTED_CLUSTER_KEY = pytest.StashKey[dict]() + @pytest.fixture(params=[False, True], ids=["no_nulls", "nulls"], scope="session") def with_nulls(request): @@ -19,8 +21,50 @@ def pytest_addoption(parser): help="Executor to use for GPUEngine.", ) + parser.addoption( + "--dask-cluster", + action="store_true", + help="Executor to use for GPUEngine.", + ) + def pytest_configure(config): import cudf_polars.testing.asserts + if ( + config.getoption("--dask-cluster") + and config.getoption("--executor") != "dask-experimental" + ): + raise pytest.UsageError( + "--dask-cluster requires --executor='dask-experimental'" + ) + cudf_polars.testing.asserts.Executor = config.getoption("--executor") + + +def pytest_sessionstart(session): + if ( + session.config.getoption("--dask-cluster") + and session.config.getoption("--executor") == "dask-experimental" + ): + from dask import config + from dask.distributed import Client, LocalCluster + + # Avoid "Sending large graph of size ..." warnings + # (We expect these for tests using literal/random arrays) + config.set({"distributed.admin.large-graph-warning-threshold": "20MB"}) + + cluster = LocalCluster() + client = Client(cluster) + session.stash[DISTRIBUTED_CLUSTER_KEY] = {"cluster": cluster, "client": client} + + +def pytest_sessionfinish(session): + if DISTRIBUTED_CLUSTER_KEY in session.stash: + cluster_info = session.stash[DISTRIBUTED_CLUSTER_KEY] + client = cluster_info.get("client") + cluster = cluster_info.get("cluster") + if client is not None: + client.shutdown() + if cluster is not None: + cluster.close() From 27d40b90fb8d1b7abe1e7b2fbdd50c3f507f45c2 Mon Sep 17 00:00:00 2001 From: Kyle Edwards Date: Tue, 25 Feb 2025 11:49:52 -0500 Subject: [PATCH 21/45] Remove `FindCUDAToolkit.cmake` from `.pre-commit-config.yaml` (#18087) Follow-up to #18081. Authors: - Kyle Edwards (https://github.com/KyleFromNVIDIA) - Bradley Dice (https://github.com/bdice) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/18087 --- .pre-commit-config.yaml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5daf124d83b..889e07bc681 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -107,10 +107,6 @@ repos: - cmakelang==0.6.13 verbose: true require_serial: true - exclude: | - (?x)^( - cpp/cmake/Modules/FindCUDAToolkit[.]cmake$ - ) - id: cmake-lint name: cmake-lint entry: ./cpp/scripts/run-cmake-format.sh cmake-lint @@ -122,10 +118,6 @@ repos: - cmakelang==0.6.13 verbose: true require_serial: true - exclude: | - (?x)^( - cpp/cmake/Modules/FindCUDAToolkit[.]cmake$ - ) - id: doxygen-check name: doxygen-check entry: ./ci/checks/doxygen.sh @@ -159,8 +151,7 @@ repos: (?x)^( cpp/include/cudf_test/cxxopts[.]hpp$| cpp/src/io/parquet/ipc/Message_generated[.]h$| - cpp/src/io/parquet/ipc/Schema_generated[.]h$| - cpp/cmake/Modules/FindCUDAToolkit[.]cmake$ + cpp/src/io/parquet/ipc/Schema_generated[.]h$ ) - id: verify-alpha-spec - id: verify-codeowners From 54c15b2a1a61f4d88437ab0433eecf27241bda77 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 25 Feb 2025 16:21:01 -0600 Subject: [PATCH 22/45] Use conda-build instead of conda-mambabuild (#18092) This changes from `conda mambabuild` to `conda build`. Conda now uses the mamba solver so no performance regressions are expected. This is a temporary change as we plan to migrate to `rattler-build` in the near future. However, this is needed sooner to drop `boa` and unblock Python 3.13 migrations. xref: https://github.com/rapidsai/build-planning/issues/149 Authors: - Bradley Dice (https://github.com/bdice) Approvers: - James Lamb (https://github.com/jameslamb) - Jake Awe (https://github.com/AyodeAwe) URL: https://github.com/rapidsai/cudf/pull/18092 --- ci/build_cpp.sh | 4 ++-- ci/build_python.sh | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ci/build_cpp.sh b/ci/build_cpp.sh index 3d06eacf9ff..0c324d01cdf 100755 --- a/ci/build_cpp.sh +++ b/ci/build_cpp.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2022-2025, NVIDIA CORPORATION. set -euo pipefail @@ -18,7 +18,7 @@ rapids-logger "Begin cpp build" sccache --zero-stats # With boa installed conda build forward to boa -RAPIDS_PACKAGE_VERSION=$(rapids-generate-version) rapids-conda-retry mambabuild \ +RAPIDS_PACKAGE_VERSION=$(rapids-generate-version) rapids-conda-retry build \ conda/recipes/libcudf sccache --show-adv-stats diff --git a/ci/build_python.sh b/ci/build_python.sh index ed90041cc77..abbdc3f3a3b 100755 --- a/ci/build_python.sh +++ b/ci/build_python.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2022-2025, NVIDIA CORPORATION. set -euo pipefail @@ -25,7 +25,7 @@ sccache --zero-stats # node works correctly # With boa installed conda build forwards to the boa builder -RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ +RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry build \ --no-test \ --channel "${CPP_CHANNEL}" \ conda/recipes/pylibcudf @@ -33,7 +33,7 @@ RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ sccache --show-adv-stats sccache --zero-stats -RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ +RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry build \ --no-test \ --channel "${CPP_CHANNEL}" \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ @@ -42,13 +42,13 @@ RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ sccache --show-adv-stats sccache --zero-stats -RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ +RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry build \ --no-test \ --channel "${CPP_CHANNEL}" \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ conda/recipes/dask-cudf -RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ +RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry build \ --no-test \ --channel "${CPP_CHANNEL}" \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ @@ -56,13 +56,13 @@ RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ sccache --show-adv-stats -RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ +RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry build \ --no-test \ --channel "${CPP_CHANNEL}" \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ conda/recipes/custreamz -RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry mambabuild \ +RAPIDS_PACKAGE_VERSION=$(head -1 ./VERSION) rapids-conda-retry build \ --no-test \ --channel "${CPP_CHANNEL}" \ --channel "${RAPIDS_CONDA_BLD_OUTPUT_DIR}" \ From 0f7a17f8767dfe5c00ea31feb894cf38a9fc1b6d Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 25 Feb 2025 15:17:40 -0800 Subject: [PATCH 23/45] Update numba dep and upper-bound numpy (#18078) This PR updates to numba-cuda 0.4 and numba 0.61. A numpy upper-bound is added since it looks like numpy 2.1 made some changes with which we are currently incompatible. Previously numba provided that upper bound for us. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/18078 --- conda/environments/all_cuda-118_arch-x86_64.yaml | 6 +++--- conda/environments/all_cuda-128_arch-x86_64.yaml | 6 +++--- conda/recipes/cudf/meta.yaml | 6 +++--- conda/recipes/pylibcudf/meta.yaml | 2 +- dependencies.yaml | 9 +++++---- python/cudf/pyproject.toml | 6 +++--- python/cudf_polars/pyproject.toml | 2 +- python/dask_cudf/pyproject.toml | 6 +++--- python/pylibcudf/pyproject.toml | 2 +- 9 files changed, 23 insertions(+), 22 deletions(-) diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index cc674732ba4..e7dbb765099 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -54,9 +54,9 @@ dependencies: - nbsphinx - ninja - notebook -- numba-cuda>=0.2.0,<0.3.0a0 -- numba>=0.59.1,<0.61.0a0 -- numpy>=1.23,<3.0a0 +- numba-cuda>=0.4.0,<0.5.0a0 +- numba>=0.59.1,<0.62.0a0 +- numpy>=1.23,<2.1 - numpydoc - nvcc_linux-64=11.8 - nvcomp==4.2.0.11 diff --git a/conda/environments/all_cuda-128_arch-x86_64.yaml b/conda/environments/all_cuda-128_arch-x86_64.yaml index 7593a72cc68..342ec8d4b59 100644 --- a/conda/environments/all_cuda-128_arch-x86_64.yaml +++ b/conda/environments/all_cuda-128_arch-x86_64.yaml @@ -53,9 +53,9 @@ dependencies: - nbsphinx - ninja - notebook -- numba-cuda>=0.2.0,<0.3.0a0 -- numba>=0.59.1,<0.61.0a0 -- numpy>=1.23,<3.0a0 +- numba-cuda>=0.4.0,<0.5.0a0 +- numba>=0.59.1,<0.62.0a0 +- numpy>=1.23,<2.1 - numpydoc - nvcomp==4.2.0.11 - nvtx>=0.2.1 diff --git a/conda/recipes/cudf/meta.yaml b/conda/recipes/cudf/meta.yaml index f817bc12c5b..43060ef1c87 100644 --- a/conda/recipes/cudf/meta.yaml +++ b/conda/recipes/cudf/meta.yaml @@ -75,9 +75,9 @@ requirements: - typing_extensions >=4.0.0 - pandas >=2.0,<2.2.4dev0 - cupy >=12.0.0 - - numba-cuda >=0.2.0,<0.3.0a0 - - numba >=0.59.1,<0.61.0a0 - - numpy >=1.23,<3.0a0 + - numba-cuda >=0.4.0,<0.5.0a0 + - numba >=0.59.1,<0.62.0a0 + - numpy >=1.23,<2.1 - pyarrow>=14.0.0,<20.0.0a0 - libcudf ={{ version }} - pylibcudf ={{ version }} diff --git a/conda/recipes/pylibcudf/meta.yaml b/conda/recipes/pylibcudf/meta.yaml index 14e2f31a5a5..ae02cf8d4e5 100644 --- a/conda/recipes/pylibcudf/meta.yaml +++ b/conda/recipes/pylibcudf/meta.yaml @@ -73,7 +73,7 @@ requirements: - python - typing_extensions >=4.0.0 - pandas >=2.0,<2.2.4dev0 - - numpy >=1.23,<3.0a0 + - numpy >=1.23,<2.1 - pyarrow>=14.0.0,<20.0.0a0 - libcudf ={{ version }} - {{ pin_compatible('rmm', max_pin='x.x') }} diff --git a/dependencies.yaml b/dependencies.yaml index e7840d56880..c7869eee922 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -723,7 +723,7 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - fsspec>=0.6.0 - - &numpy numpy>=1.23,<3.0a0 + - &numpy numpy>=1.23,<2.1 - pandas>=2.0,<2.2.4dev0 run_pylibcudf: common: @@ -753,8 +753,8 @@ dependencies: - output_types: [conda, requirements, pyproject] packages: - cachetools - - &numba-cuda-dep numba-cuda>=0.2.0,<0.3.0a0 - - &numba-dep numba>=0.59.1,<0.61.0a0 + - &numba-cuda-dep numba-cuda>=0.4.0,<0.5.0a0 + - &numba-dep numba>=0.59.1,<0.62.0a0 - nvtx>=0.2.1 - packaging - rich @@ -885,7 +885,8 @@ dependencies: matrices: - matrix: {dependencies: "oldest"} packages: - - numba-cuda==0.2.0 + - numba-cuda==0.4.0 + - numba==0.59.1 - pandas==2.0.* - matrix: {dependencies: "latest"} packages: diff --git a/python/cudf/pyproject.toml b/python/cudf/pyproject.toml index 16cd97677ef..8b8abe90ac9 100644 --- a/python/cudf/pyproject.toml +++ b/python/cudf/pyproject.toml @@ -24,9 +24,9 @@ dependencies = [ "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", "libcudf==25.4.*,>=0.0.0a0", - "numba-cuda>=0.2.0,<0.3.0a0", - "numba>=0.59.1,<0.61.0a0", - "numpy>=1.23,<3.0a0", + "numba-cuda>=0.4.0,<0.5.0a0", + "numba>=0.59.1,<0.62.0a0", + "numpy>=1.23,<2.1", "nvtx>=0.2.1", "packaging", "pandas>=2.0,<2.2.4dev0", diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 872c08a66f9..9026a0c29ca 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ [project.optional-dependencies] test = [ - "numpy>=1.23,<3.0a0", + "numpy>=1.23,<2.1", "pytest-cov", "pytest-xdist", "pytest<8", diff --git a/python/dask_cudf/pyproject.toml b/python/dask_cudf/pyproject.toml index 87bf282f376..83493d7f2a4 100644 --- a/python/dask_cudf/pyproject.toml +++ b/python/dask_cudf/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "cudf==25.4.*,>=0.0.0a0", "cupy-cuda11x>=12.0.0", "fsspec>=0.6.0", - "numpy>=1.23,<3.0a0", + "numpy>=1.23,<2.1", "pandas>=2.0,<2.2.4dev0", "pynvml>=12.0.0,<13.0.0a0", "rapids-dask-dependency==25.4.*,>=0.0.0a0", @@ -47,8 +47,8 @@ cudf = "dask_cudf.backends:CudfBackendEntrypoint" [project.optional-dependencies] test = [ "dask-cuda==25.4.*,>=0.0.0a0", - "numba-cuda>=0.2.0,<0.3.0a0", - "numba>=0.59.1,<0.61.0a0", + "numba-cuda>=0.4.0,<0.5.0a0", + "numba>=0.59.1,<0.62.0a0", "pytest-cov", "pytest-xdist", "pytest<8", diff --git a/python/pylibcudf/pyproject.toml b/python/pylibcudf/pyproject.toml index 939da65c1ec..e12d1ffdb39 100644 --- a/python/pylibcudf/pyproject.toml +++ b/python/pylibcudf/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ test = [ "fastavro>=0.22.9", "hypothesis", - "numpy>=1.23,<3.0a0", + "numpy>=1.23,<2.1", "pandas", "pytest-cov", "pytest-xdist", From 8d6bdc34c4b2d0d6be614c04af16b8064d2c723d Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 25 Feb 2025 15:21:19 -0800 Subject: [PATCH 24/45] Remove static configure step (#18091) This check has been superseded by #17781. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - James Lamb (https://github.com/jameslamb) URL: https://github.com/rapidsai/cudf/pull/18091 --- .github/workflows/pr.yaml | 11 ----------- ci/configure_cpp_static.sh | 21 --------------------- 2 files changed, 32 deletions(-) delete mode 100755 ci/configure_cpp_static.sh diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 38b890893d0..2c583598f54 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -24,7 +24,6 @@ jobs: - conda-python-cudf-tests - conda-python-other-tests - conda-java-tests - - static-configure - conda-notebook-tests - docs-build - wheel-build-libcudf @@ -192,16 +191,6 @@ jobs: arch: "amd64" container_image: "rapidsai/ci-conda:latest" run_script: "ci/test_java.sh" - static-configure: - needs: checks - secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-25.04 - with: - build_type: pull-request - # Use the wheel container so we can skip conda solves and since our - # primary static consumers (Spark) are not in conda anyway. - container_image: "rapidsai/ci-wheel:latest" - run_script: "ci/configure_cpp_static.sh" conda-notebook-tests: needs: [conda-python-build, changed-files] secrets: inherit diff --git a/ci/configure_cpp_static.sh b/ci/configure_cpp_static.sh deleted file mode 100755 index 3d0647a96f6..00000000000 --- a/ci/configure_cpp_static.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# Copyright (c) 2024-2025, NVIDIA CORPORATION. - -set -euo pipefail - -source rapids-date-string - -rapids-logger "Configure static cpp build" - -ENV_YAML_DIR="$(mktemp -d)" -REQUIREMENTS_FILE="${ENV_YAML_DIR}/requirements.txt" - -rapids-dependency-file-generator \ - --output requirements \ - --file-key test_static_build \ - --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch)" | tee "${REQUIREMENTS_FILE}" - -rapids-pip-retry install -r "${REQUIREMENTS_FILE}" -pyenv rehash - -cmake -S cpp -B build_static -GNinja -DBUILD_SHARED_LIBS=OFF -DCUDF_USE_ARROW_STATIC=ON -DBUILD_TESTS=OFF From e365986cf886fe3a9531952fe5b91a34ca466c45 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Tue, 25 Feb 2025 17:32:23 -0600 Subject: [PATCH 25/45] Run narwhals tests nightly. (#18093) This enables narwhals tests in nightly CI. Authors: - Bradley Dice (https://github.com/bdice) Approvers: - Matthew Murray (https://github.com/Matt711) - Gil Forsyth (https://github.com/gforsyth) URL: https://github.com/rapidsai/cudf/pull/18093 --- .github/workflows/test.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 12f6d751493..7046fd0e5dc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -168,3 +168,14 @@ jobs: date: ${{ inputs.date }} sha: ${{ inputs.sha }} script: "ci/test_cudf_polars_polars_tests.sh" + narwhals-tests: + secrets: inherit + uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-25.04 + with: + build_type: ${{ inputs.build_type }} + branch: ${{ inputs.branch }} + date: ${{ inputs.date }} + sha: ${{ inputs.sha }} + node_type: "gpu-l4-latest-1" + container_image: "rapidsai/ci-conda:latest" + run_script: ci/test_narwhals.sh From 18a5412ced238630bb1a6f5b15e6f319dd388090 Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Tue, 25 Feb 2025 18:57:00 -0500 Subject: [PATCH 26/45] Add new nvtext::normalize_characters API (#17818) Adds new normalizer APIs as part of the rework for the subword-tokenizer. The new API is split into 2 parts. First a normalizer object is created with appropriate state: lower-case and special-tokens. The normalizing tables are currently hardcoded inside libcudf. Future versions of the this may load these tables from some other source. The 2nd API is given the input strings column and the normalizer object and returns a normalized strings column. The normalizer object can be reused on all subsequent `normalize_characters` calls. The current `nvtext::normalize_characters` loads the normalizing tables on each call which can be significant overhead. This API will be deprecated and replaced by these 2 new ones. Some utility functions from that implementation have been refactored to be used by both until the old one is removed. The first API creates the normalizer object. ```cpp std::unique_ptr create_character_normalizer( bool do_lower_case, cudf::strings_column_view const& special_tokens, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); ``` The 2nd API uses the normalizer on a strings column: ```cpp std::unique_ptr normalize_characters( cudf::strings_column_view const& input, character_normalizer const& normalizer, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); ``` Using the python interface: ```python import cudf from cudf.core.character_normalizer import CharacterNormalizer cn = CharacterNormalizer(do_lower=False) sn = cn.normalize(input_strings) ``` Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Tianyu Liu (https://github.com/kingcrimsontianyu) - Karthikeyan (https://github.com/karthikeyann) - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/17818 --- cpp/benchmarks/text/normalize.cpp | 9 +- cpp/include/cudf/strings/detail/utilities.hpp | 14 +- cpp/include/nvtext/normalize.hpp | 111 ++++- cpp/src/strings/utilities.cu | 14 +- cpp/src/text/normalize.cu | 395 +++++++++++++++++- cpp/src/text/normalize.cuh | 100 +++++ cpp/src/text/subword/data_normalizer.cu | 76 +--- cpp/tests/text/normalize_tests.cpp | 165 +++++++- python/cudf/cudf/core/character_normalizer.py | 46 ++ python/cudf/cudf/core/column/string.py | 28 +- .../cudf/cudf/tests/text/test_text_methods.py | 8 +- .../pylibcudf/libcudf/nvtext/normalize.pxd | 15 +- .../pylibcudf/pylibcudf/nvtext/normalize.pxd | 13 +- .../pylibcudf/pylibcudf/nvtext/normalize.pyi | 10 +- .../pylibcudf/pylibcudf/nvtext/normalize.pyx | 71 +++- .../pylibcudf/tests/test_nvtext_normalize.py | 97 ++++- 16 files changed, 1018 insertions(+), 154 deletions(-) create mode 100644 cpp/src/text/normalize.cuh create mode 100644 python/cudf/cudf/core/character_normalizer.py diff --git a/cpp/benchmarks/text/normalize.cpp b/cpp/benchmarks/text/normalize.cpp index 594dc0de28a..494d5722ae4 100644 --- a/cpp/benchmarks/text/normalize.cpp +++ b/cpp/benchmarks/text/normalize.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, NVIDIA CORPORATION. + * Copyright (c) 2021-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +48,11 @@ static void bench_normalize(nvbench::state& state) [&](nvbench::launch& launch) { auto result = nvtext::normalize_spaces(input); }); } else { bool const to_lower = (normalize_type == "to_lower"); + // we expect the normalizer to be created once and re-used + // so creating it is not measured + auto normalizer = nvtext::create_character_normalizer(to_lower); state.exec(nvbench::exec_tag::sync, [&](nvbench::launch& launch) { - auto result = nvtext::normalize_characters(input, to_lower); + auto result = nvtext::normalize_characters(input, *normalizer); }); } } @@ -57,6 +60,6 @@ static void bench_normalize(nvbench::state& state) NVBENCH_BENCH(bench_normalize) .set_name("normalize") .add_int64_axis("min_width", {0}) - .add_int64_axis("max_width", {32, 64, 128, 256}) + .add_int64_axis("max_width", {128, 256}) .add_int64_axis("num_rows", {32768, 262144, 2097152}) .add_string_axis("type", {"spaces", "characters", "to_lower"}); diff --git a/cpp/include/cudf/strings/detail/utilities.hpp b/cpp/include/cudf/strings/detail/utilities.hpp index d276c5df7dc..8fb1f30f961 100644 --- a/cpp/include/cudf/strings/detail/utilities.hpp +++ b/cpp/include/cudf/strings/detail/utilities.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -96,5 +96,17 @@ int64_t get_offset_value(cudf::column_view const& offsets, size_type index, rmm::cuda_stream_view stream); +/** + * @brief Return the first and last offset in the given strings column + * + * This accounts for sliced input columns as well. + * + * @param input Strings column + * @param stream CUDA stream used for device memory operations and kernel launches + * @return First and last offset values + */ +std::pair get_first_and_last_offset(cudf::strings_column_view const& input, + rmm::cuda_stream_view stream); + } // namespace strings::detail } // namespace CUDF_EXPORT cudf diff --git a/cpp/include/nvtext/normalize.hpp b/cpp/include/nvtext/normalize.hpp index 74325f4a406..70ee7891ad7 100644 --- a/cpp/include/nvtext/normalize.hpp +++ b/cpp/include/nvtext/normalize.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024, NVIDIA CORPORATION. + * Copyright (c) 2020-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ #pragma once #include +#include #include #include #include @@ -107,5 +108,113 @@ std::unique_ptr normalize_characters( rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); +/** + * @brief Normalizer object to be used with nvtext::normalize_characters + * + * Use nvtext::create_normalizer to create this object. + * + * This normalizer includes: + * + * - adding padding around punctuation (unicode category starts with "P") + * as well as certain ASCII symbols like "^" and "$" + * - adding padding around the [CJK Unicode block + * characters](https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block)) + * - changing whitespace (e.g. `"\t", "\n", "\r"`) to just space `" "` + * - removing control characters (unicode categories "Cc" and "Cf") + * + * The padding process adds a single space before and after the character. + * Details on _unicode category_ can be found here: + * https://unicodebook.readthedocs.io/unicode.html#categories + * + * If `do_lower_case = true`, lower-casing also removes any accents. The + * accents cannot be removed from upper-case characters without lower-casing + * and lower-casing cannot be performed without also removing accents. + * However, if the accented character is already lower-case, then only the + * accent is removed. + * + * If `special_tokens` are included the padding after `[` and before `]` is not + * inserted if the characters between them match one of the given tokens. + * Also, the `special_tokens` are expected to include the `[]` characters + * at the beginning of and end of each string appropriately. + */ +struct character_normalizer { + /** + * @brief Normalizer object constructor + * + * This initializes and holds the character normalizing tables and settings. + * + * @param do_lower_case If true, upper-case characters are converted to + * lower-case and accents are stripped from those characters. + * If false, accented and upper-case characters are not transformed. + * @param special_tokens Each row is a token including the `[]` brackets. + * For example: `[BOS]`, `[EOS]`, `[UNK]`, `[SEP]`, `[PAD]`, `[CLS]`, `[MASK]` + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the returned column's device memory + */ + character_normalizer(bool do_lower_case, + cudf::strings_column_view const& special_tokens, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + ~character_normalizer(); + + struct character_normalizer_impl; + std::unique_ptr _impl; +}; + +/** + * @brief Create a normalizer object + * + * Creates a normalizer object which can be reused on multiple calls to + * nvtext::normalize_characters + * + * @see nvtext::character_normalizer + * + * @param do_lower_case If true, upper-case characters are converted to + * lower-case and accents are stripped from those characters. + * If false, accented and upper-case characters are not transformed. + * @param special_tokens Individual tokens including `[]` brackets. + * Default is no special tokens. + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the returned column's device memory + * @return Object to be used with nvtext::normalize_characters + */ +std::unique_ptr create_character_normalizer( + bool do_lower_case, + cudf::strings_column_view const& special_tokens = cudf::strings_column_view(cudf::column_view{ + cudf::data_type{cudf::type_id::STRING}, 0, nullptr, nullptr, 0}), + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + +/** + * @brief Normalizes the text in input strings column + * + * @see nvtext::character_normalizer for details on the normalizer behavior + * + * @code{.pseudo} + * cn = create_character_normalizer(true) + * s = ["éâîô\teaio", "ĂĆĖÑÜ", "ACENU", "$24.08", "[a,bb]"] + * s1 = normalize_characters(s,cn) + * s1 is now ["eaio eaio", "acenu", "acenu", " $ 24 . 08", " [ a , bb ] "] + * + * cn = create_character_normalizer(false) + * s2 = normalize_characters(s,cn) + * s2 is now ["éâîô eaio", "ĂĆĖÑÜ", "ACENU", " $ 24 . 08", " [ a , bb ] "] + * @endcode + * + * A null input element at row `i` produces a corresponding null entry + * for row `i` in the output column. + * + * @param input The input strings to normalize + * @param normalizer Normalizer to use for this function + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Memory resource to allocate any returned objects + * @return Normalized strings column + */ +std::unique_ptr normalize_characters( + cudf::strings_column_view const& input, + character_normalizer const& normalizer, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + /** @} */ // end of group } // namespace CUDF_EXPORT nvtext diff --git a/cpp/src/strings/utilities.cu b/cpp/src/strings/utilities.cu index 45bd4615435..c5d46598d4a 100644 --- a/cpp/src/strings/utilities.cu +++ b/cpp/src/strings/utilities.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -180,6 +180,18 @@ int64_t get_offset_value(cudf::column_view const& offsets, : cudf::detail::get_value(offsets, index, stream); } +std::pair get_first_and_last_offset(cudf::strings_column_view const& input, + rmm::cuda_stream_view stream) +{ + if (input.is_empty()) { return {0L, 0L}; } + auto const first_offset = (input.offset() == 0) ? 0 + : cudf::strings::detail::get_offset_value( + input.offsets(), input.offset(), stream); + auto const last_offset = + cudf::strings::detail::get_offset_value(input.offsets(), input.size() + input.offset(), stream); + return {first_offset, last_offset}; +} + } // namespace detail rmm::device_uvector create_string_vector_from_column( diff --git a/cpp/src/text/normalize.cu b/cpp/src/text/normalize.cu index 7e2b766862d..0e680e98ec5 100644 --- a/cpp/src/text/normalize.cu +++ b/cpp/src/text/normalize.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024, NVIDIA CORPORATION. + * Copyright (c) 2020-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ +#include "text/normalize.cuh" #include "text/subword/detail/data_normalizer.hpp" #include "text/subword/detail/tokenizer_utils.cuh" #include "text/utilities/tokenize_ops.cuh" @@ -22,10 +23,11 @@ #include #include #include -#include #include #include #include +#include +#include #include #include #include @@ -38,9 +40,13 @@ #include +#include +#include +#include #include #include #include +#include #include #include @@ -103,6 +109,12 @@ constexpr uint32_t UTF8_1BYTE = 0x0080; constexpr uint32_t UTF8_2BYTE = 0x0800; constexpr uint32_t UTF8_3BYTE = 0x01'0000; +__device__ int8_t cp_to_utf8(uint32_t codepoint, char* out) +{ + auto utf8 = cudf::strings::detail::codepoint_to_utf8(codepoint); + return cudf::strings::detail::from_char_utf8(utf8, out); +} + /** * @brief Convert code-point arrays into UTF-8 bytes for each string. */ @@ -148,26 +160,8 @@ struct codepoint_to_utf8_fn { // convert each code-point to 1-4 UTF-8 encoded bytes char* out_ptr = d_chars + d_offsets[idx]; for (uint32_t jdx = 0; jdx < count; ++jdx) { - uint32_t code_point = *str_cps++; - if (code_point < UTF8_1BYTE) // ASCII range - *out_ptr++ = static_cast(code_point); - else if (code_point < UTF8_2BYTE) { // create two-byte UTF-8 - // b00001xxx:byyyyyyyy => b110xxxyy:b10yyyyyy - *out_ptr++ = static_cast((((code_point << 2) & 0x00'1F00) | 0x00'C000) >> 8); - *out_ptr++ = static_cast((code_point & 0x3F) | 0x0080); - } else if (code_point < UTF8_3BYTE) { // create three-byte UTF-8 - // bxxxxxxxx:byyyyyyyy => b1110xxxx:b10xxxxyy:b10yyyyyy - *out_ptr++ = static_cast((((code_point << 4) & 0x0F'0000) | 0x00E0'0000) >> 16); - *out_ptr++ = static_cast((((code_point << 2) & 0x00'3F00) | 0x00'8000) >> 8); - *out_ptr++ = static_cast((code_point & 0x3F) | 0x0080); - } else { // create four-byte UTF-8 - // maximum code-point value is 0x0011'0000 - // b000xxxxx:byyyyyyyy:bzzzzzzzz => b11110xxx:b10xxyyyy:b10yyyyzz:b10zzzzzz - *out_ptr++ = static_cast((((code_point << 6) & 0x0700'0000u) | 0xF000'0000u) >> 24); - *out_ptr++ = static_cast((((code_point << 4) & 0x003F'0000u) | 0x0080'0000u) >> 16); - *out_ptr++ = static_cast((((code_point << 2) & 0x00'3F00u) | 0x00'8000u) >> 8); - *out_ptr++ = static_cast((code_point & 0x3F) | 0x0080); - } + uint32_t codepoint = *str_cps++; + out_ptr += cp_to_utf8(codepoint, out_ptr); } } }; @@ -261,4 +255,361 @@ std::unique_ptr normalize_characters(cudf::strings_column_view con return detail::normalize_characters(input, do_lower_case, stream, mr); } +struct character_normalizer::character_normalizer_impl { + rmm::device_uvector cp_metadata; + rmm::device_uvector aux_table; + bool do_lower_case; + std::unique_ptr special_tokens; + rmm::device_uvector special_tokens_view; + + cudf::device_span get_special_tokens() const + { + return special_tokens_view; + } + + character_normalizer_impl(rmm::device_uvector&& cp_metadata, + rmm::device_uvector&& aux_table, + bool do_lower_case, + std::unique_ptr&& special_tokens, + rmm::device_uvector&& special_tokens_view) + : cp_metadata(std::move(cp_metadata)), + aux_table(std::move(aux_table)), + do_lower_case{do_lower_case}, + special_tokens{std::move(special_tokens)}, + special_tokens_view{std::move(special_tokens_view)} + { + } +}; + +character_normalizer::character_normalizer(bool do_lower_case, + cudf::strings_column_view const& special_tokens, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref) +{ + auto cp_metadata = nvtext::detail::get_codepoint_metadata(stream); + auto aux_table = nvtext::detail::get_aux_codepoint_data(stream); + CUDF_EXPECTS( + !special_tokens.has_nulls(), "special tokens should not have nulls", std::invalid_argument); + + auto sorted = std::move( + cudf::sort(cudf::table_view({special_tokens.parent()}), {}, {}, stream)->release().front()); + if (do_lower_case) { + // lower-case the tokens so they will match the normalized input + sorted = cudf::strings::to_lower(cudf::strings_column_view(sorted->view()), stream); + } + + auto tokens_view = cudf::strings::detail::create_string_vector_from_column( + cudf::strings_column_view(sorted->view()), stream, cudf::get_current_device_resource_ref()); + + _impl = std::make_unique(std::move(cp_metadata), + std::move(aux_table), + do_lower_case, + std::move(sorted), + std::move(tokens_view)); +} + +character_normalizer::~character_normalizer() {} + +std::unique_ptr create_character_normalizer( + bool do_lower_case, + cudf::strings_column_view const& special_tokens, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return std::make_unique(do_lower_case, special_tokens, stream, mr); +} + +namespace detail { +namespace { + +/** + * @brief Kernel handles fixing up the normalized data to account for any special tokens + * + * This undoes the padding added around the `[]` for patterns matching the strings in the + * special_tokens array. + * + * Launched as a thread per input byte (total_count). + * + * @param d_normalized The normalized set of UTF-8 characters; 3 uints per input byte + * @param total_count Number of bytes represented by d_normalized; len(d_normalized)/3 + * @param special_tokens Tokens to check against + */ +CUDF_KERNEL void special_tokens_kernel(uint32_t* d_normalized, + int64_t total_count, + cudf::device_span special_tokens) +{ + auto const idx = cudf::detail::grid_1d::global_thread_id(); + if (idx >= total_count) { return; } + auto const begin = d_normalized + (idx * MAX_NEW_CHARS) + 1; + if (*begin != '[') { return; } + auto const end = begin + cuda::std::min(6L, total_count - idx) * MAX_NEW_CHARS; + auto const match = thrust::find(thrust::seq, begin, end, static_cast(']')); + if (match == end) { return; } + char candidate[8]; + auto const ch_begin = + thrust::transform_iterator(begin, [](auto v) { return static_cast(v); }); + auto const ch_end = ch_begin + thrust::distance(begin, match + 1); + auto last = thrust::copy_if( + thrust::seq, ch_begin, ch_end, candidate, [](auto c) { return c != 0 && c != ' '; }); + *last = 0; // only needed for debug + + auto const size = static_cast(thrust::distance(candidate, last)); + auto const token = cudf::string_view(candidate, size); + // the binary_search expects the special_tokens to be sorted + if (!thrust::binary_search(thrust::seq, special_tokens.begin(), special_tokens.end(), token)) { + return; + } + + // fix up chars to remove the extra spaces + *(begin + 1) = 0; // removes space after '[' + *(match - 1) = 0; // removes space before ']' +} + +/** + * @brief The normalizer kernel + * + * Launched as a thread per input byte (total_bytes). + * + * Converts the input d_chars into codepoints to lookup in the provided tables. + * Once processed, the d_output contains 3 uints per input byte each encoded + * as output UTF-8. Any zero values are to removed by a subsequent kernel call. + * + * @param d_chars The characters for the input strings column to normalize + * @param total_bytes The number of bytes in the d_chars + * @param cp_metadata First lookup table for codepoint metadata + * @param aux_table Second lookup table containing possible replacement characters + * @param do_lower_case True if the normalization includes lower-casing characters + * @param d_output The output of the normalization (UTF-8 encoded) + */ +CUDF_KERNEL void data_normalizer_kernel(char const* d_chars, + int64_t total_bytes, + codepoint_metadata_type const* cp_metadata, + aux_codepoint_data_type const* aux_table, + bool do_lower_case, + uint32_t* d_output) +{ + uint32_t replacement[MAX_NEW_CHARS] = {0}; + + auto const idx = cudf::detail::grid_1d::global_thread_id(); + + if ((idx < total_bytes) && cudf::strings::detail::is_begin_utf8_char(d_chars[idx])) { + auto const cp = [utf8 = d_chars + idx] { + cudf::char_utf8 ch_utf8 = *utf8; + if (ch_utf8 > 0x7F) { cudf::strings::detail::to_char_utf8(utf8, ch_utf8); } + return cudf::strings::detail::utf8_to_codepoint(ch_utf8); + }(); + auto const metadata = cp_metadata[cp]; + + if (!should_remove_cp(metadata, do_lower_case)) { + int8_t num_new_chars = 1; + // retrieve the normalized value for cp + auto const new_cp = do_lower_case || always_replace(metadata) ? get_first_cp(metadata) : cp; + replacement[0] = new_cp == 0 ? cp : new_cp; + + if (do_lower_case && is_multi_char_transform(metadata)) { + auto const next_cps = aux_table[cp]; + replacement[1] = static_cast(next_cps >> 32); + replacement[2] = static_cast(next_cps & 0xFFFFFFFF); + num_new_chars = 2 + (replacement[2] != 0); + } + + if (should_add_spaces(metadata, do_lower_case) && (num_new_chars == 1)) { + replacement[1] = replacement[0]; + replacement[0] = SPACE_CODE_POINT; // add spaces around the new codepoint + replacement[2] = SPACE_CODE_POINT; + num_new_chars = 3; + } + + // convert codepoints back to UTF-8 in-place + for (int k = 0; k < num_new_chars; ++k) { + auto const new_cp = replacement[k]; + if (new_cp) { cp_to_utf8(new_cp, reinterpret_cast(replacement + k)); } + } + } + } + + // employ an optimized coalesced writer to output replacement as a block of transposed data + using block_store = + cub::BlockStore; + __shared__ typename block_store::TempStorage bs_stg; + auto block_base = d_output + blockIdx.x * blockDim.x * MAX_NEW_CHARS; + block_store(bs_stg).Store(block_base, replacement); +} + +/** + * @brief Computes the output sizes for each row + * + * The input offsets are used with segmented-reduce to count the number of + * non-zero values for each output row. + * + * @param d_normalized The UTF-8 encoded normalized values + * @param offsets These identify the row boundaries + * @param offset Only non-zero if the input column has been sliced + * @param size The number of output rows (sames as the number of input rows) + * @param stream Stream used for allocating device memory and launching kernels + * @return The sizes of each output row + */ +template +rmm::device_uvector compute_sizes(cudf::device_span d_normalized, + OffsetType offsets, + int64_t offset, + cudf::size_type size, + rmm::cuda_stream_view stream) +{ + auto output_sizes = rmm::device_uvector(size, stream); + + auto d_data = d_normalized.data(); + + // counts the non-zero bytes in the d_data array + auto d_in = cudf::detail::make_counting_transform_iterator( + 0, cuda::proclaim_return_type([d_data] __device__(auto idx) { + idx = idx * MAX_NEW_CHARS; + // transform function counts number of non-zero bytes in uint32_t value + auto tfn = [](uint32_t v) -> cudf::size_type { + return ((v & 0xFF) > 0) + ((v & 0xFF00) > 0) + ((v & 0xFF0000) > 0) + + ((v & 0xFF000000) > 0); + }; + auto const begin = d_data + idx; + auto const end = begin + MAX_NEW_CHARS; + return thrust::transform_reduce(thrust::seq, begin, end, tfn, 0, thrust::plus{}); + })); + + // DeviceSegmentedReduce is used to compute the size of each output row + auto d_out = output_sizes.begin(); + auto temp = std::size_t{0}; + if (offset == 0) { + cub::DeviceSegmentedReduce::Sum( + nullptr, temp, d_in, d_out, size, offsets, offsets + 1, stream.value()); + auto d_temp = rmm::device_buffer{temp, stream}; + cub::DeviceSegmentedReduce::Sum( + d_temp.data(), temp, d_in, d_out, size, offsets, offsets + 1, stream.value()); + } else { + // offsets need to be normalized for segmented-reduce to work efficiently + auto offsets_itr = thrust::transform_iterator( + offsets, + cuda::proclaim_return_type([offset] __device__(auto o) { return o - offset; })); + cub::DeviceSegmentedReduce::Sum( + nullptr, temp, d_in, d_out, size, offsets_itr, offsets_itr + 1, stream.value()); + auto d_temp = rmm::device_buffer{temp, stream}; + cub::DeviceSegmentedReduce::Sum( + d_temp.data(), temp, d_in, d_out, size, offsets_itr, offsets_itr + 1, stream.value()); + } + + return output_sizes; +} + +// handles ranges above int32 max +template +OutputIterator remove_copy_safe(InputIterator first, + InputIterator last, + OutputIterator result, + T const& value, + rmm::cuda_stream_view stream) +{ + auto const copy_size = std::min(static_cast(std::distance(first, last)), + static_cast(std::numeric_limits::max())); + + auto itr = first; + while (itr != last) { + auto const copy_end = + static_cast(std::distance(itr, last)) <= copy_size ? last : itr + copy_size; + result = thrust::remove_copy(rmm::exec_policy(stream), itr, copy_end, result, value); + itr = copy_end; + } + return result; +} + +// handles ranges above int32 max +template +Iterator remove_safe(Iterator first, Iterator last, T const& value, rmm::cuda_stream_view stream) +{ + auto const size = std::min(static_cast(std::distance(first, last)), + static_cast(std::numeric_limits::max())); + + auto result = first; + auto itr = first; + while (itr != last) { + auto end = static_cast(std::distance(itr, last)) <= size ? last : itr + size; + result = thrust::remove(rmm::exec_policy(stream), itr, end, value); + itr = end; + } + return result; +} +} // namespace + +std::unique_ptr normalize_characters(cudf::strings_column_view const& input, + character_normalizer const& normalizer, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + if (input.is_empty()) { return cudf::make_empty_column(cudf::data_type{cudf::type_id::STRING}); } + + auto [first_offset, last_offset] = + cudf::strings::detail::get_first_and_last_offset(input, stream); + auto const chars_size = last_offset - first_offset; + auto const d_input_chars = input.chars_begin(stream) + first_offset; + + if (chars_size == 0) { return std::make_unique(input.parent(), stream, mr); } + + constexpr int64_t block_size = 256; + cudf::detail::grid_1d grid{chars_size, block_size}; + auto const max_new_char_total = cudf::util::round_up_safe(chars_size, block_size) * MAX_NEW_CHARS; + + auto const& parameters = normalizer._impl; + + auto d_normalized = rmm::device_uvector(max_new_char_total, stream); + data_normalizer_kernel<<>>( + d_input_chars, + chars_size, + parameters->cp_metadata.data(), + parameters->aux_table.data(), + parameters->do_lower_case, + d_normalized.data()); + + // This removes space added around any special tokens in the form of [ttt]. + // An alternate approach is to do a multi-replace of '[ ttt ]' with '[ttt]' right + // before returning the output strings column. + auto const special_tokens = parameters->get_special_tokens(); + if (!special_tokens.empty()) { + special_tokens_kernel<<>>( + d_normalized.data(), chars_size, special_tokens); + } + + // Use segmented-reduce over the non-zero codepoints to get the size of the output rows + auto const input_offsets = + cudf::detail::offsetalator_factory::make_input_iterator(input.offsets(), input.offset()); + auto output_sizes = + compute_sizes(d_normalized, input_offsets, first_offset, input.size(), stream); + + // convert the sizes to offsets + auto [offsets, total_size] = cudf::strings::detail::make_offsets_child_column( + output_sizes.begin(), output_sizes.end(), stream, mr); + + // create output chars by calling remove_copy(0) on the bytes in d_normalized + auto chars = rmm::device_uvector(total_size, stream, mr); + auto const begin = reinterpret_cast(d_normalized.begin()); + // the remove() above speeds up the remove_copy() by roughly 10% + auto const end = + reinterpret_cast(remove_safe(d_normalized.begin(), d_normalized.end(), 0, stream)); + remove_copy_safe(begin, end, chars.data(), 0, stream); + + return cudf::make_strings_column(input.size(), + std::move(offsets), + chars.release(), + input.null_count(), + cudf::detail::copy_bitmask(input.parent(), stream, mr)); +} + +} // namespace detail + +std::unique_ptr normalize_characters(cudf::strings_column_view const& input, + character_normalizer const& normalizer, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_FUNC_RANGE(); + return detail::normalize_characters(input, normalizer, stream, mr); +} + } // namespace nvtext diff --git a/cpp/src/text/normalize.cuh b/cpp/src/text/normalize.cuh new file mode 100644 index 00000000000..3972726d536 --- /dev/null +++ b/cpp/src/text/normalize.cuh @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025, 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. + */ + +#pragma once + +#include "text/subword/detail/cp_data.h" + +namespace nvtext { +namespace detail { + +/** + * @brief Bit used to filter out invalid code points. + * + * When normalizing characters to code point values, if this bit is set, + * the code point should be filtered out before returning from the normalizer. + */ +constexpr uint32_t FILTER_BIT = 22; + +/** + * @brief Retrieve new code point from metadata value. + * + * @param metadata Value from the codepoint_metadata table. + * @return The replacement character if appropriate. + */ +__device__ constexpr uint32_t get_first_cp(uint32_t metadata) { return metadata & NEW_CP_MASK; } + +/** + * @brief Retrieve token category from the metadata value. + * + * Category values are 0-5: + * 0 - character should be padded + * 1 - pad character if lower-case + * 2 - character should be removed + * 3 - remove character if lower-case + * 4 - whitespace character -- always replace + * 5 - uncategorized + * + * @param metadata Value from the codepoint_metadata table. + * @return Category value. + */ +__device__ constexpr uint32_t extract_token_cat(uint32_t metadata) +{ + return (metadata >> TOKEN_CAT_SHIFT) & TOKEN_CAT_MASK; +} + +/** + * @brief Return true if category of metadata value specifies the character should be replaced. + */ +__device__ constexpr bool should_remove_cp(uint32_t metadata, bool lower_case) +{ + auto const cat = extract_token_cat(metadata); + return (cat == TOKEN_CAT_REMOVE_CHAR) || (lower_case && (cat == TOKEN_CAT_REMOVE_CHAR_IF_LOWER)); +} + +/** + * @brief Return true if category of metadata value specifies the character should be padded. + */ +__device__ constexpr bool should_add_spaces(uint32_t metadata, bool lower_case) +{ + auto const cat = extract_token_cat(metadata); + return (cat == TOKEN_CAT_ADD_SPACE) || (lower_case && (cat == TOKEN_CAT_ADD_SPACE_IF_LOWER)); +} + +/** + * @brief Return true if category of metadata value specifies the character should be replaced. + */ +__device__ constexpr bool always_replace(uint32_t metadata) +{ + return extract_token_cat(metadata) == TOKEN_CAT_ALWAYS_REPLACE; +} + +/** + * @brief Returns true if metadata value includes a multi-character transform bit equal to 1. + */ +__device__ constexpr bool is_multi_char_transform(uint32_t metadata) +{ + return (metadata >> MULTICHAR_SHIFT) & MULTICHAR_MASK; +} + +/** + * @brief Returns true if the byte passed in could be a valid head byte for + * a utf8 character. That is, not binary `10xxxxxx` + */ +__device__ constexpr bool is_head_byte(unsigned char utf8_byte) { return (utf8_byte >> 6) != 2; } + +} // namespace detail +} // namespace nvtext diff --git a/cpp/src/text/subword/data_normalizer.cu b/cpp/src/text/subword/data_normalizer.cu index 7a39199011e..4c54409c41a 100644 --- a/cpp/src/text/subword/data_normalizer.cu +++ b/cpp/src/text/subword/data_normalizer.cu @@ -14,6 +14,7 @@ * limitations under the License. */ +#include "text/normalize.cuh" #include "text/subword/detail/data_normalizer.hpp" #include "text/subword/detail/tokenizer_utils.cuh" @@ -38,81 +39,6 @@ namespace nvtext { namespace detail { namespace { -/** - * @brief Bit used to filter out invalid code points. - * - * When normalizing characters to code point values, if this bit is set, - * the code point should be filtered out before returning from the normalizer. - */ -constexpr uint32_t FILTER_BIT = 22; - -/** - * @brief Retrieve new code point from metadata value. - * - * @param metadata Value from the codepoint_metadata table. - * @return The replacement character if appropriate. - */ -__device__ uint32_t get_first_cp(uint32_t metadata) { return metadata & NEW_CP_MASK; } - -/** - * @brief Retrieve token category from the metadata value. - * - * Category values are 0-5: - * 0 - character should be padded - * 1 - pad character if lower-case - * 2 - character should be removed - * 3 - remove character if lower-case - * 4 - whitespace character -- always replace - * 5 - uncategorized - * - * @param metadata Value from the codepoint_metadata table. - * @return Category value. - */ -__device__ uint32_t extract_token_cat(uint32_t metadata) -{ - return (metadata >> TOKEN_CAT_SHIFT) & TOKEN_CAT_MASK; -} - -/** - * @brief Return true if category of metadata value specifies the character should be replaced. - */ -__device__ bool should_remove_cp(uint32_t metadata, bool lower_case) -{ - auto const cat = extract_token_cat(metadata); - return (cat == TOKEN_CAT_REMOVE_CHAR) || (lower_case && (cat == TOKEN_CAT_REMOVE_CHAR_IF_LOWER)); -} - -/** - * @brief Return true if category of metadata value specifies the character should be padded. - */ -__device__ bool should_add_spaces(uint32_t metadata, bool lower_case) -{ - auto const cat = extract_token_cat(metadata); - return (cat == TOKEN_CAT_ADD_SPACE) || (lower_case && (cat == TOKEN_CAT_ADD_SPACE_IF_LOWER)); -} - -/** - * @brief Return true if category of metadata value specifies the character should be replaced. - */ -__device__ bool always_replace(uint32_t metadata) -{ - return extract_token_cat(metadata) == TOKEN_CAT_ALWAYS_REPLACE; -} - -/** - * @brief Returns true if metadata value includes a multi-character transform bit equal to 1. - */ -__device__ bool is_multi_char_transform(uint32_t metadata) -{ - return (metadata >> MULTICHAR_SHIFT) & MULTICHAR_MASK; -} - -/** - * @brief Returns true if the byte passed in could be a valid head byte for - * a utf8 character. That is, not binary `10xxxxxx` - */ -__device__ bool is_head_byte(unsigned char utf8_byte) { return (utf8_byte >> 6) != 2; } - /** * @brief Converts a UTF-8 character into a unicode code point value. * diff --git a/cpp/tests/text/normalize_tests.cpp b/cpp/tests/text/normalize_tests.cpp index 2515cc917fa..530148eb654 100644 --- a/cpp/tests/text/normalize_tests.cpp +++ b/cpp/tests/text/normalize_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024, NVIDIA CORPORATION. + * Copyright (c) 2020-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,6 +74,10 @@ TEST_F(TextNormalizeTest, NormalizeEmptyTest) EXPECT_EQ(results->size(), 0); results = nvtext::normalize_characters(strings_view, false); EXPECT_EQ(results->size(), 0); + + auto normalizer = nvtext::create_character_normalizer(true); + results = nvtext::normalize_characters(strings_view, *normalizer); + EXPECT_EQ(results->size(), 0); } TEST_F(TextNormalizeTest, AllNullStrings) @@ -84,6 +88,10 @@ TEST_F(TextNormalizeTest, AllNullStrings) CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, strings); results = nvtext::normalize_characters(strings_view, false); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, strings); + + auto normalizer = nvtext::create_character_normalizer(true); + results = nvtext::normalize_characters(strings_view, *normalizer); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, strings); } TEST_F(TextNormalizeTest, SomeNullStrings) @@ -93,27 +101,21 @@ TEST_F(TextNormalizeTest, SomeNullStrings) auto results = nvtext::normalize_characters(strings_view, false); cudf::test::strings_column_wrapper expected({"", " . ", "a"}, {false, true, true}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + auto normalizer = nvtext::create_character_normalizer(true); + results = nvtext::normalize_characters(strings_view, *normalizer); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } TEST_F(TextNormalizeTest, NormalizeCharacters) { // These include punctuation, accents, whitespace, and CJK characters - std::vector h_strings{"abc£def", - nullptr, - "éè â îô\taeio", - "\tĂĆĖÑ Ü", - "ACEN U", - "P^NP", - "$41.07", - "[a,b]", - "丏丟", - ""}; - auto validity = - thrust::make_transform_iterator(h_strings.begin(), [](auto str) { return str != nullptr; }); - cudf::test::strings_column_wrapper strings(h_strings.begin(), h_strings.end(), validity); - cudf::strings_column_view strings_view(strings); + auto input = cudf::test::strings_column_wrapper( + {"abc£def", "", "éè â îô\taeio", "\tĂĆĖÑ Ü", "ACEN U", "P^NP", "$41.07", "[a,b]", "丏丟", ""}, + {1, 0, 1, 1, 1, 1, 1, 1, 1, 1}); + auto sv = cudf::strings_column_view(input); { - auto results = nvtext::normalize_characters(strings_view, true); + auto results = nvtext::normalize_characters(sv, true); cudf::test::strings_column_wrapper expected({"abc£def", "", "ee a io aeio", @@ -124,11 +126,11 @@ TEST_F(TextNormalizeTest, NormalizeCharacters) " [ a , b ] ", " 丏 丟 ", ""}, - validity); + {1, 0, 1, 1, 1, 1, 1, 1, 1, 1}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } { - auto results = nvtext::normalize_characters(strings_view, false); + auto results = nvtext::normalize_characters(sv, false); cudf::test::strings_column_wrapper expected({"abc£def", "", "éè â îô aeio", @@ -139,11 +141,117 @@ TEST_F(TextNormalizeTest, NormalizeCharacters) " [ a , b ] ", " 丏 丟 ", ""}, - validity); + {1, 0, 1, 1, 1, 1, 1, 1, 1, 1}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } } +TEST_F(TextNormalizeTest, WithNormalizer) +{ + auto long_row = + "this entry is intended to pad out past 256 bytes which is currently the block size"; + // the following include punctuation, accents, whitespace, and CJK characters + auto input = cudf::test::strings_column_wrapper({"abc£def", + "", + "éè â îô\taeio", + "\tĂĆĖÑ Ü", + "ACEN U", + "P^NP", + "$41.07", + "[a,b]", + "丏丟", + "", + long_row, + long_row, + long_row}, + {1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + + auto const sv = cudf::strings_column_view(input); + + auto normalizer = nvtext::create_character_normalizer(true); + auto results = nvtext::normalize_characters(sv, *normalizer); + auto expected = cudf::test::strings_column_wrapper({"abc£def", + "", + "ee a io aeio", + " acen u", + "acen u", + "p ^ np", + " $ 41 . 07", + " [ a , b ] ", + " 丏 丟 ", + "", + long_row, + long_row, + long_row}, + {1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = nvtext::normalize_characters(sv, *normalizer); // test normalizer re-use + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + normalizer = nvtext::create_character_normalizer(false); + results = nvtext::normalize_characters(sv, *normalizer); + expected = cudf::test::strings_column_wrapper({"abc£def", + "", + "éè â îô aeio", + " ĂĆĖÑ Ü", + "ACEN U", + "P ^ NP", + " $ 41 . 07", + " [ a , b ] ", + " 丏 丟 ", + "", + long_row, + long_row, + long_row}, + {1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = nvtext::normalize_characters(sv, *normalizer); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); +} + +TEST_F(TextNormalizeTest, SpecialTokens) +{ + auto long_row = + "this entry is intended to pad out past 256 bytes which is currently the block size"; + auto input = + cudf::test::strings_column_wrapper({"[BOS]Some strings with [PAD] special[SEP]tokens[EOS]", + "[bos]these should[sep]work too[eos]", + "some[non]tokens[eol]too", + long_row, + long_row, + long_row}); + + auto sv = cudf::strings_column_view(input); + auto special_tokens = cudf::test::strings_column_wrapper({"[BOS]", "[EOS]", "[SEP]", "[PAD]"}); + auto stv = cudf::strings_column_view(special_tokens); + + auto normalizer = nvtext::create_character_normalizer(true, stv); + auto results = nvtext::normalize_characters(sv, *normalizer); + auto expected = cudf::test::strings_column_wrapper( + {" [bos] some strings with [pad] special [sep] tokens [eos] ", + " [bos] these should [sep] work too [eos] ", + "some [ non ] tokens [ eol ] too", + long_row, + long_row, + long_row}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = nvtext::normalize_characters(sv, *normalizer); // and again + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + normalizer = nvtext::create_character_normalizer(false, stv); + results = nvtext::normalize_characters(sv, *normalizer); + expected = cudf::test::strings_column_wrapper( + {" [BOS] Some strings with [PAD] special [SEP] tokens [EOS] ", + " [ bos ] these should [ sep ] work too [ eos ] ", + "some [ non ] tokens [ eol ] too", + long_row, + long_row, + long_row}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + results = nvtext::normalize_characters(sv, *normalizer); // and again + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); +} + TEST_F(TextNormalizeTest, NormalizeSlicedColumn) { cudf::test::strings_column_wrapper strings( @@ -151,10 +259,21 @@ TEST_F(TextNormalizeTest, NormalizeSlicedColumn) std::vector sliced = cudf::split(strings, {4}); auto results = nvtext::normalize_characters(cudf::strings_column_view(sliced.front()), true); - cudf::test::strings_column_wrapper expected({"abc£def", "ee a io aeio", "acen u", "p ^ np"}); + auto expected = + cudf::test::strings_column_wrapper({"abc£def", "ee a io aeio", "acen u", "p ^ np"}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + results = nvtext::normalize_characters(cudf::strings_column_view(sliced[1]), false); + expected = cudf::test::strings_column_wrapper({" $ 41 . 07", " [ a , b ] ", " 丏 丟 "}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + auto normalizer = nvtext::create_character_normalizer(true); + results = nvtext::normalize_characters(cudf::strings_column_view(sliced.front()), *normalizer); + expected = cudf::test::strings_column_wrapper({"abc£def", "ee a io aeio", "acen u", "p ^ np"}); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); - results = nvtext::normalize_characters(cudf::strings_column_view(sliced[1]), false); - cudf::test::strings_column_wrapper expected2({" $ 41 . 07", " [ a , b ] ", " 丏 丟 "}); - CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected2); + normalizer = nvtext::create_character_normalizer(false); + results = nvtext::normalize_characters(cudf::strings_column_view(sliced[1]), *normalizer); + expected = cudf::test::strings_column_wrapper({" $ 41 . 07", " [ a , b ] ", " 丏 丟 "}); + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); } diff --git a/python/cudf/cudf/core/character_normalizer.py b/python/cudf/cudf/core/character_normalizer.py new file mode 100644 index 00000000000..1240c0e1eb7 --- /dev/null +++ b/python/cudf/cudf/core/character_normalizer.py @@ -0,0 +1,46 @@ +# Copyright (c) 2025, NVIDIA CORPORATION. + +from __future__ import annotations + +import pylibcudf as plc + +import cudf + + +class CharacterNormalizer: + """ + A normalizer object used to normalize input text. + + Parameters + ---------- + do_lower : bool + If True, the normalizer should also lower-case + while normalizing. + special_tokens : cudf.Series + Series of special tokens. + """ + + def __init__( + self, + do_lower: bool, + special_tokens: cudf.Series = cudf.Series([], dtype="object"), + ) -> None: + self.normalizer = plc.nvtext.normalize.CharacterNormalizer( + do_lower, special_tokens._column.to_pylibcudf(mode="read") + ) + + def normalize(self, text: cudf.Series) -> cudf.Series: + """ + Parameters + ---------- + text : cudf.Series + The strings to be normalized. + + Returns + ------- + cudf.Series + Normalized strings + """ + result = text._column.normalize_characters(self.normalizer) + + return cudf.Series._from_column(result) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 04a72017c33..c0ad33ec7d6 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -4679,8 +4679,10 @@ def normalize_characters(self, do_lower: bool = True) -> SeriesOrIndex: r""" Normalizes strings characters for tokenizing. - This uses the normalizer that is built into the - subword_tokenize function which includes: + .. deprecated:: 25.04 + Use `CharacterNormalizer` instead. + + The normalizer function includes: - adding padding around punctuation (unicode category starts with "P") as well as certain ASCII symbols like "^" and "$" @@ -4720,8 +4722,13 @@ def normalize_characters(self, do_lower: bool = True) -> SeriesOrIndex: 2 $ 99 dtype: object """ + warnings.warn( + "normalize_characters is deprecated and will be removed in a future " + "version. Use CharacterNormalizer instead.", + FutureWarning, + ) return self._return_or_inplace( - self._column.normalize_characters(do_lower) + self._column.characters_normalize(do_lower) ) def tokenize(self, delimiter: str = " ") -> SeriesOrIndex: @@ -6256,14 +6263,25 @@ def normalize_spaces(self) -> Self: ) @acquire_spill_lock() - def normalize_characters(self, do_lower: bool = True) -> Self: + def characters_normalize(self, do_lower: bool = True) -> Self: return ColumnBase.from_pylibcudf( # type: ignore[return-value] - plc.nvtext.normalize.normalize_characters( + plc.nvtext.normalize.characters_normalize( self.to_pylibcudf(mode="read"), do_lower, ) ) + @acquire_spill_lock() + def normalize_characters( + self, normalizer: plc.nvtext.normalize.CharacterNormalizer + ) -> Self: + return ColumnBase.from_pylibcudf( # type: ignore[return-value] + plc.nvtext.normalize.normalize_characters( + self.to_pylibcudf(mode="read"), + normalizer, + ) + ) + @acquire_spill_lock() def replace_tokens( self, targets: Self, replacements: Self, delimiter: plc.Scalar diff --git a/python/cudf/cudf/tests/text/test_text_methods.py b/python/cudf/cudf/tests/text/test_text_methods.py index 86e1e46c1a2..dc45827d2e8 100644 --- a/python/cudf/cudf/tests/text/test_text_methods.py +++ b/python/cudf/cudf/tests/text/test_text_methods.py @@ -8,6 +8,7 @@ import cudf from cudf.core.byte_pair_encoding import BytePairEncoder +from cudf.core.character_normalizer import CharacterNormalizer from cudf.core.tokenize_vocabulary import TokenizeVocabulary from cudf.testing import assert_eq @@ -251,7 +252,8 @@ def test_normalize_characters(): ] ) - actual = strings.str.normalize_characters() + normalizer_lower = CharacterNormalizer(True) + actual = normalizer_lower.normalize(strings.str) assert type(expected) is type(actual) assert_eq(expected, actual) @@ -265,7 +267,9 @@ def test_normalize_characters(): "Stock ^ $ 1", ] ) - actual = strings.str.normalize_characters(do_lower=False) + + normalizer = CharacterNormalizer(False) + actual = normalizer.normalize(strings.str) assert type(expected) is type(actual) assert_eq(expected, actual) diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd index f8b082c8429..2cf2bfb8ac9 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/normalize.pxd @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. from libcpp cimport bool from libcpp.memory cimport unique_ptr from pylibcudf.exception_handler cimport libcudf_exception_handler @@ -16,3 +16,16 @@ cdef extern from "nvtext/normalize.hpp" namespace "nvtext" nogil: const column_view & strings, bool do_lower_case ) except +libcudf_exception_handler + + cdef struct character_normalizer "nvtext::character_normalizer": + pass + + cdef unique_ptr[character_normalizer] create_character_normalizer( + bool do_lower_case, + const column_view & strings + ) except +libcudf_exception_handler + + cdef unique_ptr[column] normalize_characters( + const column_view & strings, + const character_normalizer & normalizer + ) except +libcudf_exception_handler diff --git a/python/pylibcudf/pylibcudf/nvtext/normalize.pxd b/python/pylibcudf/pylibcudf/nvtext/normalize.pxd index 90676145afa..e6688e19762 100644 --- a/python/pylibcudf/pylibcudf/nvtext/normalize.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/normalize.pxd @@ -1,9 +1,18 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. from libcpp cimport bool +from libcpp.memory cimport unique_ptr from pylibcudf.column cimport Column +from pylibcudf.libcudf.nvtext.normalize cimport character_normalizer +cdef class CharacterNormalizer: + cdef unique_ptr[character_normalizer] c_obj cpdef Column normalize_spaces(Column input) -cpdef Column normalize_characters(Column input, bool do_lower_case) +cpdef Column characters_normalize(Column input, bool do_lower_case) + +cpdef Column normalize_characters( + Column input, + CharacterNormalizer normalizer +) diff --git a/python/pylibcudf/pylibcudf/nvtext/normalize.pyi b/python/pylibcudf/pylibcudf/nvtext/normalize.pyi index 1d90a5a8960..d722ef6c79e 100644 --- a/python/pylibcudf/pylibcudf/nvtext/normalize.pyi +++ b/python/pylibcudf/pylibcudf/nvtext/normalize.pyi @@ -1,6 +1,12 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. from pylibcudf.column import Column +class CharacterNormalizer: + def __init__(self, do_lower_case: bool, special_tokens: Column): ... + def normalize_spaces(input: Column) -> Column: ... -def normalize_characters(input: Column, do_lower_case: bool) -> Column: ... +def characters_normalize(input: Column, do_lower_case: bool) -> Column: ... +def normalize_characters( + input: Column, normalizer: CharacterNormalizer +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/normalize.pyx b/python/pylibcudf/pylibcudf/nvtext/normalize.pyx index b259ccaefa6..6a18c205841 100644 --- a/python/pylibcudf/pylibcudf/nvtext/normalize.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/normalize.pyx @@ -1,16 +1,37 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. +from cython.operator cimport dereference from libcpp cimport bool from libcpp.memory cimport unique_ptr from libcpp.utility cimport move from pylibcudf.column cimport Column from pylibcudf.libcudf.column.column cimport column -from pylibcudf.libcudf.nvtext.normalize cimport ( - normalize_characters as cpp_normalize_characters, - normalize_spaces as cpp_normalize_spaces, -) +from pylibcudf.libcudf.column.column_view cimport column_view +from pylibcudf.libcudf.nvtext cimport normalize as cpp_normalize -__all__ = ["normalize_characters", "normalize_spaces"] +__all__ = [ + "CharacterNormalizer" + "normalize_characters", + "normalize_spaces", + "characters_normalize" +] + +cdef class CharacterNormalizer: + """The normalizer object to be used with ``normalize_characters``. + + For details, see :cpp:class:`cudf::nvtext::character_normalizer`. + """ + def __cinit__(self, bool do_lower_case, Column tokens): + cdef column_view c_tokens = tokens.view() + with nogil: + self.c_obj = move( + cpp_normalize.create_character_normalizer( + do_lower_case, + c_tokens + ) + ) + + __hash__ = None cpdef Column normalize_spaces(Column input): """ @@ -32,12 +53,12 @@ cpdef Column normalize_spaces(Column input): cdef unique_ptr[column] c_result with nogil: - c_result = cpp_normalize_spaces(input.view()) + c_result = cpp_normalize.normalize_spaces(input.view()) return Column.from_libcudf(move(c_result)) -cpdef Column normalize_characters(Column input, bool do_lower_case): +cpdef Column characters_normalize(Column input, bool do_lower_case): """ Normalizes strings characters for tokenizing. @@ -60,6 +81,38 @@ cpdef Column normalize_characters(Column input, bool do_lower_case): cdef unique_ptr[column] c_result with nogil: - c_result = cpp_normalize_characters(input.view(), do_lower_case) + c_result = cpp_normalize.normalize_characters( + input.view(), + do_lower_case + ) + + return Column.from_libcudf(move(c_result)) + + +cpdef Column normalize_characters(Column input, CharacterNormalizer normalizer): + """ + Normalizes strings characters for tokenizing. + + For details, see :cpp:func:`normalize_characters` + + Parameters + ---------- + input : Column + Input strings + normalizer : CharacterNormalizer + Normalizer object used for modifying the input column text + + Returns + ------- + Column + Normalized strings column + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_normalize.normalize_characters( + input.view(), + dereference(normalizer.c_obj.get()) + ) return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py index 25b6d1389ec..47bbb191be6 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_normalize.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. import pyarrow as pa import pytest @@ -15,7 +15,7 @@ def norm_spaces_input_data(): @pytest.fixture(scope="module") def norm_chars_input_data(): - arr = ["éâîô\teaio", "ĂĆĖÑÜ", "ACENU", "$24.08", "[a,bb]"] + arr = ["éâîô\teaio", "ĂĆĖÑÜ", "ACENU", "$24.08", "[a,bb]", "[pad]"] return pa.array(arr) @@ -29,15 +29,98 @@ def test_normalize_spaces(norm_spaces_input_data): @pytest.mark.parametrize("do_lower", [True, False]) def test_normalize_characters(norm_chars_input_data, do_lower): - result = plc.nvtext.normalize.normalize_characters( + result = plc.nvtext.normalize.characters_normalize( plc.interop.from_arrow(norm_chars_input_data), do_lower, ) - expected = pa.array( - ["eaio eaio", "acenu", "acenu", " $ 24 . 08", " [ a , bb ] "] + if do_lower: + expected = pa.array( + [ + "eaio eaio", + "acenu", + "acenu", + " $ 24 . 08", + " [ a , bb ] ", + " [ pad ] ", + ] + ) + else: + expected = pa.array( + [ + "éâîô eaio", + "ĂĆĖÑÜ", + "ACENU", + " $ 24 . 08", + " [ a , bb ] ", + " [ pad ] ", + ] + ) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize("do_lower", [True, False]) +def test_normalizer(norm_chars_input_data, do_lower): + result = plc.nvtext.normalize.normalize_characters( + plc.interop.from_arrow(norm_chars_input_data), + plc.nvtext.normalize.CharacterNormalizer( + do_lower, + plc.column_factories.make_empty_column(plc.types.TypeId.STRING), + ), + ) + if do_lower: + expected = pa.array( + [ + "eaio eaio", + "acenu", + "acenu", + " $ 24 . 08", + " [ a , bb ] ", + " [ pad ] ", + ] + ) + else: + expected = pa.array( + [ + "éâîô eaio", + "ĂĆĖÑÜ", + "ACENU", + " $ 24 . 08", + " [ a , bb ] ", + " [ pad ] ", + ] + ) + assert_column_eq(result, expected) + + +@pytest.mark.parametrize("do_lower", [True, False]) +def test_normalizer_with_special_tokens(norm_chars_input_data, do_lower): + special_tokens = pa.array(["[pad]"]) + result = plc.nvtext.normalize.normalize_characters( + plc.interop.from_arrow(norm_chars_input_data), + plc.nvtext.normalize.CharacterNormalizer( + do_lower, plc.interop.from_arrow(special_tokens) + ), ) - if not do_lower: + if do_lower: + expected = pa.array( + [ + "eaio eaio", + "acenu", + "acenu", + " $ 24 . 08", + " [ a , bb ] ", + " [pad] ", + ] + ) + else: expected = pa.array( - ["éâîô eaio", "ĂĆĖÑÜ", "ACENU", " $ 24 . 08", " [ a , bb ] "] + [ + "éâîô eaio", + "ĂĆĖÑÜ", + "ACENU", + " $ 24 . 08", + " [ a , bb ] ", + " [pad] ", + ] ) assert_column_eq(result, expected) From 5eb552754020bed652f3f278a6b5cc494eeb9bce Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Tue, 25 Feb 2025 16:15:40 -0800 Subject: [PATCH 27/45] Remove unused var (#18096) The `cython_lib_dir` was removed as part of the switch to publishing a libcudf wheel. Authors: - Vyas Ramasubramani (https://github.com/vyasr) Approvers: - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/18096 --- python/cudf/CMakeLists.txt | 4 ---- python/cudf_kafka/CMakeLists.txt | 4 ---- python/pylibcudf/CMakeLists.txt | 4 ---- 3 files changed, 12 deletions(-) diff --git a/python/cudf/CMakeLists.txt b/python/cudf/CMakeLists.txt index 2a17bc5dbb7..090e475471d 100644 --- a/python/cudf/CMakeLists.txt +++ b/python/cudf/CMakeLists.txt @@ -37,7 +37,3 @@ rapids_cython_init() add_subdirectory(cudf/_lib) add_subdirectory(udf_cpp) - -if(DEFINED cython_lib_dir) - rapids_cython_add_rpath_entries(TARGET cudf PATHS "${cython_lib_dir}") -endif() diff --git a/python/cudf_kafka/CMakeLists.txt b/python/cudf_kafka/CMakeLists.txt index 3e12eb6aa41..13b859bc33b 100644 --- a/python/cudf_kafka/CMakeLists.txt +++ b/python/cudf_kafka/CMakeLists.txt @@ -35,7 +35,3 @@ include(rapids-cython-core) rapids_cython_init() add_subdirectory(cudf_kafka/_lib) - -if(DEFINED cython_lib_dir) - rapids_cython_add_rpath_entries(TARGET cudf_kafka PATHS "${cython_lib_dir}") -endif() diff --git a/python/pylibcudf/CMakeLists.txt b/python/pylibcudf/CMakeLists.txt index fe6e73a3f14..153570a4a7e 100644 --- a/python/pylibcudf/CMakeLists.txt +++ b/python/pylibcudf/CMakeLists.txt @@ -37,7 +37,3 @@ include(rapids-cython-core) rapids_cython_init() add_subdirectory(pylibcudf) - -if(DEFINED cython_lib_dir) - rapids_cython_add_rpath_entries(TARGET cudf PATHS "${cython_lib_dir}") -endif() From d8b3d801ec4830102242db1fa60a88e1a0bb7299 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:58:14 -0800 Subject: [PATCH 28/45] Fix scatter_by_map with spilling enabled (#18095) closes https://github.com/rapidsai/cudf/issues/18088 Before the old Cython bindings of `columns_split` spill locked the conversion from libcudf to a cudf Python column. When I replaced these bindings, this spill locking was removed during the refactor. I'm spot checking that other APIs are not affected. If so I can open PRs for those Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Matthew Murray (https://github.com/Matt711) URL: https://github.com/rapidsai/cudf/pull/18095 --- python/cudf/cudf/core/indexed_frame.py | 6 +++++- python/cudf/cudf/tests/test_spilling.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 9c48b31a309..211d161696e 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -3308,9 +3308,13 @@ def _split(self, splits, keep_index: bool = True) -> list[Self]: splits, ) + @acquire_spill_lock() + def split_from_pylibcudf(split: list[plc.Column]) -> list[ColumnBase]: + return [ColumnBase.from_pylibcudf(col) for col in split] + return [ self._from_columns_like_self( - [ColumnBase.from_pylibcudf(col) for col in split], + split_from_pylibcudf(split), self._column_names, self.index.names if keep_index else None, ) diff --git a/python/cudf/cudf/tests/test_spilling.py b/python/cudf/cudf/tests/test_spilling.py index 13d98e43ddc..08226dd7f6d 100644 --- a/python/cudf/cudf/tests/test_spilling.py +++ b/python/cudf/cudf/tests/test_spilling.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022-2024, NVIDIA CORPORATION. +# Copyright (c) 2022-2025, NVIDIA CORPORATION. from __future__ import annotations import contextlib @@ -784,3 +784,12 @@ def test_spilling_and_copy_on_write(manager: SpillManager): assert not a.is_spilled assert a.owner.exposed assert not b.owner.exposed + + +def test_scatter_by_map(): + data = range(10) + with cudf.option_context("spill", True): + df = cudf.DataFrame(data) + result = df.scatter_by_map(data) + for i, res in zip(data, result): + assert_eq(res, cudf.DataFrame([i], index=[i])) From 46b9799ea55b899e08f6b758ec90e9742a72d159 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Tue, 25 Feb 2025 19:14:57 -0600 Subject: [PATCH 29/45] Fix `test_scan_csv_multi` cudf-polars test (#18064) The current implementation of `test_scan_csv_multi` does not work if the compute task is run on distinct worker processes (because it changes directory in lieu of using a proper file path). Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Peter Andreas Entschev (https://github.com/pentschev) - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/18064 --- python/cudf_polars/tests/test_scan.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/python/cudf_polars/tests/test_scan.py b/python/cudf_polars/tests/test_scan.py index 9c58a24c065..8ff0db084b1 100644 --- a/python/cudf_polars/tests/test_scan.py +++ b/python/cudf_polars/tests/test_scan.py @@ -1,9 +1,7 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations -import os - import pytest import polars as pl @@ -203,8 +201,11 @@ def test_scan_csv_multi(tmp_path, filename, glob, nrows_skiprows): f.write("""foo,bar,baz\n1,2,3\n3,4,5""") with (tmp_path / "test*.csv").open("w") as f: f.write("""foo,bar,baz\n1,2,3\n3,4,5""") - os.chdir(tmp_path) - q = pl.scan_csv(filename, glob=glob, n_rows=n_rows, skip_rows=skiprows) + if isinstance(filename, list): + source = [tmp_path / fn for fn in filename] + else: + source = tmp_path / filename + q = pl.scan_csv(source, glob=glob, n_rows=n_rows, skip_rows=skiprows) assert_gpu_result_equal(q) From 72d5792c79f11c90f43c6991dd54e082b3c0ad98 Mon Sep 17 00:00:00 2001 From: "Richard (Rick) Zamora" Date: Wed, 26 Feb 2025 08:14:26 -0600 Subject: [PATCH 30/45] Relax inconsistent schema handling in `dask_cudf.read_parquet` (#17554) Addresses an issue raised offline by @praateekmahajan Authors: - Richard (Rick) Zamora (https://github.com/rjzamora) Approvers: - Tom Augspurger (https://github.com/TomAugspurger) - Mads R. B. Kristensen (https://github.com/madsbk) URL: https://github.com/rapidsai/cudf/pull/17554 --- .../dask_cudf/dask_cudf/_legacy/io/parquet.py | 18 +++---- .../dask_cudf/io/tests/test_parquet.py | 48 ++++++++++++++++++- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/python/dask_cudf/dask_cudf/_legacy/io/parquet.py b/python/dask_cudf/dask_cudf/_legacy/io/parquet.py index c0792663c7e..c0b9d71653c 100644 --- a/python/dask_cudf/dask_cudf/_legacy/io/parquet.py +++ b/python/dask_cudf/dask_cudf/_legacy/io/parquet.py @@ -434,18 +434,12 @@ def set_object_dtypes_from_pa_schema(df, schema): # pyarrow schema. if schema: for col_name, col in df._data.items(): - if col_name is None: - # Pyarrow cannot handle `None` as a field name. - # However, this should be a simple range index that - # we can ignore anyway - continue - typ = cudf_dtype_from_pa_type(schema.field(col_name).type) - if ( - col_name in schema.names - and not isinstance(typ, (cudf.ListDtype, cudf.StructDtype)) - and isinstance(col, cudf.core.column.StringColumn) - ): - df._data[col_name] = col.astype(typ) + if col_name in schema.names: + typ = cudf_dtype_from_pa_type(schema.field(col_name).type) + if not isinstance( + typ, (cudf.ListDtype, cudf.StructDtype) + ) and isinstance(col, cudf.core.column.StringColumn): + df._data[col_name] = col.astype(typ) to_parquet = dd.to_parquet diff --git a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py index 9f7031f4d2a..3a88668e6d2 100644 --- a/python/dask_cudf/dask_cudf/io/tests/test_parquet.py +++ b/python/dask_cudf/dask_cudf/io/tests/test_parquet.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd +import pyarrow as pa import pytest import dask @@ -486,6 +487,52 @@ def test_create_metadata_file_inconsistent_schema(tmpdir): dd.assert_eq(ddf1.compute(), ddf2.compute()) +@pytest.mark.parametrize("specify_schema", [True, False]) +def test_read_inconsistent_schema(tmpdir, specify_schema): + if specify_schema: + # If we specify the expected schema, + # we also need to specify the partitioning. + kwargs = { + "dataset": { + "schema": pa.schema( + [ + ("id", pa.int64()), + ("text", pa.string()), + ("meta1", pa.struct([("field1", pa.string())])), + ] + ), + "partitioning": None, + }, + } + else: + kwargs = {} + + records = [ + {"id": 123, "text": "foo"}, + { + "text": "bar", + "meta1": [{"field1": "cat"}], + "id": 456, + }, + ] + columns = ["text", "id"] + pd.DataFrame(records[:1]).to_parquet(tmpdir / "part.0.parquet") + pd.DataFrame(records[1:]).to_parquet(tmpdir / "part.1.parquet") + # Check that cuDF and Dask cuDF match + dd.assert_eq( + cudf.read_parquet( + tmpdir, columns=columns, allow_mismatched_pq_schemas=True + ), + dask_cudf.read_parquet(tmpdir, columns=columns, **kwargs), + check_index=False, + ) + # Check that "pandas" and "cudf" backends match + dd.assert_eq( + dd.read_parquet(tmpdir, columns=columns), + dask_cudf.read_parquet(tmpdir, columns=columns, **kwargs), + ) + + @pytest.mark.parametrize( "data", [ @@ -526,7 +573,6 @@ def test_cudf_list_struct_write(tmpdir): def test_null_partition(tmpdir): - import pyarrow as pa from pyarrow.dataset import HivePartitioning ids = pd.Series([0, 1, None], dtype="Int64") From e5d866bc68c4762ebd6e3e888e4abeaf4ccd9302 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 26 Feb 2025 07:01:27 -0800 Subject: [PATCH 31/45] Short circuit Index.equal if compared Index isn't same type (#18067) closes https://github.com/rapidsai/cudf/issues/8689 Before, comparing two different Index subclasses would execute a GPU kernel when we know they wouldn't be equal (e.g. DatetimeIndex equals RangeIndex). This PR add a short circuit clause to check that we are comparing the same subclasses. Also ensures we don't return a `np.bool_` object from this result. Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - GALI PREM SAGAR (https://github.com/galipremsagar) URL: https://github.com/rapidsai/cudf/pull/18067 --- python/cudf/cudf/core/column/column.py | 2 +- python/cudf/cudf/core/index.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index 06dc4058115..67a0aa7a781 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -713,7 +713,7 @@ def all(self, skipna: bool = True) -> bool: # is empty. if self.null_count == self.size: return True - return self.reduce("all") + return bool(self.reduce("all")) def any(self, skipna: bool = True) -> bool: # Early exit for fast cases. diff --git a/python/cudf/cudf/core/index.py b/python/cudf/cudf/core/index.py index 1730a692dc1..f4e5f6e96ae 100644 --- a/python/cudf/cudf/core/index.py +++ b/python/cudf/cudf/core/index.py @@ -1286,6 +1286,15 @@ def equals(self, other) -> bool: elif other_is_categorical and not self_is_categorical: self = self.astype(other.dtype) check_dtypes = True + elif ( + not self_is_categorical + and not other_is_categorical + and not isinstance(other, RangeIndex) + and not isinstance(self, type(other)) + ): + # Can compare Index to CategoricalIndex or RangeIndex + # Other comparisons are invalid + return False try: return self._column.equals( From 1a8d6368405fac3c5e55592fef2d9259081b045c Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:00:27 -0800 Subject: [PATCH 32/45] Enforce deprecation of dtype parameter in sum/product (#18070) xref https://github.com/rapidsai/cudf/pull/16313 Deprecated in 24.08 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/18070 --- python/cudf/cudf/core/column/column.py | 20 +++++--------------- python/cudf/cudf/core/column/timedelta.py | 3 +-- python/cudf/cudf/core/indexed_frame.py | 8 -------- python/cudf/cudf/tests/test_reductions.py | 8 -------- 4 files changed, 6 insertions(+), 33 deletions(-) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index 67a0aa7a781..b57d1f03981 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -2,7 +2,6 @@ from __future__ import annotations -import warnings from collections import abc from collections.abc import MutableSequence, Sequence from functools import cached_property @@ -1946,8 +1945,7 @@ def _reduce( skipna=skipna, min_count=min_count ) if isinstance(preprocessed, ColumnBase): - dtype = kwargs.pop("dtype", None) - return preprocessed.reduce(op, dtype, **kwargs) + return preprocessed.reduce(op, **kwargs) return preprocessed def _can_return_nan(self, skipna: bool | None = None) -> bool: @@ -2110,16 +2108,8 @@ def scan(self, scan_op: str, inclusive: bool, **kwargs) -> Self: ) ) - def reduce(self, reduction_op: str, dtype=None, **kwargs) -> ScalarLike: - if dtype is not None: - warnings.warn( - "dtype is deprecated and will be remove in a future release. " - "Cast the result (e.g. .astype) after the operation instead.", - FutureWarning, - ) - col_dtype = dtype - else: - col_dtype = self._reduction_result_dtype(reduction_op) + def reduce(self, reduction_op: str, **kwargs) -> ScalarLike: + col_dtype = self._reduction_result_dtype(reduction_op) # check empty case if len(self) <= self.null_count: @@ -2148,7 +2138,7 @@ def reduce(self, reduction_op: str, dtype=None, **kwargs) -> ScalarLike: }: scale = -plc_scalar.type().scale() # https://docs.microsoft.com/en-us/sql/t-sql/data-types/precision-scale-and-length-transact-sql - p = col_dtype.precision + p = col_dtype.precision # type: ignore[union-attr] nrows = len(self) if reduction_op in {"min", "max"}: new_p = p @@ -2162,7 +2152,7 @@ def reduce(self, reduction_op: str, dtype=None, **kwargs) -> ScalarLike: raise NotImplementedError( f"{reduction_op} not implemented for decimal types." ) - precision = max(min(new_p, col_dtype.MAX_PRECISION), 0) + precision = max(min(new_p, col_dtype.MAX_PRECISION), 0) # type: ignore[union-attr] new_dtype = type(col_dtype)(precision, scale) result_col = result_col.astype(new_dtype) elif isinstance(col_dtype, IntervalDtype): diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index 1cbbac0f8cc..8b0ef9f0cc8 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -452,14 +452,13 @@ def sum( self, skipna: bool | None = None, min_count: int = 0, - dtype: Dtype | None = None, ) -> pd.Timedelta: return pd.Timedelta( # Since sum isn't overridden in Numerical[Base]Column, mypy only # sees the signature from Reducible (which doesn't have the extra # parameters from ColumnBase._reduce) so we have to ignore this. self.astype(np.dtype(np.int64)).sum( # type: ignore - skipna=skipna, min_count=min_count, dtype=dtype + skipna=skipna, min_count=min_count ), unit=self.time_unit, ).as_unit(self.time_unit) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 211d161696e..9d426ad6bf7 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -1328,7 +1328,6 @@ def sum( self, axis=no_default, skipna=True, - dtype=None, numeric_only=False, min_count=0, **kwargs, @@ -1342,8 +1341,6 @@ def sum( Axis for the function to be applied on. skipna: bool, default True Exclude NA/null values when computing the result. - dtype: data type - Data type to cast the result to. numeric_only : bool, default False If True, includes only float, int, boolean columns. If False, will raise error in-case there are @@ -1373,7 +1370,6 @@ def sum( "sum", axis=axis, skipna=skipna, - dtype=dtype, numeric_only=numeric_only, min_count=min_count, **kwargs, @@ -1384,7 +1380,6 @@ def product( self, axis=no_default, skipna=True, - dtype=None, numeric_only=False, min_count=0, **kwargs, @@ -1398,8 +1393,6 @@ def product( Axis for the function to be applied on. skipna: bool, default True Exclude NA/null values when computing the result. - dtype: data type - Data type to cast the result to. numeric_only : bool, default False If True, includes only float, int, boolean columns. If False, will raise error in-case there are @@ -1432,7 +1425,6 @@ def product( "prod" if axis in {1, "columns"} else "product", axis=axis, skipna=skipna, - dtype=dtype, numeric_only=numeric_only, min_count=min_count, **kwargs, diff --git a/python/cudf/cudf/tests/test_reductions.py b/python/cudf/cudf/tests/test_reductions.py index 80ffce9e8be..75e38b9246a 100644 --- a/python/cudf/cudf/tests/test_reductions.py +++ b/python/cudf/cudf/tests/test_reductions.py @@ -512,14 +512,6 @@ def test_reduction_column_multiindex(): assert_eq(result, expected) -@pytest.mark.parametrize("op", ["sum", "product"]) -def test_dtype_deprecated(op): - ser = cudf.Series(range(5)) - with pytest.warns(FutureWarning): - result = getattr(ser, op)(dtype=np.dtype(np.int8)) - assert isinstance(result, np.int8) - - @pytest.mark.parametrize( "columns", [pd.RangeIndex(2), pd.Index([0, 1], dtype="int8")] ) From 54e740af7a08b99cca84f4f668886031a2c36e71 Mon Sep 17 00:00:00 2001 From: MithunR Date: Wed, 26 Feb 2025 11:12:10 -0800 Subject: [PATCH 33/45] Remove static column vectors from window function tests. (#18099) Fixes #18079. This commit fixes the failures reported in #18079, where the use of static column vector objects in the tests causes the use of a CUDA runtime context before it's been initialized, causing the tests to fail with: ``` parallel_for failed: cudaErrorInvalidResourceHandle: invalid resource handle ``` The solution is to switch the static column vectors to runtime, as a member of the test utility class `rolling_runner`. Authors: - MithunR (https://github.com/mythrocks) Approvers: - Bradley Dice (https://github.com/bdice) - David Wendt (https://github.com/davidwendt) URL: https://github.com/rapidsai/cudf/pull/18099 --- cpp/tests/rolling/offset_row_window_test.cpp | 28 +++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/cpp/tests/rolling/offset_row_window_test.cpp b/cpp/tests/rolling/offset_row_window_test.cpp index dcaa47e722b..4477ca388df 100644 --- a/cpp/tests/rolling/offset_row_window_test.cpp +++ b/cpp/tests/rolling/offset_row_window_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, NVIDIA CORPORATION. + * Copyright (c) 2021-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,18 +43,21 @@ auto constexpr null = int32_t{0}; // NULL representation for int32_t; auto no_nulls_list() { return nulls_at({}); } struct OffsetRowWindowTest : public cudf::test::BaseFixture { - static ints_column const _keys; // {0, 0, 0, 0, 0, 0, 1, 1, 1, 1}; - static ints_column const _values; // {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - struct rolling_runner { cudf::window_bounds _preceding, _following; cudf::size_type _min_periods; bool _grouped = true; + ints_column const _keys; // {0, 0, 0, 0, 0, 0, 1, 1, 1, 1}; + ints_column const _values; // {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; rolling_runner(cudf::window_bounds const& preceding, cudf::window_bounds const& following, cudf::size_type min_periods_ = 1) - : _preceding{preceding}, _following{following}, _min_periods{min_periods_} + : _preceding{preceding}, + _following{following}, + _min_periods{min_periods_}, + _keys{0, 0, 0, 0, 0, 0, 1, 1, 1, 1}, + _values{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} { } @@ -80,9 +83,6 @@ struct OffsetRowWindowTest : public cudf::test::BaseFixture { }; }; -ints_column const OffsetRowWindowTest::_keys{0, 0, 0, 0, 0, 0, 1, 1, 1, 1}; -ints_column const OffsetRowWindowTest::_values{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - auto const AGG_COUNT_NON_NULL = cudf::make_count_aggregation(cudf::null_policy::EXCLUDE); auto const AGG_COUNT_ALL = @@ -96,7 +96,8 @@ TEST_F(OffsetRowWindowTest, OffsetRowWindow_Grouped_3_to_Minus_1) { auto const preceding = cudf::window_bounds::get(3); auto const following = cudf::window_bounds::get(-1); - auto run_rolling = rolling_runner{preceding, following}.min_periods(1).grouped(true); + auto run_rolling = rolling_runner{preceding, following}; + run_rolling.min_periods(1).grouped(true); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*run_rolling(*AGG_COUNT_NON_NULL), ints_column{{0, 1, 2, 2, 2, 2, 0, 1, 2, 2}, nulls_at({0, 6})}); @@ -136,7 +137,8 @@ TEST_F(OffsetRowWindowTest, OffsetRowWindow_Ungrouped_3_to_Minus_1) { auto const preceding = cudf::window_bounds::get(3); auto const following = cudf::window_bounds::get(-1); - auto run_rolling = rolling_runner{preceding, following}.min_periods(1).grouped(false); + auto run_rolling = rolling_runner{preceding, following}; + run_rolling.min_periods(1).grouped(false); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*run_rolling(*AGG_COUNT_NON_NULL), ints_column{{0, 1, 2, 2, 2, 2, 2, 2, 2, 2}, nulls_at({0})}); @@ -176,7 +178,8 @@ TEST_F(OffsetRowWindowTest, OffsetRowWindow_Grouped_0_to_2) { auto const preceding = cudf::window_bounds::get(0); auto const following = cudf::window_bounds::get(2); - auto run_rolling = rolling_runner{preceding, following}.min_periods(1).grouped(true); + auto run_rolling = rolling_runner{preceding, following}; + run_rolling.min_periods(1).grouped(true); CUDF_TEST_EXPECT_COLUMNS_EQUAL( *run_rolling(*AGG_COUNT_NON_NULL), @@ -219,7 +222,8 @@ TEST_F(OffsetRowWindowTest, OffsetRowWindow_Ungrouped_0_to_2) { auto const preceding = cudf::window_bounds::get(0); auto const following = cudf::window_bounds::get(2); - auto run_rolling = rolling_runner{preceding, following}.min_periods(1).grouped(false); + auto run_rolling = rolling_runner{preceding, following}; + run_rolling.min_periods(1).grouped(false); CUDF_TEST_EXPECT_COLUMNS_EQUAL(*run_rolling(*AGG_COUNT_NON_NULL), ints_column{{2, 2, 2, 2, 2, 2, 2, 2, 1, null}, nulls_at({9})}); From 79d0b75a5327f72cdc14297885257a8979bdf0f2 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:01:22 -0800 Subject: [PATCH 34/45] Align StringColumn constructor with ColumnBase base class (#18086) With this PR, the constructors of all subclasses of `ColumnBase` are aligned. This will allow us to, in the future, more easily align on an interface for a public `ColumnBase` that other libraries can use to extend cudf. Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/18086 --- python/cudf/cudf/core/column/column.py | 9 +++++---- python/cudf/cudf/core/column/string.py | 24 ++++++++++++++---------- python/cudf/cudf/tests/test_string.py | 10 +++++++++- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index b57d1f03981..89ac39b2be5 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -2312,13 +2312,14 @@ def build_column( offset=offset, null_count=null_count, ) - elif dtype.type in (np.object_, np.str_): + elif dtype == CUDF_STRING_DTYPE: return cudf.core.column.StringColumn( - data=data, - mask=mask, + data=data, # type: ignore[arg-type] size=size, + dtype=dtype, + mask=mask, offset=offset, - children=children, + children=children, # type: ignore[arg-type] null_count=null_count, ) elif isinstance(dtype, ListDtype): diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index c0ad33ec7d6..28e8b98edfe 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -21,7 +21,7 @@ import cudf.core.column.datetime as datetime from cudf.api.types import is_integer, is_scalar, is_string_dtype from cudf.core._internals import binaryop -from cudf.core.buffer import acquire_spill_lock +from cudf.core.buffer import Buffer, acquire_spill_lock from cudf.core.column.column import ColumnBase from cudf.core.column.methods import ColumnMethods from cudf.core.scalar import pa_scalar_to_plc_scalar @@ -46,7 +46,6 @@ ScalarLike, SeriesOrIndex, ) - from cudf.core.buffer import Buffer from cudf.core.column.lists import ListColumn from cudf.core.column.numerical import NumericalColumn @@ -5595,13 +5594,14 @@ class StringColumn(column.ColumnBase): Parameters ---------- + data : Buffer + Buffer of the string data mask : Buffer The validity mask offset : int Data offset children : Tuple[Column] - Two non-null columns containing the string data and offsets - respectively + Columns containing the offsets """ _start_offset: int | None @@ -5629,14 +5629,20 @@ class StringColumn(column.ColumnBase): def __init__( self, - data: Buffer | None = None, + data: Buffer, + size: int | None, + dtype: np.dtype, mask: Buffer | None = None, - size: int | None = None, # TODO: make non-optional offset: int = 0, null_count: int | None = None, - children: tuple["column.ColumnBase", ...] = (), + children: tuple[column.ColumnBase] = (), # type: ignore[assignment] ): - dtype = cudf.api.types.dtype("object") + if not isinstance(data, Buffer): + raise ValueError("data must be a Buffer") + if dtype != CUDF_STRING_DTYPE: + raise ValueError(f"dtypy must be {CUDF_STRING_DTYPE}") + if len(children) > 1: + raise ValueError("StringColumn must have at most 1 offset column.") if size is None: for child in children: @@ -5731,8 +5737,6 @@ def base_size(self) -> int: # override for string column @property def data(self): - if self.base_data is None: - return None if self._data is None: if ( self.offset == 0 diff --git a/python/cudf/cudf/tests/test_string.py b/python/cudf/cudf/tests/test_string.py index 164fcb06624..18aee0001c4 100644 --- a/python/cudf/cudf/tests/test_string.py +++ b/python/cudf/cudf/tests/test_string.py @@ -13,8 +13,11 @@ import pyarrow as pa import pytest +import rmm + import cudf from cudf import concat +from cudf.core.buffer import as_buffer from cudf.core.column.string import StringColumn from cudf.core.index import Index from cudf.testing import assert_eq @@ -1202,7 +1205,12 @@ def test_string_misc_name(ps_gs, name): def test_string_no_children_properties(): - empty_col = StringColumn(children=()) + empty_col = StringColumn( + as_buffer(rmm.DeviceBuffer(size=0)), + size=0, + dtype=np.dtype("object"), + children=(), + ) assert empty_col.base_children == () assert empty_col.base_size == 0 From aa7f436bdc22fb5b25903252c437e32fbc8b33c0 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:55:25 -0800 Subject: [PATCH 35/45] Allow pivot_table to accept single label index and column arguments (#18115) closes https://github.com/rapidsai/cudf/issues/12410 closes https://github.com/rapidsai/cudf/issues/12409 The fix just mirrors the pandas logic. Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/18115 --- python/cudf/cudf/core/reshape.py | 20 +++++++++----------- python/cudf/cudf/tests/test_reshape.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/python/cudf/cudf/core/reshape.py b/python/cudf/cudf/core/reshape.py index c5d2fd349e9..7d76907916f 100644 --- a/python/cudf/cudf/core/reshape.py +++ b/python/cudf/cudf/core/reshape.py @@ -1526,9 +1526,9 @@ def pivot_table( ---------- data : DataFrame values : column name or list of column names to aggregate, optional - index : list of column names + index : scalar or list of column names Values to group by in the rows. - columns : list of column names + columns : scalar or list of column names Values to group by in the columns. aggfunc : str or dict, default "mean" If dict is passed, the key is column to aggregate @@ -1562,6 +1562,11 @@ def pivot_table( if sort is not True: raise NotImplementedError("sort is not supported yet") + if is_scalar(index): + index = [index] + if is_scalar(columns): + columns = [columns] + keys = index + columns values_passed = values is not None @@ -1620,15 +1625,8 @@ def pivot_table( table = table.fillna(fill_value) # discard the top level - if values_passed and not values_multi and table._data.multiindex: - column_names = table._data.level_names[1:] - table_columns = tuple( - map(lambda column: column[1:], table._column_names) - ) - table.columns = pd.MultiIndex.from_tuples( - tuples=table_columns, names=column_names - ) - + if values_passed and not values_multi and table._data.nlevels > 1: + table.columns = table._data.to_pandas_index.droplevel(0) if len(index) == 0 and len(columns) > 0: table = table.T diff --git a/python/cudf/cudf/tests/test_reshape.py b/python/cudf/cudf/tests/test_reshape.py index 7fbe072dde7..eae73e47955 100644 --- a/python/cudf/cudf/tests/test_reshape.py +++ b/python/cudf/cudf/tests/test_reshape.py @@ -798,6 +798,25 @@ def test_dataframe_pivot_table_simple(aggfunc, fill_value): assert_eq(expected, actual, check_dtype=False) +@pytest.mark.parametrize("index", ["A", ["A"]]) +@pytest.mark.parametrize("columns", ["C", ["C"]]) +def test_pivot_table_scalar_index_columns(index, columns): + data = { + "A": ["one", "one", "two", "three"] * 6, + "B": ["A", "B", "C"] * 8, + "C": ["foo", "foo", "foo", "bar", "bar", "bar"] * 4, + "D": range(24), + "E": range(24), + } + result = cudf.DataFrame(data).pivot_table( + values="D", index=index, columns=columns, aggfunc="sum" + ) + expected = pd.DataFrame(data).pivot_table( + values="D", index=index, columns=columns, aggfunc="sum" + ) + assert_eq(result, expected) + + def test_crosstab_simple(): a = np.array( [ From 7713bc1e8a339644815421b442abd6f91e04e15b Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 26 Feb 2025 19:06:55 -0800 Subject: [PATCH 36/45] Simplify DecimalDtype and DecimalColumn operations (#18111) Broken off (the non-breaking parts) from https://github.com/rapidsai/cudf/pull/18035/ as that PR will probably not move forward since it would require a pyarrow minimum version bump to 19 Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/18111 --- docs/cudf/source/conf.py | 1 + python/cudf/cudf/core/column/decimal.py | 30 +++++----------- python/cudf/cudf/core/column/timedelta.py | 4 ++- python/cudf/cudf/core/dtypes.py | 43 ++++++++++++----------- python/cudf/cudf/core/scalar.py | 6 ++-- 5 files changed, 37 insertions(+), 47 deletions(-) diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index c74da8d0ca9..8eea644363b 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -585,6 +585,7 @@ def on_missing_reference(app, env, node, contnode): ("py:class", "pd.DataFrame"), ("py:class", "pandas.core.indexes.frozen.FrozenList"), ("py:class", "pa.Array"), + ("py:class", "pa.Decimal128Type"), ("py:class", "ScalarLike"), ("py:class", "ParentType"), ("py:class", "pyarrow.lib.DataType"), diff --git a/python/cudf/cudf/core/column/decimal.py b/python/cudf/cudf/core/column/decimal.py index 3c603c8e6ef..8db6f805bce 100644 --- a/python/cudf/cudf/core/column/decimal.py +++ b/python/cudf/cudf/core/column/decimal.py @@ -13,7 +13,6 @@ import pylibcudf as plc import cudf -from cudf.api.types import is_scalar from cudf.core._internals import binaryop from cudf.core.buffer import acquire_spill_lock, as_buffer from cudf.core.column.column import ColumnBase @@ -73,11 +72,8 @@ def __cuda_array_interface__(self): def as_decimal_column( self, dtype: Dtype, - ) -> "DecimalBaseColumn": - if ( - isinstance(dtype, cudf.core.dtypes.DecimalDtype) - and dtype.scale < self.dtype.scale - ): + ) -> DecimalBaseColumn: + if isinstance(dtype, DecimalDtype) and dtype.scale < self.dtype.scale: warnings.warn( "cuDF truncates when downcasting decimals to a lower scale. " "To round, use Series.round() or DataFrame.round()." @@ -204,22 +200,17 @@ def normalize_binop_value(self, other) -> Self | cudf.Scalar: other = other.astype(self.dtype) return other if isinstance(other, cudf.Scalar) and isinstance( - # TODO: Should it be possible to cast scalars of other numerical - # types to decimal? other.dtype, - cudf.core.dtypes.DecimalDtype, + DecimalDtype, ): + # TODO: Should it be possible to cast scalars of other numerical + # types to decimal? if _same_precision_and_scale(self.dtype, other.dtype): other = other.astype(self.dtype) return other - elif is_scalar(other) and isinstance(other, (int, Decimal)): - other = Decimal(other) - metadata = other.as_tuple() - precision = max(len(metadata.digits), metadata.exponent) - scale = -cast(int, metadata.exponent) - return cudf.Scalar( - other, dtype=self.dtype.__class__(precision, scale) - ) + elif isinstance(other, (int, Decimal)): + dtype = self.dtype._from_decimal(Decimal(other)) + return cudf.Scalar(other, dtype=dtype) return NotImplemented def as_numerical_column( @@ -373,11 +364,6 @@ def __init__( children=children, ) - def __setitem__(self, key, value): - if isinstance(value, np.integer): - value = int(value) - super().__setitem__(key, value) - @classmethod def from_arrow(cls, data: pa.Array): dtype = Decimal64Dtype.from_arrow(data.type) diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index 8b0ef9f0cc8..d02681d389d 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -309,7 +309,9 @@ def total_seconds(self) -> ColumnBase: # https://github.com/rapidsai/cudf/issues/17664 return ( (self.astype(np.dtype(np.int64)) * conversion) - .astype(cudf.Decimal128Dtype(38, 9)) + .astype( + cudf.Decimal128Dtype(cudf.Decimal128Dtype.MAX_PRECISION, 9) + ) .round(decimals=abs(int(math.log10(conversion)))) .astype(np.dtype(np.float64)) ) diff --git a/python/cudf/cudf/core/dtypes.py b/python/cudf/cudf/core/dtypes.py index 977208f5eb4..ac9c4d23cc2 100644 --- a/python/cudf/cudf/core/dtypes.py +++ b/python/cudf/cudf/core/dtypes.py @@ -776,35 +776,36 @@ def _recursively_replace_fields(self, result: dict) -> dict: class DecimalDtype(_BaseDtype): _metadata = ("precision", "scale") - def __init__(self, precision, scale=0): + def __init__(self, precision: int, scale: int = 0) -> None: self._validate(precision, scale) - self._typ = pa.decimal128(precision, scale) + self._precision = precision + self._scale = scale @property - def str(self): + def str(self) -> str: return f"{self.name!s}({self.precision}, {self.scale})" @property - def precision(self): + def precision(self) -> int: """ The decimal precision, in number of decimal digits (an integer). """ - return self._typ.precision + return self._precision @precision.setter - def precision(self, value): + def precision(self, value: int) -> None: self._validate(value, self.scale) - self._typ = pa.decimal128(precision=value, scale=self.scale) + self._precision = value @property - def scale(self): + def scale(self) -> int: """ The decimal scale (an integer). """ - return self._typ.scale + return self._scale @property - def itemsize(self): + def itemsize(self) -> int: """ Length of one column element in bytes. """ @@ -815,14 +816,14 @@ def type(self): # might need to account for precision and scale here return decimal.Decimal - def to_arrow(self): + def to_arrow(self) -> pa.Decimal128Type: """ Return the equivalent ``pyarrow`` dtype. """ - return self._typ + return pa.decimal128(self.precision, self.scale) @classmethod - def from_arrow(cls, typ): + def from_arrow(cls, typ: pa.Decimal128Type) -> Self: """ Construct a cudf decimal dtype from a ``pyarrow`` dtype @@ -856,23 +857,23 @@ def __repr__(self): ) @classmethod - def _validate(cls, precision, scale=0): + def _validate(cls, precision: int, scale: int) -> None: if precision > cls.MAX_PRECISION: raise ValueError( f"Cannot construct a {cls.__name__}" f" with precision > {cls.MAX_PRECISION}" ) if abs(scale) > precision: - raise ValueError(f"scale={scale} exceeds precision={precision}") + raise ValueError(f"{scale=} cannot exceed {precision=}") @classmethod - def _from_decimal(cls, decimal): + def _from_decimal(cls, decimal: decimal.Decimal) -> Self: """ Create a cudf.DecimalDtype from a decimal.Decimal object """ metadata = decimal.as_tuple() - precision = max(len(metadata.digits), -metadata.exponent) - return cls(precision, -metadata.exponent) + precision = max(len(metadata.digits), -metadata.exponent) # type: ignore[operator] + return cls(precision, -metadata.exponent) # type: ignore[operator] def serialize(self) -> tuple[dict, list]: return ( @@ -885,7 +886,7 @@ def serialize(self) -> tuple[dict, list]: ) @classmethod - def deserialize(cls, header: dict, frames: list): + def deserialize(cls, header: dict, frames: list) -> Self: _check_type(cls, header, frames, is_valid_class=issubclass) return cls(header["precision"], header["scale"]) @@ -896,8 +897,8 @@ def __eq__(self, other: Dtype) -> bool: return False return self.precision == other.precision and self.scale == other.scale - def __hash__(self): - return hash(self._typ) + def __hash__(self) -> int: + return hash(self.to_arrow()) @doc_apply( diff --git a/python/cudf/cudf/core/scalar.py b/python/cudf/cudf/core/scalar.py index cf85282cccb..29139768a36 100644 --- a/python/cudf/cudf/core/scalar.py +++ b/python/cudf/cudf/core/scalar.py @@ -85,9 +85,9 @@ def _preprocess_host_value(value, dtype) -> tuple[ScalarLike, Dtype]: return value.as_py(), dtype if isinstance(dtype, cudf.core.dtypes.DecimalDtype): - value = pa.scalar( - value, type=pa.decimal128(dtype.precision, dtype.scale) - ).as_py() + if isinstance(value, np.integer): + value = int(value) + value = pa.scalar(value, type=dtype.to_arrow()).as_py() if isinstance(value, decimal.Decimal) and dtype is None: dtype = cudf.Decimal128Dtype._from_decimal(value) From 601d0a10c853ef837c948e536a8b5a11f4cd26ab Mon Sep 17 00:00:00 2001 From: GALI PREM SAGAR Date: Wed, 26 Feb 2025 21:34:11 -0600 Subject: [PATCH 37/45] Add `as_proxy_object` API to `cudf.pandas` (#18072) This is a public API to proxify true `pandas` or `cudf` objects. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Bradley Dice (https://github.com/bdice) - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/18072 --- python/cudf/cudf/pandas/__init__.py | 7 +- python/cudf/cudf/pandas/fast_slow_proxy.py | 27 +++++- .../cudf_pandas_tests/test_cudf_pandas.py | 88 +++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/pandas/__init__.py b/python/cudf/cudf/pandas/__init__.py index 52fc945709e..742a6b57e59 100644 --- a/python/cudf/cudf/pandas/__init__.py +++ b/python/cudf/cudf/pandas/__init__.py @@ -8,12 +8,17 @@ import pylibcudf import rmm.mr -from .fast_slow_proxy import is_proxy_instance, is_proxy_object +from .fast_slow_proxy import ( + as_proxy_object, + is_proxy_instance, + is_proxy_object, +) from .magics import load_ipython_extension from .profiler import Profiler __all__ = [ "Profiler", + "as_proxy_object", "install", "is_proxy_instance", "is_proxy_object", diff --git a/python/cudf/cudf/pandas/fast_slow_proxy.py b/python/cudf/cudf/pandas/fast_slow_proxy.py index 45944452c17..147971e8bee 100644 --- a/python/cudf/cudf/pandas/fast_slow_proxy.py +++ b/python/cudf/cudf/pandas/fast_slow_proxy.py @@ -151,7 +151,7 @@ def make_final_proxy_type( additional_attributes Mapping of additional attributes to add to the class (optional), these will override any defaulted attributes (e.g. - ``__init__`). If you want to remove a defaulted attribute + ``__init__``). If you want to remove a defaulted attribute completely, pass the special sentinel ``_DELETE`` as a value. postprocess Optional function called to allow the proxy to postprocess @@ -1335,6 +1335,31 @@ def _get_proxy_base_class(cls): return object +def as_proxy_object(obj: Any) -> Any: + """ + Wraps a cudf or pandas object in a proxy object if applicable. + + There will be no memory transfer, i.e., GPU objects stay on GPU and + CPU objects stay on CPU. The object will be wrapped in a + proxy object. This is useful for ensuring that the object is + compatible with the fast-slow proxy system. + + Parameters + ---------- + obj : Any + The object to wrap. + + Returns + ------- + Any + The wrapped proxy object if applicable, otherwise the original object. + """ + if _is_final_type(obj): + typ = get_final_type_map()[type(obj)] + return typ._fsproxy_wrap(obj, None) + return obj + + def is_proxy_instance(obj, type): return is_proxy_object(obj) and obj.__class__.__name__ == type.__name__ diff --git a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py index 47de8fb1435..d3bfd9298c2 100644 --- a/python/cudf/cudf_pandas_tests/test_cudf_pandas.py +++ b/python/cudf/cudf_pandas_tests/test_cudf_pandas.py @@ -44,6 +44,7 @@ OOMFallbackError, TypeFallbackError, _Unusable, + as_proxy_object, is_proxy_object, ) from cudf.testing import assert_eq @@ -1979,6 +1980,93 @@ def test_numpy_data_access(): assert type(expected) is type(actual) +@pytest.mark.parametrize( + "obj", + [ + pd.DataFrame({"a": [1, 2, 3]}), + pd.Series([1, 2, 3]), + pd.Index([1, 2, 3]), + pd.Categorical([1, 2, 3]), + pd.to_datetime(["2021-01-01", "2021-01-02"]), + pd.to_timedelta(["1 days", "2 days"]), + xpd.DataFrame({"a": [1, 2, 3]}), + xpd.Series([1, 2, 3]), + xpd.Index([1, 2, 3]), + xpd.Categorical([1, 2, 3]), + xpd.to_datetime(["2021-01-01", "2021-01-02"]), + xpd.to_timedelta(["1 days", "2 days"]), + cudf.DataFrame({"a": [1, 2, 3]}), + cudf.Series([1, 2, 3]), + cudf.Index([1, 2, 3]), + cudf.Index([1, 2, 3], dtype="category"), + cudf.to_datetime(["2021-01-01", "2021-01-02"]), + cudf.Index([1, 2, 3], dtype="timedelta64[ns]"), + [1, 2, 3], + {"a": 1, "b": 2}, + (1, 2, 3), + ], +) +def test_as_proxy_object(obj): + proxy_obj = as_proxy_object(obj) + if isinstance( + obj, + ( + pd.DataFrame, + pd.Series, + pd.Index, + pd.Categorical, + xpd.DataFrame, + xpd.Series, + xpd.Index, + xpd.Categorical, + cudf.DataFrame, + cudf.Series, + cudf.Index, + ), + ): + assert is_proxy_object(proxy_obj) + if isinstance(proxy_obj, xpd.DataFrame): + tm.assert_frame_equal(proxy_obj, xpd.DataFrame(obj)) + elif isinstance(proxy_obj, xpd.Series): + tm.assert_series_equal(proxy_obj, xpd.Series(obj)) + elif isinstance(proxy_obj, xpd.Index): + tm.assert_index_equal(proxy_obj, xpd.Index(obj)) + else: + tm.assert_equal(proxy_obj, obj) + else: + assert not is_proxy_object(proxy_obj) + assert proxy_obj == obj + + +def test_as_proxy_object_doesnot_copy_series(): + s = pd.Series([1, 2, 3]) + proxy_obj = as_proxy_object(s) + s[0] = 10 + assert proxy_obj[0] == 10 + tm.assert_series_equal(s, proxy_obj) + + +def test_as_proxy_object_doesnot_copy_dataframe(): + df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) + proxy_obj = as_proxy_object(df) + df.iloc[0, 0] = 10 + assert proxy_obj.iloc[0, 0] == 10 + tm.assert_frame_equal(df, proxy_obj) + + +def test_as_proxy_object_doesnot_copy_index(): + idx = pd.Index([1, 2, 3]) + proxy_obj = as_proxy_object(idx) + assert proxy_obj._fsproxy_wrapped is idx + + +def test_as_proxy_object_no_op_for_intermediates(): + s = pd.Series(["abc", "def", "ghi"]) + str_attr = s.str + proxy_obj = as_proxy_object(str_attr) + assert proxy_obj is str_attr + + def test_pickle_round_trip_proxy_numpy_array(array): arr, proxy_arr = array pickled_arr = BytesIO() From 10048b813bc4054c9a092f31194a676e7459e840 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke <10647082+mroeschke@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:26:23 -0800 Subject: [PATCH 38/45] Make Column.view/can_cast_safely accept a dtype object (#18066) Partially broken off from https://github.com/rapidsai/cudf/pull/17978 Since Column objects are technically private, not marking this as breaking. Authors: - Matthew Roeschke (https://github.com/mroeschke) Approvers: - Vyas Ramasubramani (https://github.com/vyasr) URL: https://github.com/rapidsai/cudf/pull/18066 --- python/cudf/cudf/core/column/categorical.py | 3 +- python/cudf/cudf/core/column/column.py | 10 ++---- python/cudf/cudf/core/column/datetime.py | 3 +- python/cudf/cudf/core/column/string.py | 35 +++++++++------------ python/cudf/cudf/core/column/timedelta.py | 13 +++++--- python/cudf/cudf/tests/test_column.py | 15 ++++++--- python/cudf/cudf/utils/utils.py | 3 +- 7 files changed, 44 insertions(+), 38 deletions(-) diff --git a/python/cudf/cudf/core/column/categorical.py b/python/cudf/cudf/core/column/categorical.py index a57ff9a7817..d41e448254c 100644 --- a/python/cudf/cudf/core/column/categorical.py +++ b/python/cudf/cudf/core/column/categorical.py @@ -36,6 +36,7 @@ ColumnBinaryOperand, ColumnLike, Dtype, + DtypeObj, ScalarLike, SeriesOrIndex, SeriesOrSingleColumnIndex, @@ -1168,7 +1169,7 @@ def _mimic_inplace( self._codes = other_col.codes return out - def view(self, dtype: Dtype) -> ColumnBase: + def view(self, dtype: DtypeObj) -> ColumnBase: raise NotImplementedError( "Categorical column views are not currently supported" ) diff --git a/python/cudf/cudf/core/column/column.py b/python/cudf/cudf/core/column/column.py index 89ac39b2be5..61f4f7d52fb 100644 --- a/python/cudf/cudf/core/column/column.py +++ b/python/cudf/cudf/core/column/column.py @@ -950,7 +950,7 @@ def copy(self, deep: bool = True) -> Self: ), ) - def view(self, dtype: Dtype) -> ColumnBase: + def view(self, dtype: DtypeObj) -> ColumnBase: """ View the data underlying a column as different dtype. The source column must divide evenly into the size of @@ -959,13 +959,9 @@ def view(self, dtype: Dtype) -> ColumnBase: Parameters ---------- - dtype : NumPy dtype, string + dtype : Dtype object The dtype to view the data as - """ - - dtype = cudf.dtype(dtype) - if dtype.kind in ("o", "u", "s"): raise TypeError( "Bytes viewed as str without metadata is ambiguous" @@ -1586,7 +1582,7 @@ def distinct_count(self, dropna: bool = True) -> int: self._distinct_count[dropna] = result return self._distinct_count[dropna] - def can_cast_safely(self, to_dtype: Dtype) -> bool: + def can_cast_safely(self, to_dtype: DtypeObj) -> bool: raise NotImplementedError() @acquire_spill_lock() diff --git a/python/cudf/cudf/core/column/datetime.py b/python/cudf/cudf/core/column/datetime.py index 92d5c39e69d..213e91d7b3f 100644 --- a/python/cudf/cudf/core/column/datetime.py +++ b/python/cudf/cudf/core/column/datetime.py @@ -47,6 +47,7 @@ ColumnBinaryOperand, DatetimeLikeScalar, Dtype, + DtypeObj, ScalarLike, ) from cudf.core.column.numerical import NumericalColumn @@ -837,7 +838,7 @@ def is_unique(self) -> bool: def isin(self, values: Sequence) -> ColumnBase: return cudf.core.tools.datetimes._isin_datetimelike(self, values) - def can_cast_safely(self, to_dtype: Dtype) -> bool: + def can_cast_safely(self, to_dtype: DtypeObj) -> bool: if to_dtype.kind == "M": # type: ignore[union-attr] to_res, _ = np.datetime_data(to_dtype) self_res, _ = np.datetime_data(self.dtype) diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 28e8b98edfe..944f5cd6d26 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -43,6 +43,7 @@ ColumnBinaryOperand, ColumnLike, Dtype, + DtypeObj, ScalarLike, SeriesOrIndex, ) @@ -5640,7 +5641,7 @@ def __init__( if not isinstance(data, Buffer): raise ValueError("data must be a Buffer") if dtype != CUDF_STRING_DTYPE: - raise ValueError(f"dtypy must be {CUDF_STRING_DTYPE}") + raise ValueError(f"dtype must be {CUDF_STRING_DTYPE}") if len(children) > 1: raise ValueError("StringColumn must have at most 1 offset column.") @@ -5826,23 +5827,22 @@ def __contains__(self, item: ScalarLike) -> bool: other = [item] if is_scalar(item) else item return self.contains(column.as_column(other, dtype=self.dtype)).any() - def as_numerical_column(self, dtype: Dtype) -> NumericalColumn: - out_dtype = cudf.api.types.dtype(dtype) - if out_dtype.kind == "b": + def as_numerical_column(self, dtype: np.dtype) -> NumericalColumn: + if dtype.kind == "b": with acquire_spill_lock(): plc_column = plc.strings.attributes.count_characters( self.to_pylibcudf(mode="read") ) result = ColumnBase.from_pylibcudf(plc_column) return (result > np.int8(0)).fillna(False) - elif out_dtype.kind in {"i", "u"}: + elif dtype.kind in {"i", "u"}: if not self.is_integer().all(): raise ValueError( "Could not convert strings to integer " "type due to presence of non-integer values." ) cast_func = plc.strings.convert.convert_integers.to_integers - elif out_dtype.kind == "f": + elif dtype.kind == "f": if not self.is_float().all(): raise ValueError( "Could not convert strings to float " @@ -5850,10 +5850,8 @@ def as_numerical_column(self, dtype: Dtype) -> NumericalColumn: ) cast_func = plc.strings.convert.convert_floats.to_floats else: - raise ValueError( - f"dtype must be a numerical type, not {out_dtype}" - ) - plc_dtype = dtype_to_pylibcudf_type(out_dtype) + raise ValueError(f"dtype must be a numerical type, not {dtype}") + plc_dtype = dtype_to_pylibcudf_type(dtype) with acquire_spill_lock(): return type(self).from_pylibcudf( # type: ignore[return-value] cast_func(self.to_pylibcudf(mode="read"), plc_dtype) @@ -5973,17 +5971,15 @@ def to_pandas( else: return super().to_pandas(nullable=nullable, arrow_type=arrow_type) - def can_cast_safely(self, to_dtype: Dtype) -> bool: - to_dtype = cudf.api.types.dtype(to_dtype) - + def can_cast_safely(self, to_dtype: DtypeObj) -> bool: if self.dtype == to_dtype: return True - elif to_dtype.kind in {"i", "u"} and not self.is_integer().all(): - return False - elif to_dtype.kind == "f" and not self.is_float().all(): - return False - else: + elif to_dtype.kind in {"i", "u"} and self.is_integer().all(): + return True + elif to_dtype.kind == "f" and self.is_float().all(): return True + else: + return False def find_and_replace( self, @@ -6122,12 +6118,11 @@ def _binaryop( return NotImplemented @copy_docstring(ColumnBase.view) - def view(self, dtype) -> ColumnBase: + def view(self, dtype: DtypeObj) -> ColumnBase: if self.null_count > 0: raise ValueError( "Can not produce a view of a string column with nulls" ) - dtype = cudf.api.types.dtype(dtype) str_byte_offset = self.base_children[0].element_indexing(self.offset) str_end_byte_offset = self.base_children[0].element_indexing( self.offset + self.size diff --git a/python/cudf/cudf/core/column/timedelta.py b/python/cudf/cudf/core/column/timedelta.py index d02681d389d..e4d47f492c2 100644 --- a/python/cudf/cudf/core/column/timedelta.py +++ b/python/cudf/cudf/core/column/timedelta.py @@ -28,7 +28,12 @@ if TYPE_CHECKING: from collections.abc import Sequence - from cudf._typing import ColumnBinaryOperand, DatetimeLikeScalar, Dtype + from cudf._typing import ( + ColumnBinaryOperand, + DatetimeLikeScalar, + Dtype, + DtypeObj, + ) _unit_to_nanoseconds_conversion = { "ns": 1, @@ -380,10 +385,10 @@ def find_and_replace( ), ) - def can_cast_safely(self, to_dtype: Dtype) -> bool: - if to_dtype.kind == "m": # type: ignore[union-attr] + def can_cast_safely(self, to_dtype: DtypeObj) -> bool: + if to_dtype.kind == "m": to_res, _ = np.datetime_data(to_dtype) - self_res, _ = np.datetime_data(self.dtype) + self_res = self.time_unit max_int = np.iinfo(np.int64).max diff --git a/python/cudf/cudf/tests/test_column.py b/python/cudf/cudf/tests/test_column.py index 2996a88c171..b7cd2388f30 100644 --- a/python/cudf/cudf/tests/test_column.py +++ b/python/cudf/cudf/tests/test_column.py @@ -290,6 +290,8 @@ def test_column_chunked_array_creation(): ], ) def test_column_view_valid_numeric_to_numeric(data, from_dtype, to_dtype): + from_dtype = np.dtype(from_dtype) + to_dtype = np.dtype(to_dtype) cpu_data = np.asarray(data, dtype=from_dtype) gpu_data = as_column(data, dtype=from_dtype) @@ -314,6 +316,8 @@ def test_column_view_valid_numeric_to_numeric(data, from_dtype, to_dtype): ], ) def test_column_view_invalid_numeric_to_numeric(data, from_dtype, to_dtype): + from_dtype = np.dtype(from_dtype) + to_dtype = np.dtype(to_dtype) cpu_data = np.asarray(data, dtype=from_dtype) gpu_data = as_column(data, dtype=from_dtype) @@ -337,6 +341,7 @@ def test_column_view_invalid_numeric_to_numeric(data, from_dtype, to_dtype): ], ) def test_column_view_valid_string_to_numeric(data, to_dtype): + to_dtype = np.dtype(to_dtype) expect = cudf.Series._from_column(cudf.Series(data)._column.view(to_dtype)) got = cudf.Series(str_host_view(data, to_dtype)) @@ -352,7 +357,7 @@ def test_column_view_nulls_widths_even(): sr = cudf.Series(data, dtype="int32") expect = cudf.Series(expect_data, dtype="float32") - got = cudf.Series._from_column(sr._column.view("float32")) + got = cudf.Series._from_column(sr._column.view(np.dtype(np.float32))) assert_eq(expect, got) @@ -364,7 +369,7 @@ def test_column_view_nulls_widths_even(): sr = cudf.Series(data, dtype="float64") expect = cudf.Series(expect_data, dtype="int64") - got = cudf.Series._from_column(sr._column.view("int64")) + got = cudf.Series._from_column(sr._column.view(np.dtype(np.int64))) assert_eq(expect, got) @@ -376,7 +381,7 @@ def test_column_view_numeric_slice(slc): expect = cudf.Series(data[slc].view("int64")) got = cudf.Series._from_column( - sr._column.slice(slc.start, slc.stop).view("int64") + sr._column.slice(slc.start, slc.stop).view(np.dtype(np.int64)) ) assert_eq(expect, got) @@ -389,7 +394,9 @@ def test_column_view_string_slice(slc): data = ["a", "bcde", "cd", "efg", "h"] expect = cudf.Series._from_column( - cudf.Series(data)._column.slice(slc.start, slc.stop).view("int8") + cudf.Series(data) + ._column.slice(slc.start, slc.stop) + .view(np.dtype(np.int8)) ) got = cudf.Series(str_host_view(data[slc], "int8")) diff --git a/python/cudf/cudf/utils/utils.py b/python/cudf/cudf/utils/utils.py index fd946937945..2678a4f8116 100644 --- a/python/cudf/cudf/utils/utils.py +++ b/python/cudf/cudf/utils/utils.py @@ -18,9 +18,10 @@ import cudf.api.types from cudf.core import column from cudf.core.buffer import as_buffer +from cudf.utils.dtypes import SIZE_TYPE_DTYPE # The size of the mask in bytes -mask_dtype = cudf.api.types.dtype(np.int32) +mask_dtype = SIZE_TYPE_DTYPE mask_bitsize = mask_dtype.itemsize * 8 # Mapping from ufuncs to the corresponding binary operators. From b8ec71a24b4b8a3e3a997f38881ddfedd698610e Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 27 Feb 2025 06:56:24 -0500 Subject: [PATCH 39/45] Bump polars version to <1.24 (#18076) The PR upgrades the Polars version to 1.23. Authors: - Matthew Murray (https://github.com/Matt711) - GALI PREM SAGAR (https://github.com/galipremsagar) - Bradley Dice (https://github.com/bdice) Approvers: - Matthew Roeschke (https://github.com/mroeschke) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/18076 --- ci/test_cudf_polars_polars_tests.sh | 2 ++ .../environments/all_cuda-118_arch-x86_64.yaml | 2 +- .../environments/all_cuda-128_arch-x86_64.yaml | 2 +- conda/recipes/cudf-polars/meta.yaml | 2 +- dependencies.yaml | 2 +- .../cudf_polars/cudf_polars/testing/plugin.py | 1 - python/cudf_polars/cudf_polars/utils/dtypes.py | 18 ++++++++++++++---- python/cudf_polars/pyproject.toml | 2 +- 8 files changed, 21 insertions(+), 10 deletions(-) diff --git a/ci/test_cudf_polars_polars_tests.sh b/ci/test_cudf_polars_polars_tests.sh index 3466edacfc5..1df7bb61834 100755 --- a/ci/test_cudf_polars_polars_tests.sh +++ b/ci/test_cudf_polars_polars_tests.sh @@ -26,6 +26,8 @@ git clone https://github.com/pola-rs/polars.git --branch "${TAG}" --depth 1 # Install requirements for running polars tests rapids-logger "Install polars test requirements" +# TODO: Remove sed command when polars-cloud supports 1.23 +sed -i '/^polars-cloud$/d' polars/py-polars/requirements-dev.txt rapids-pip-retry install -r polars/py-polars/requirements-dev.txt -r polars/py-polars/requirements-ci.txt # shellcheck disable=SC2317 diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml index e7dbb765099..a23981b4e72 100644 --- a/conda/environments/all_cuda-118_arch-x86_64.yaml +++ b/conda/environments/all_cuda-118_arch-x86_64.yaml @@ -66,7 +66,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.20,<1.23 +- polars>=1.20,<1.24 - pre-commit - ptxcompiler - pyarrow>=14.0.0,<20.0.0a0 diff --git a/conda/environments/all_cuda-128_arch-x86_64.yaml b/conda/environments/all_cuda-128_arch-x86_64.yaml index 342ec8d4b59..e2b9302dc36 100644 --- a/conda/environments/all_cuda-128_arch-x86_64.yaml +++ b/conda/environments/all_cuda-128_arch-x86_64.yaml @@ -64,7 +64,7 @@ dependencies: - pandas - pandas>=2.0,<2.2.4dev0 - pandoc -- polars>=1.20,<1.23 +- polars>=1.20,<1.24 - pre-commit - pyarrow>=14.0.0,<20.0.0a0 - pydata-sphinx-theme>=0.15.4 diff --git a/conda/recipes/cudf-polars/meta.yaml b/conda/recipes/cudf-polars/meta.yaml index 1d36ab2a3e4..64a147d3c63 100644 --- a/conda/recipes/cudf-polars/meta.yaml +++ b/conda/recipes/cudf-polars/meta.yaml @@ -43,7 +43,7 @@ requirements: run: - python - pylibcudf ={{ version }} - - polars >=1.20,<1.23 + - polars >=1.20,<1.24 - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }} test: diff --git a/dependencies.yaml b/dependencies.yaml index c7869eee922..1578dadc793 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -813,7 +813,7 @@ dependencies: common: - output_types: [conda, requirements, pyproject] packages: - - polars>=1.20,<1.23 + - polars>=1.20,<1.24 run_cudf_polars_experimental: common: - output_types: [conda, requirements, pyproject] diff --git a/python/cudf_polars/cudf_polars/testing/plugin.py b/python/cudf_polars/cudf_polars/testing/plugin.py index a7b10a6e8fa..9b798688992 100644 --- a/python/cudf_polars/cudf_polars/testing/plugin.py +++ b/python/cudf_polars/cudf_polars/testing/plugin.py @@ -197,7 +197,6 @@ def pytest_configure(config: pytest.Config) -> None: "tests/unit/io/test_multiscan.py::test_include_file_paths[scan_csv-write_csv]": "Need to expose include_file_paths xref: cudf#18012", "tests/unit/streaming/test_streaming_io.py::test_parquet_eq_statistics[False]": "Debug output on stderr doesn't match", # Maybe flaky, order-dependent? - "tests/unit/test_projections.py::test_schema_full_outer_join_projection_pd_13287": "Order-specific result check, query is correct but in different order", "tests/unit/test_queries.py::test_group_by_agg_equals_zero_3535": "libcudf sums all nulls to null, not zero", } diff --git a/python/cudf_polars/cudf_polars/utils/dtypes.py b/python/cudf_polars/cudf_polars/utils/dtypes.py index 6bb5d78c488..85a4f007cf0 100644 --- a/python/cudf_polars/cudf_polars/utils/dtypes.py +++ b/python/cudf_polars/cudf_polars/utils/dtypes.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. # SPDX-License-Identifier: Apache-2.0 """Datatype utilities.""" @@ -71,7 +71,9 @@ def can_cast(from_: plc.DataType, to: plc.DataType) -> bool: ------- True if casting is supported, False otherwise """ - has_empty = from_.id() == plc.TypeId.EMPTY or to.id() == plc.TypeId.EMPTY + to_is_empty = to.id() == plc.TypeId.EMPTY + from_is_empty = from_.id() == plc.TypeId.EMPTY + has_empty = to_is_empty or from_is_empty return ( ( from_ == to @@ -84,8 +86,16 @@ def can_cast(from_: plc.DataType, to: plc.DataType) -> bool: ) ) ) - or (from_.id() == plc.TypeId.STRING and is_numeric_not_bool(to)) - or (to.id() == plc.TypeId.STRING and is_numeric_not_bool(from_)) + or ( + from_.id() == plc.TypeId.STRING + and not to_is_empty + and is_numeric_not_bool(to) + ) + or ( + to.id() == plc.TypeId.STRING + and not from_is_empty + and is_numeric_not_bool(from_) + ) ) diff --git a/python/cudf_polars/pyproject.toml b/python/cudf_polars/pyproject.toml index 9026a0c29ca..e9fc054efc2 100644 --- a/python/cudf_polars/pyproject.toml +++ b/python/cudf_polars/pyproject.toml @@ -19,7 +19,7 @@ authors = [ license = { text = "Apache 2.0" } requires-python = ">=3.10" dependencies = [ - "polars>=1.20,<1.23", + "polars>=1.20,<1.24", "pylibcudf==25.4.*,>=0.0.0a0", ] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`. classifiers = [ From 25f17ad02615afd7cbb9ee2784de392f6e0c7a66 Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:13:23 -0500 Subject: [PATCH 40/45] Make pylibcudf traits raise exceptions gracefully rather than terminating in C++ (#18117) Closes #18110 Authors: - Matthew Murray (https://github.com/Matt711) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: https://github.com/rapidsai/cudf/pull/18117 --- .../pylibcudf/libcudf/utilities/traits.pxd | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd b/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd index 93f13a7e11f..33749141590 100644 --- a/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/utilities/traits.pxd @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. from libcpp cimport bool from libcpp.vector cimport vector from pylibcudf.exception_handler cimport libcudf_exception_handler @@ -6,22 +6,22 @@ from pylibcudf.libcudf.types cimport data_type cdef extern from "cudf/utilities/traits.hpp" namespace "cudf" nogil: - cdef bool is_relationally_comparable(data_type) - cdef bool is_equality_comparable(data_type) - cdef bool is_numeric(data_type) - cdef bool is_numeric_not_bool(data_type) - cdef bool is_index_type(data_type) - cdef bool is_unsigned(data_type) - cdef bool is_integral(data_type) - cdef bool is_integral_not_bool(data_type) - cdef bool is_floating_point(data_type) - cdef bool is_boolean(data_type) - cdef bool is_timestamp(data_type) - cdef bool is_fixed_point(data_type) - cdef bool is_duration(data_type) - cdef bool is_chrono(data_type) - cdef bool is_dictionary(data_type) - cdef bool is_fixed_width(data_type) - cdef bool is_compound(data_type) - cdef bool is_nested(data_type) - cdef bool is_bit_castable(data_type, data_type) + cdef bool is_relationally_comparable(data_type) except +libcudf_exception_handler + cdef bool is_equality_comparable(data_type) except +libcudf_exception_handler + cdef bool is_numeric(data_type) except +libcudf_exception_handler + cdef bool is_numeric_not_bool(data_type) except +libcudf_exception_handler + cdef bool is_index_type(data_type) except +libcudf_exception_handler + cdef bool is_unsigned(data_type) except +libcudf_exception_handler + cdef bool is_integral(data_type) except +libcudf_exception_handler + cdef bool is_integral_not_bool(data_type) except +libcudf_exception_handler + cdef bool is_floating_point(data_type) except +libcudf_exception_handler + cdef bool is_boolean(data_type) except +libcudf_exception_handler + cdef bool is_timestamp(data_type) except +libcudf_exception_handler + cdef bool is_fixed_point(data_type) except +libcudf_exception_handler + cdef bool is_duration(data_type) except +libcudf_exception_handler + cdef bool is_chrono(data_type) except +libcudf_exception_handler + cdef bool is_dictionary(data_type) except +libcudf_exception_handler + cdef bool is_fixed_width(data_type) except +libcudf_exception_handler + cdef bool is_compound(data_type) except +libcudf_exception_handler + cdef bool is_nested(data_type) except +libcudf_exception_handler + cdef bool is_bit_castable(data_type, data_type) except +libcudf_exception_handler From b92d2c0adcca94a5cd04d9206fc89ca059f50f36 Mon Sep 17 00:00:00 2001 From: Vyas Ramasubramani Date: Thu, 27 Feb 2025 10:14:24 -0800 Subject: [PATCH 41/45] Remove now non-existent job (#18123) This job was removed from PRs in https://github.com/rapidsai/cudf/pull/18091 but I forgot to remove the corresponding nightly test job. --- .github/workflows/test.yaml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7046fd0e5dc..8357a12e221 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -46,18 +46,6 @@ jobs: arch: "amd64" container_image: "rapidsai/ci-conda:latest" run_script: "ci/test_cpp_memcheck.sh" - static-configure: - secrets: inherit - uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-25.04 - with: - build_type: ${{ inputs.build_type }} - branch: ${{ inputs.branch }} - date: ${{ inputs.date }} - sha: ${{ inputs.sha }} - # Use the wheel container so we can skip conda solves and since our - # primary static consumers (Spark) are not in conda anyway. - container_image: "rapidsai/ci-wheel:latest" - run_script: "ci/configure_cpp_static.sh" cpp-linters: secrets: inherit uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-25.04 From 960bb28f426d004ed96ac066e07675d87bb186de Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 27 Feb 2025 13:25:54 -0600 Subject: [PATCH 42/45] Use cpu16 for build CI jobs (#18124) We use `cpu16` for PR jobs that build libcudf (conda and wheels). We also need to use `cpu16` for the corresponding jobs in `build.yaml`. --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 11104037c5e..148861c0fa2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -34,6 +34,7 @@ jobs: branch: ${{ inputs.branch }} date: ${{ inputs.date }} sha: ${{ inputs.sha }} + node_type: "cpu16" python-build: needs: [cpp-build] secrets: inherit @@ -77,6 +78,7 @@ jobs: branch: ${{ inputs.branch }} sha: ${{ inputs.sha }} date: ${{ inputs.date }} + node_type: "cpu16" script: ci/build_wheel_libcudf.sh wheel-publish-libcudf: needs: wheel-build-libcudf From 08ea13a407f09babe647fef8cf98595c7e710f0b Mon Sep 17 00:00:00 2001 From: Michael Schellenberger Costa Date: Thu, 27 Feb 2025 20:35:44 +0100 Subject: [PATCH 43/45] Add include for `` (#18102) There are some files that use `std::function` and it seems they were relying on transitive includes from CCCL headers because building cudf fails with CCCL 2.8, which is the next CCCL release in line for rapids Authors: - Michael Schellenberger Costa (https://github.com/miscco) Approvers: - David Wendt (https://github.com/davidwendt) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/18102 --- cpp/benchmarks/common/random_distribution_factory.cuh | 3 ++- cpp/src/column/column_device_view.cu | 3 ++- cpp/src/io/functions.cpp | 1 + cpp/src/io/json/host_tree_algorithms.cu | 3 ++- cpp/src/io/json/read_json.cu | 1 + cpp/src/io/orc/aggregate_orc_metadata.cpp | 1 + cpp/src/io/orc/writer_impl.cu | 1 + cpp/src/io/parquet/reader_impl_chunking.cu | 1 + cpp/src/io/parquet/writer_impl.cu | 1 + cpp/src/lists/dremel.cu | 4 +++- cpp/src/strings/regex/regex.cuh | 3 ++- cpp/src/strings/replace/multi_re.cu | 3 ++- cpp/src/table/row_operators.cu | 4 +++- cpp/src/text/bpe/load_merge_pairs.cu | 3 ++- cpp/tests/groupby/tdigest_tests.cu | 4 +++- cpp/tests/io/metadata_utilities.cpp | 4 +++- cpp/tests/io/parquet_writer_test.cpp | 1 + cpp/tests/reductions/scan_tests.cpp | 3 ++- 18 files changed, 33 insertions(+), 11 deletions(-) diff --git a/cpp/benchmarks/common/random_distribution_factory.cuh b/cpp/benchmarks/common/random_distribution_factory.cuh index c27616132d0..32424fbaaa3 100644 --- a/cpp/benchmarks/common/random_distribution_factory.cuh +++ b/cpp/benchmarks/common/random_distribution_factory.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024, NVIDIA CORPORATION. + * Copyright (c) 2020-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ #include #include +#include #include #include diff --git a/cpp/src/column/column_device_view.cu b/cpp/src/column/column_device_view.cu index 9dc39f01ab3..c304d705f9b 100644 --- a/cpp/src/column/column_device_view.cu +++ b/cpp/src/column/column_device_view.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ #include #include +#include #include namespace cudf { diff --git a/cpp/src/io/functions.cpp b/cpp/src/io/functions.cpp index 53c1d335a40..204aca8a69c 100644 --- a/cpp/src/io/functions.cpp +++ b/cpp/src/io/functions.cpp @@ -36,6 +36,7 @@ #include #include +#include #include namespace cudf::io { diff --git a/cpp/src/io/json/host_tree_algorithms.cu b/cpp/src/io/json/host_tree_algorithms.cu index 7b9fc25d1cc..e506d60a2be 100644 --- a/cpp/src/io/json/host_tree_algorithms.cu +++ b/cpp/src/io/json/host_tree_algorithms.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, NVIDIA CORPORATION. + * Copyright (c) 2024-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ #include #include +#include namespace cudf::io::json::detail { diff --git a/cpp/src/io/json/read_json.cu b/cpp/src/io/json/read_json.cu index 0c95c2b05e8..c265ac5e316 100644 --- a/cpp/src/io/json/read_json.cu +++ b/cpp/src/io/json/read_json.cu @@ -43,6 +43,7 @@ #include #include +#include #include namespace cudf::io::json::detail { diff --git a/cpp/src/io/orc/aggregate_orc_metadata.cpp b/cpp/src/io/orc/aggregate_orc_metadata.cpp index 050bf692c14..77643d294e8 100644 --- a/cpp/src/io/orc/aggregate_orc_metadata.cpp +++ b/cpp/src/io/orc/aggregate_orc_metadata.cpp @@ -19,6 +19,7 @@ #include "io/utilities/row_selection.hpp" #include +#include #include namespace cudf::io::orc::detail { diff --git a/cpp/src/io/orc/writer_impl.cu b/cpp/src/io/orc/writer_impl.cu index dbf5e293c4e..3a20ffbce19 100644 --- a/cpp/src/io/orc/writer_impl.cu +++ b/cpp/src/io/orc/writer_impl.cu @@ -64,6 +64,7 @@ #include #include +#include #include #include #include diff --git a/cpp/src/io/parquet/reader_impl_chunking.cu b/cpp/src/io/parquet/reader_impl_chunking.cu index 03a37327e9b..be1e7d38fff 100644 --- a/cpp/src/io/parquet/reader_impl_chunking.cu +++ b/cpp/src/io/parquet/reader_impl_chunking.cu @@ -40,6 +40,7 @@ #include #include +#include #include namespace cudf::io::parquet::detail { diff --git a/cpp/src/io/parquet/writer_impl.cu b/cpp/src/io/parquet/writer_impl.cu index 9e50fafa8a7..4a410cec558 100644 --- a/cpp/src/io/parquet/writer_impl.cu +++ b/cpp/src/io/parquet/writer_impl.cu @@ -53,6 +53,7 @@ #include #include +#include #include #include #include diff --git a/cpp/src/lists/dremel.cu b/cpp/src/lists/dremel.cu index 469442d46d4..d7b1bf360fe 100644 --- a/cpp/src/lists/dremel.cu +++ b/cpp/src/lists/dremel.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,8 @@ #include #include +#include + namespace cudf::detail { namespace { /** diff --git a/cpp/src/strings/regex/regex.cuh b/cpp/src/strings/regex/regex.cuh index d22fb04696c..6071a9fdd2d 100644 --- a/cpp/src/strings/regex/regex.cuh +++ b/cpp/src/strings/regex/regex.cuh @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. All rights reserved. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ #include #include +#include #include namespace cudf { diff --git a/cpp/src/strings/replace/multi_re.cu b/cpp/src/strings/replace/multi_re.cu index 0777253bb38..af8b53ccd8c 100644 --- a/cpp/src/strings/replace/multi_re.cu +++ b/cpp/src/strings/replace/multi_re.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ #include #include +#include namespace cudf { namespace strings { diff --git a/cpp/src/table/row_operators.cu b/cpp/src/table/row_operators.cu index 990c4855a14..d77cc0cf17a 100644 --- a/cpp/src/table/row_operators.cu +++ b/cpp/src/table/row_operators.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,8 @@ #include +#include + namespace cudf { namespace experimental { diff --git a/cpp/src/text/bpe/load_merge_pairs.cu b/cpp/src/text/bpe/load_merge_pairs.cu index a13a435a271..9118fe54ab2 100644 --- a/cpp/src/text/bpe/load_merge_pairs.cu +++ b/cpp/src/text/bpe/load_merge_pairs.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ #include #include +#include #include #include diff --git a/cpp/tests/groupby/tdigest_tests.cu b/cpp/tests/groupby/tdigest_tests.cu index 883a5093bd1..ad92e322ee2 100644 --- a/cpp/tests/groupby/tdigest_tests.cu +++ b/cpp/tests/groupby/tdigest_tests.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, NVIDIA CORPORATION. + * Copyright (c) 2021-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,8 @@ #include #include +#include + namespace { /** * @brief Functor to generate a tdigest by key. diff --git a/cpp/tests/io/metadata_utilities.cpp b/cpp/tests/io/metadata_utilities.cpp index 380d66c53f9..980d8d8b3d1 100644 --- a/cpp/tests/io/metadata_utilities.cpp +++ b/cpp/tests/io/metadata_utilities.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, NVIDIA CORPORATION. + * Copyright (c) 2021-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ #include #include +#include + namespace cudf::test { void expect_metadata_equal(cudf::io::table_input_metadata in_meta, diff --git a/cpp/tests/io/parquet_writer_test.cpp b/cpp/tests/io/parquet_writer_test.cpp index e201dc0565c..d99e19822c0 100644 --- a/cpp/tests/io/parquet_writer_test.cpp +++ b/cpp/tests/io/parquet_writer_test.cpp @@ -33,6 +33,7 @@ #include #include +#include using cudf::test::iterators::no_nulls; diff --git a/cpp/tests/reductions/scan_tests.cpp b/cpp/tests/reductions/scan_tests.cpp index 5f911597b02..c6c419706e0 100644 --- a/cpp/tests/reductions/scan_tests.cpp +++ b/cpp/tests/reductions/scan_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024, NVIDIA CORPORATION. + * Copyright (c) 2019-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ #include #include +#include #include using aggregation = cudf::aggregation; From 4fda491e84bf212e16ab8d6ee5cf97da6d67362b Mon Sep 17 00:00:00 2001 From: David Wendt <45795991+davidwendt@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:42:40 -0500 Subject: [PATCH 44/45] Add new nvtext tokenized minhash API (#17944) Creates a new minhash API that works on ngrams of row elements given a list column of strings. ``` std::unique_ptr minhash_ngrams( cudf::lists_column_view const& input, cudf::size_type ngrams, uint32_t seed, cudf::device_span parameter_a, cudf::device_span parameter_b, rmm::cuda_stream_view stream, rmm::device_async_resource_ref mr); ``` The input column is expected to be rows of words (strings) and each row is hashed using a sliding window of words (ngrams) and then the permuted algorithm is re-used to produce the minhash values. Authors: - David Wendt (https://github.com/davidwendt) Approvers: - Ayush Dattagupta (https://github.com/ayushdg) - Matthew Murray (https://github.com/Matt711) - Yunsong Wang (https://github.com/PointKernel) - Muhammad Haseeb (https://github.com/mhaseeb123) - Shruti Shivakumar (https://github.com/shrshi) URL: https://github.com/rapidsai/cudf/pull/17944 --- cpp/include/nvtext/minhash.hpp | 94 +++++ cpp/src/text/minhash.cu | 392 +++++++++++++++--- cpp/tests/text/minhash_tests.cpp | 173 +++++++- python/cudf/cudf/core/column/string.py | 114 +++++ .../cudf/cudf/tests/text/test_text_methods.py | 42 ++ .../pylibcudf/libcudf/nvtext/minhash.pxd | 18 +- python/pylibcudf/pylibcudf/nvtext/minhash.pxd | 18 +- python/pylibcudf/pylibcudf/nvtext/minhash.pyi | 8 +- python/pylibcudf/pylibcudf/nvtext/minhash.pyx | 96 ++++- .../pylibcudf/tests/test_nvtext_minhash.py | 48 ++- 10 files changed, 911 insertions(+), 92 deletions(-) diff --git a/cpp/include/nvtext/minhash.hpp b/cpp/include/nvtext/minhash.hpp index 43f060fdafa..5f978a0d8ec 100644 --- a/cpp/include/nvtext/minhash.hpp +++ b/cpp/include/nvtext/minhash.hpp @@ -125,5 +125,99 @@ std::unique_ptr minhash64( rmm::cuda_stream_view stream = cudf::get_default_stream(), rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); +/** + * @brief Returns the minhash values for each input row + * + * This function uses MurmurHash3_x86_32 for the hash algorithm. + * + * The input row is first hashed using the given `seed` over a sliding window + * of `ngrams` of strings. These hash values are then combined with the `a` + * and `b` parameter values using the following formula: + * ``` + * max_hash = max of uint32 + * mp = (1 << 61) - 1 + * hv[i] = hash value of a ngrams at i + * pv[i] = ((hv[i] * a[i] + b[i]) % mp) & max_hash + * ``` + * + * This calculation is performed on each set of ngrams and the minimum value + * is computed as follows: + * ``` + * mh[j,i] = min(pv[i]) for all ngrams in row j + * and where i=[0,a.size()) + * ``` + * + * Any null row entries result in corresponding null output rows. + * + * @throw std::invalid_argument if the ngrams < 2 + * @throw std::invalid_argument if parameter_a is empty + * @throw std::invalid_argument if `parameter_b.size() != parameter_a.size()` + * @throw std::overflow_error if `parameter_a.size() * input.size()` exceeds the column size limit + * + * @param input Strings column to compute minhash + * @param ngrams The number of strings to hash within each row + * @param seed Seed value used for the hash algorithm + * @param parameter_a Values used for the permuted calculation + * @param parameter_b Values used for the permuted calculation + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the returned column's device memory + * @return List column of minhash values for each string per seed + */ +std::unique_ptr minhash_ngrams( + cudf::lists_column_view const& input, + cudf::size_type ngrams, + uint32_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + +/** + * @brief Returns the minhash values for each input row + * + * This function uses MurmurHash3_x64_128 for the hash algorithm. + * + * The input row is first hashed using the given `seed` over a sliding window + * of `ngrams` of strings. These hash values are then combined with the `a` + * and `b` parameter values using the following formula: + * ``` + * max_hash = max of uint64 + * mp = (1 << 61) - 1 + * hv[i] = hash value of a ngrams at i + * pv[i] = ((hv[i] * a[i] + b[i]) % mp) & max_hash + * ``` + * + * This calculation is performed on each set of ngrams and the minimum value + * is computed as follows: + * ``` + * mh[j,i] = min(pv[i]) for all ngrams in row j + * and where i=[0,a.size()) + * ``` + * + * Any null row entries result in corresponding null output rows. + * + * @throw std::invalid_argument if the ngrams < 2 + * @throw std::invalid_argument if parameter_a is empty + * @throw std::invalid_argument if `parameter_b.size() != parameter_a.size()` + * @throw std::overflow_error if `parameter_a.size() * input.size()` exceeds the column size limit + * + * @param input List strings column to compute minhash + * @param ngrams The number of strings to hash within each row + * @param seed Seed value used for the hash algorithm + * @param parameter_a Values used for the permuted calculation + * @param parameter_b Values used for the permuted calculation + * @param stream CUDA stream used for device memory operations and kernel launches + * @param mr Device memory resource used to allocate the returned column's device memory + * @return List column of minhash values for each string per seed + */ +std::unique_ptr minhash64_ngrams( + cudf::lists_column_view const& input, + cudf::size_type ngrams, + uint64_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + rmm::cuda_stream_view stream = cudf::get_default_stream(), + rmm::device_async_resource_ref mr = cudf::get_current_device_resource_ref()); + /** @} */ // end of group } // namespace CUDF_EXPORT nvtext diff --git a/cpp/src/text/minhash.cu b/cpp/src/text/minhash.cu index 50c16c8ba6c..663595af5df 100644 --- a/cpp/src/text/minhash.cu +++ b/cpp/src/text/minhash.cu @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -62,19 +63,20 @@ constexpr cudf::thread_index_type tile_size = block_size; constexpr cuda::std::size_t params_per_thread = 16; // Separate kernels are used to process strings above and below this value (in bytes). -constexpr cudf::size_type wide_string_threshold = 1 << 18; // 256K +constexpr cudf::size_type wide_row_threshold = 1 << 18; // 256K // The number of blocks per string for the above-threshold kernel processing. -constexpr cudf::size_type blocks_per_string = 64; +constexpr cudf::size_type blocks_per_row = 64; // The above values were determined using the redpajama and books_sample datasets /** * @brief Hashing kernel launched as a thread per tile-size (block or warp) + * for strings column * * This kernel computes the hashes for each string using the seed and the specified * hash function. The width is used to compute rolling substrings to hash over. * The hashes are stored in d_hashes to be used in the minhash_kernel. * - * This kernel also counts the number of strings above the wide_string_threshold + * This kernel also counts the number of strings above the wide_row_threshold * and proactively initializes the output values for those strings. * * @tparam HashFunction The hash function to use for this kernel @@ -84,7 +86,7 @@ constexpr cudf::size_type blocks_per_string = 64; * @param seed The seed used for the hash function * @param width Width in characters used for determining substrings to hash * @param d_hashes The resulting hash values are stored here - * @param threshold_count Stores the number of strings above wide_string_threshold + * @param threshold_count Stores the number of strings above wide_row_threshold * @param param_count Number of parameters (used for the proactive initialize) * @param d_results Final results vector (used for the proactive initialize) */ @@ -146,7 +148,7 @@ CUDF_KERNEL void minhash_seed_kernel(cudf::column_device_view const d_strings, } // logic appended here so an extra kernel is not required - if (size_bytes >= wide_string_threshold) { + if (size_bytes >= wide_row_threshold) { if (lane_idx == 0) { // count the number of wide strings cuda::atomic_ref ref{*threshold_count}; @@ -160,31 +162,130 @@ CUDF_KERNEL void minhash_seed_kernel(cudf::column_device_view const d_strings, } } +/** + * @brief Hashing kernel launched as a thread per tile-size (block or warp) + * for a lists column + * + * This kernel computes the hashes for each row using the seed and the specified + * hash function. The ngrams identifies consecutive strings to hash over in + * sliding window formation. The hashes are stored in d_hashes and used as input + * to the minhash_kernel. + * + * This kernel also counts the number of rows above the wide_row_threshold + * and proactively initializes the output values for those rows. + * + * @tparam HashFunction The hash function to use for this kernel + * @tparam hash_value_type Derived from HashFunction result_type + * + * @param d_input The input column to hash + * @param seed The seed used for the hash function + * @param ngrams Number of strings in each row to hash + * @param d_hashes The resulting hash values are stored here + * @param threshold_count Stores the number of rows above wide_row_threshold + * @param param_count Number of parameters (used for the proactive initialize) + * @param d_results Final results vector (used for the proactive initialize) + */ +template +CUDF_KERNEL void minhash_ngrams_kernel(cudf::detail::lists_column_device_view const d_input, + hash_value_type seed, + cudf::size_type ngrams, + hash_value_type* d_hashes, + cudf::size_type* threshold_count, + cudf::size_type param_count, + hash_value_type* d_results) +{ + auto const tid = cudf::detail::grid_1d::global_thread_id(); + auto const row_idx = tid / tile_size; + if (row_idx >= d_input.size()) { return; } + if (d_input.is_null(row_idx)) { return; } + + // retrieve this row's offset to locate the output position in d_hashes + auto const offsets_itr = d_input.offsets().data() + d_input.offset(); + auto const offset = offsets_itr[row_idx]; + auto const size_row = offsets_itr[row_idx + 1] - offset; + if (size_row == 0) { return; } + + auto const d_row = cudf::list_device_view(d_input, row_idx); + auto const lane_idx = static_cast(tid % tile_size); + + // hashes for this row/thread are stored here + auto seed_hashes = d_hashes + offset - offsets_itr[0] + lane_idx; + auto const hasher = HashFunction(seed); + + for (auto idx = lane_idx; idx < size_row; idx += tile_size, seed_hashes += tile_size) { + if (d_row.is_null(idx)) { + *seed_hashes = 0; + continue; + } + + auto next_idx = cuda::std::min(idx + ngrams, size_row - 1); + if ((idx != 0) && ((next_idx - idx) < ngrams)) { + *seed_hashes = 0; + continue; + } + + auto const first_str = d_row.element(idx); + auto const last_str = d_row.element(next_idx); + // build super-string since adjacent strings are contiguous in memory + auto const size = static_cast( + thrust::distance(first_str.data(), last_str.data()) + last_str.size_bytes()); + auto const hash_str = cudf::string_view(first_str.data(), size); + hash_value_type hv; + if constexpr (std::is_same_v) { + hv = hasher(hash_str); + } else { + hv = cuda::std::get<0>(hasher(hash_str)); + } + // disallowing hash to zero case + *seed_hashes = cuda::std::max(hv, hash_value_type{1}); + } + + // logic appended here to count long rows so an extra kernel is not required + if (size_row >= wide_row_threshold) { + if (lane_idx == 0) { + // count the number of wide rows + cuda::atomic_ref ref{*threshold_count}; + ref.fetch_add(1, cuda::std::memory_order_relaxed); + } + // initialize the output -- only needed for wider rows + auto d_output = d_results + (row_idx * param_count); + for (auto i = lane_idx; i < param_count; i += tile_size) { + d_output[i] = cuda::std::numeric_limits::max(); + } + } +} + /** * @brief Permutation calculation kernel * - * This kernel uses the hashes from the minhash_seed_kernel and the parameter_a and - * parameter_b values to compute the final output results. + * This kernel uses the hashes from the minhash_seed_kernel or minhash_ngrams_kernel + * and the 'parameter_a' and 'parameter_b' values to compute the final output. * The output is the number of input rows (N) by the number of parameter values (M). - * Each output[i] is the calculated result for parameter_a/b[0:M]. + * Each row output[i] is the calculated result for parameter_a/b[0:M]. + * + * This kernel is launched with either blocks per row of 1 for rows + * below the wide_row_threshold or blocks per row = blocks_per_rows + * for rows above wide_row_threshold. * - * This kernel is launched with either blocks per strings of 1 for strings - * below the wide_strings_threshold or blocks per string = blocks_per_strings - * for strings above wide_strings_threshold. + * Note that this was refactored to accommodate lists of strings which is possible + * since there is no need here to access the characters, only the hash values. + * The offsets and width are used to locate and count the hash values produced by + * kernels above for each input row. * + * @tparam offsets_type Type for the offsets iterator for the input column * @tparam hash_value_type Derived from HashFunction result_type - * @tparam blocks_per_string Number of blocks used to process each string + * @tparam blocks_per_row Number of blocks used to process each row * - * @param d_strings The input strings to hash - * @param indices The indices of the strings in d_strings to process + * @param offsets_itr The offsets are used to address the d_hashes + * @param indices The indices of the rows in the input column * @param parameter_a 1st set of parameters for the calculation result * @param parameter_b 2nd set of parameters for the calculation result - * @param width Used for calculating the number of available hashes in each string - * @param d_hashes The hash values computed in minhash_seed_kernel + * @param width Used for calculating the number of available hashes in each row + * @param d_hashes The hash values computed in one of the hash kernels * @param d_results Final results vector of calculate values */ -template -CUDF_KERNEL void minhash_kernel(cudf::column_device_view const d_strings, +template +CUDF_KERNEL void minhash_kernel(offsets_type offsets_itr, cudf::device_span indices, cudf::device_span parameter_a, cudf::device_span parameter_b, @@ -193,41 +294,36 @@ CUDF_KERNEL void minhash_kernel(cudf::column_device_view const d_strings, hash_value_type* d_results) { auto const tid = cudf::detail::grid_1d::global_thread_id(); - auto const idx = (tid / blocks_per_string) / block_size; + auto const idx = (tid / blocks_per_row) / block_size; if (idx >= indices.size()) { return; } - auto const str_idx = indices[idx]; - if (d_strings.is_null(str_idx)) { return; } + auto const row_idx = indices[idx]; auto const block = cooperative_groups::this_thread_block(); - int const section_idx = block.group_index().x % blocks_per_string; + int const section_idx = block.group_index().x % blocks_per_row; - auto const offsets = d_strings.child(cudf::strings_column_view::offsets_column_index); - auto const offsets_itr = - cudf::detail::input_offsetalator(offsets.head(), offsets.type(), d_strings.offset()); - auto const offset = offsets_itr[str_idx]; - auto const size_bytes = static_cast(offsets_itr[str_idx + 1] - offset); + auto const offset = offsets_itr[row_idx]; + auto const row_size = static_cast(offsets_itr[row_idx + 1] - offset); // number of items to process in this block; - // last block also includes any remainder values from the size_bytes/blocks_per_string truncation + // last block also includes any remainder values from the row_size/blocks_per_row truncation // example: - // each section_size for string with size 588090 and blocks_per_string=64 is 9188 + // each section_size for string with size 588090 and blocks_per_row=64 is 9188 // except the last section which is 9188 + (588090 % 64) = 9246 - auto const section_size = - (size_bytes / blocks_per_string) + - (section_idx < (blocks_per_string - 1) ? 0 : size_bytes % blocks_per_string); - auto const section_offset = section_idx * (size_bytes / blocks_per_string); + auto const section_size = (row_size / blocks_per_row) + + (section_idx < (blocks_per_row - 1) ? 0 : row_size % blocks_per_row); + auto const section_offset = section_idx * (row_size / blocks_per_row); // hash values for this block/section auto const seed_hashes = d_hashes + offset - offsets_itr[0] + section_offset; // width used here as a max value since a string's char-count <= byte-count auto const hashes_size = - section_idx < (blocks_per_string - 1) + section_idx < (blocks_per_row - 1) ? section_size - : cuda::std::max(static_cast(size_bytes > 0), section_size - width + 1); + : cuda::std::max(static_cast(row_size > 0), section_size - width + 1); - auto const init = size_bytes == 0 ? 0 : cuda::std::numeric_limits::max(); + auto const init = row_size == 0 ? 0 : cuda::std::numeric_limits::max(); auto const lane_idx = block.thread_rank(); - auto const d_output = d_results + (str_idx * parameter_a.size()); + auto const d_output = d_results + (row_idx * parameter_a.size()); auto const begin = seed_hashes + lane_idx; auto const end = seed_hashes + hashes_size; @@ -273,7 +369,7 @@ CUDF_KERNEL void minhash_kernel(cudf::column_device_view const d_strings, // cooperative groups does not have a min function and cub::BlockReduce was slower auto const minv = thrust::reduce(thrust::seq, values, values + block_size, init, thrust::minimum{}); - if constexpr (blocks_per_string > 1) { + if constexpr (blocks_per_row > 1) { // accumulates mins for each block into d_output cuda::atomic_ref ref{d_output[lane_idx + i]}; ref.fetch_min(minv, cuda::std::memory_order_relaxed); @@ -285,6 +381,46 @@ CUDF_KERNEL void minhash_kernel(cudf::column_device_view const d_strings, } } +/** + * @brief Partition input rows by row size + * + * The returned index is the first row above the wide_row_threshold size. + * The returned vector are the indices partitioned above and below the + * wide_row_threshold size. + * + * @param size Number of rows in the input column + * @param threshold_count Number of rows above wide_row_threshold + * @param tfn Transform function returns the size of each row + * @param stream Stream used for allocation and kernel launches + */ +template +std::pair> partition_input( + cudf::size_type size, + cudf::size_type threshold_count, + transform_fn tfn, + rmm::cuda_stream_view stream) +{ + auto indices = rmm::device_uvector(size, stream); + thrust::sequence(rmm::exec_policy(stream), indices.begin(), indices.end()); + cudf::size_type threshold_index = threshold_count < size ? size : 0; + + // if we counted a split of above/below threshold then + // compute partitions based on the size of each string + if ((threshold_count > 0) && (threshold_count < size)) { + auto sizes = rmm::device_uvector(size, stream); + auto begin = thrust::counting_iterator(0); + auto end = begin + size; + thrust::transform(rmm::exec_policy_nosync(stream), begin, end, sizes.data(), tfn); + // these 2 are slightly faster than using partition() + thrust::sort_by_key( + rmm::exec_policy_nosync(stream), sizes.begin(), sizes.end(), indices.begin()); + auto const lb = thrust::lower_bound( + rmm::exec_policy_nosync(stream), sizes.begin(), sizes.end(), wide_row_threshold); + threshold_index = static_cast(thrust::distance(sizes.begin(), lb)); + } + return {threshold_index, std::move(indices)}; +} + template std::unique_ptr minhash_fn(cudf::strings_column_view const& input, hash_value_type seed, @@ -334,40 +470,112 @@ std::unique_ptr minhash_fn(cudf::strings_column_view const& input, d_threshold_count.data(), parameter_a.size(), d_results); - auto const threshold_count = d_threshold_count.value(stream); - auto indices = rmm::device_uvector(input.size(), stream); - thrust::sequence(rmm::exec_policy(stream), indices.begin(), indices.end()); - cudf::size_type threshold_index = threshold_count < input.size() ? input.size() : 0; + auto transform_fn = [d_strings = *d_strings] __device__(auto idx) -> cudf::size_type { + if (d_strings.is_null(idx)) { return 0; } + return d_strings.element(idx).size_bytes(); + }; + auto [threshold_index, indices] = + partition_input(input.size(), d_threshold_count.value(stream), transform_fn, stream); - // if we counted a split of above/below threshold then - // compute partitions based on the size of each string - if ((threshold_count > 0) && (threshold_count < input.size())) { - auto sizes = rmm::device_uvector(input.size(), stream); - thrust::transform(rmm::exec_policy_nosync(stream), - thrust::counting_iterator(0), - thrust::counting_iterator(input.size()), - sizes.data(), - cuda::proclaim_return_type( - [d_strings = *d_strings] __device__(auto idx) -> cudf::size_type { - if (d_strings.is_null(idx)) { return 0; } - return d_strings.element(idx).size_bytes(); - })); - thrust::sort_by_key( - rmm::exec_policy_nosync(stream), sizes.begin(), sizes.end(), indices.begin()); - auto const lb = thrust::lower_bound( - rmm::exec_policy_nosync(stream), sizes.begin(), sizes.end(), wide_string_threshold); - threshold_index = static_cast(thrust::distance(sizes.begin(), lb)); + auto input_offsets = + cudf::detail::offsetalator_factory::make_input_iterator(input.offsets(), input.offset()); + using offsets_type = decltype(input_offsets); + + // handle the strings below the threshold width + if (threshold_index > 0) { + auto d_indices = cudf::device_span(indices.data(), threshold_index); + cudf::detail::grid_1d grid{static_cast(d_indices.size()) * block_size, + block_size}; + minhash_kernel + <<>>( + input_offsets, d_indices, parameter_a, parameter_b, width, d_hashes.data(), d_results); + } + + // handle the strings above the threshold width + if (threshold_index < input.size()) { + auto const count = static_cast(input.size() - threshold_index); + auto d_indices = + cudf::device_span(indices.data() + threshold_index, count); + cudf::detail::grid_1d grid{count * block_size * blocks_per_row, block_size}; + minhash_kernel + <<>>( + input_offsets, d_indices, parameter_a, parameter_b, width, d_hashes.data(), d_results); } + return results; +} + +template +std::unique_ptr minhash_ngrams_fn( + cudf::lists_column_view const& input, + cudf::size_type ngrams, + hash_value_type seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + CUDF_EXPECTS(ngrams >= 2, + "Parameter ngrams should be an integer value of 2 or greater", + std::invalid_argument); + CUDF_EXPECTS(!parameter_a.empty(), "Parameters A and B cannot be empty", std::invalid_argument); + CUDF_EXPECTS(parameter_a.size() == parameter_b.size(), + "Parameters A and B should have the same number of elements", + std::invalid_argument); + CUDF_EXPECTS( + (static_cast(input.size()) * parameter_a.size()) < + static_cast(std::numeric_limits::max()), + "The number of parameters times the number of input rows exceeds the column size limit", + std::overflow_error); + + auto const output_type = cudf::data_type{cudf::type_to_id()}; + if (input.is_empty()) { return cudf::make_empty_column(output_type); } + + auto const d_input = cudf::column_device_view::create(input.parent(), stream); + + auto results = + cudf::make_numeric_column(output_type, + input.size() * static_cast(parameter_a.size()), + cudf::mask_state::UNALLOCATED, + stream, + mr); + auto d_results = results->mutable_view().data(); + + cudf::detail::grid_1d grid{static_cast(input.size()) * block_size, + block_size}; + auto const hashes_size = input.child().size(); + auto d_hashes = rmm::device_uvector(hashes_size, stream); + auto d_threshold_count = cudf::detail::device_scalar(0, stream); + + auto d_list = cudf::detail::lists_column_device_view(*d_input); + minhash_ngrams_kernel + <<>>(d_list, + seed, + ngrams, + d_hashes.data(), + d_threshold_count.data(), + parameter_a.size(), + d_results); + + auto sizes_fn = [d_list] __device__(auto idx) -> cudf::size_type { + if (d_list.is_null(idx)) { return 0; } + return cudf::list_device_view(d_list, idx).size(); + }; + auto [threshold_index, indices] = + partition_input(input.size(), d_threshold_count.value(stream), sizes_fn, stream); + + auto input_offsets = input.offsets_begin(); // already includes input.offset() + using offset_type = decltype(input_offsets); + // handle the strings below the threshold width if (threshold_index > 0) { auto d_indices = cudf::device_span(indices.data(), threshold_index); cudf::detail::grid_1d grid{static_cast(d_indices.size()) * block_size, block_size}; - minhash_kernel + minhash_kernel <<>>( - *d_strings, d_indices, parameter_a, parameter_b, width, d_hashes.data(), d_results); + input_offsets, d_indices, parameter_a, parameter_b, ngrams, d_hashes.data(), d_results); } // handle the strings above the threshold width @@ -375,10 +583,10 @@ std::unique_ptr minhash_fn(cudf::strings_column_view const& input, auto const count = static_cast(input.size() - threshold_index); auto d_indices = cudf::device_span(indices.data() + threshold_index, count); - cudf::detail::grid_1d grid{count * block_size * blocks_per_string, block_size}; - minhash_kernel + cudf::detail::grid_1d grid{count * block_size * blocks_per_row, block_size}; + minhash_kernel <<>>( - *d_strings, d_indices, parameter_a, parameter_b, width, d_hashes.data(), d_results); + input_offsets, d_indices, parameter_a, parameter_b, ngrams, d_hashes.data(), d_results); } return results; @@ -426,6 +634,20 @@ std::unique_ptr minhash(cudf::strings_column_view const& input, return build_list_result(input.parent(), std::move(hashes), parameter_a.size(), stream, mr); } +std::unique_ptr minhash_ngrams(cudf::lists_column_view const& input, + cudf::size_type ngrams, + uint32_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + using HashFunction = cudf::hashing::detail::MurmurHash3_x86_32; + auto hashes = detail::minhash_ngrams_fn( + input, ngrams, seed, parameter_a, parameter_b, stream, mr); + return build_list_result(input.parent(), std::move(hashes), parameter_a.size(), stream, mr); +} + std::unique_ptr minhash64(cudf::strings_column_view const& input, uint64_t seed, cudf::device_span parameter_a, @@ -440,6 +662,20 @@ std::unique_ptr minhash64(cudf::strings_column_view const& input, return build_list_result(input.parent(), std::move(hashes), parameter_a.size(), stream, mr); } +std::unique_ptr minhash64_ngrams(cudf::lists_column_view const& input, + cudf::size_type ngrams, + uint64_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) +{ + using HashFunction = cudf::hashing::detail::MurmurHash3_x64_128; + auto hashes = detail::minhash_ngrams_fn( + input, ngrams, seed, parameter_a, parameter_b, stream, mr); + return build_list_result(input.parent(), std::move(hashes), parameter_a.size(), stream, mr); +} + } // namespace detail std::unique_ptr minhash(cudf::strings_column_view const& input, @@ -454,6 +690,19 @@ std::unique_ptr minhash(cudf::strings_column_view const& input, return detail::minhash(input, seed, parameter_a, parameter_b, width, stream, mr); } +std::unique_ptr minhash_ngrams(cudf::lists_column_view const& input, + cudf::size_type ngrams, + uint32_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) + +{ + CUDF_FUNC_RANGE(); + return detail::minhash_ngrams(input, ngrams, seed, parameter_a, parameter_b, stream, mr); +} + std::unique_ptr minhash64(cudf::strings_column_view const& input, uint64_t seed, cudf::device_span parameter_a, @@ -466,4 +715,17 @@ std::unique_ptr minhash64(cudf::strings_column_view const& input, return detail::minhash64(input, seed, parameter_a, parameter_b, width, stream, mr); } +std::unique_ptr minhash64_ngrams(cudf::lists_column_view const& input, + cudf::size_type ngrams, + uint64_t seed, + cudf::device_span parameter_a, + cudf::device_span parameter_b, + rmm::cuda_stream_view stream, + rmm::device_async_resource_ref mr) + +{ + CUDF_FUNC_RANGE(); + return detail::minhash64_ngrams(input, ngrams, seed, parameter_a, parameter_b, stream, mr); +} + } // namespace nvtext diff --git a/cpp/tests/text/minhash_tests.cpp b/cpp/tests/text/minhash_tests.cpp index 8bfb17e0efd..db43484ab09 100644 --- a/cpp/tests/text/minhash_tests.cpp +++ b/cpp/tests/text/minhash_tests.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024, NVIDIA CORPORATION. + * Copyright (c) 2023-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -187,6 +187,15 @@ TEST_F(MinHashTest, EmptyTest) auto params64 = cudf::test::fixed_width_column_wrapper({1, 2, 3}); results = nvtext::minhash64(view, 0, cudf::column_view(params64), cudf::column_view(params64), 4); EXPECT_EQ(results->size(), 0); + + auto empty = cudf::test::lists_column_wrapper(); + auto lview = cudf::lists_column_view(empty); + results = + nvtext::minhash_ngrams(lview, 4, 0, cudf::column_view(params), cudf::column_view(params)); + EXPECT_EQ(results->size(), 0); + results = + nvtext::minhash64_ngrams(lview, 4, 0, cudf::column_view(params64), cudf::column_view(params64)); + EXPECT_EQ(results->size(), 0); } TEST_F(MinHashTest, ErrorsTest) @@ -194,17 +203,20 @@ TEST_F(MinHashTest, ErrorsTest) auto input = cudf::test::strings_column_wrapper({"this string intentionally left blank"}); auto view = cudf::strings_column_view(input); auto empty = cudf::test::fixed_width_column_wrapper(); - EXPECT_THROW(nvtext::minhash(view, 0, cudf::column_view(empty), cudf::column_view(empty), 0), - std::invalid_argument); + auto eview = cudf::column_view(empty); + EXPECT_THROW(nvtext::minhash(view, 0, eview, eview, 0), std::invalid_argument); auto empty64 = cudf::test::fixed_width_column_wrapper(); - EXPECT_THROW( - nvtext::minhash64(view, 0, cudf::column_view(empty64), cudf::column_view(empty64), 0), - std::invalid_argument); - EXPECT_THROW(nvtext::minhash(view, 0, cudf::column_view(empty), cudf::column_view(empty), 4), - std::invalid_argument); - EXPECT_THROW( - nvtext::minhash64(view, 0, cudf::column_view(empty64), cudf::column_view(empty64), 4), - std::invalid_argument); + auto eview64 = cudf::column_view(empty64); + EXPECT_THROW(nvtext::minhash64(view, 0, eview64, eview64, 0), std::invalid_argument); + EXPECT_THROW(nvtext::minhash(view, 0, eview, eview, 4), std::invalid_argument); + EXPECT_THROW(nvtext::minhash64(view, 0, eview64, eview64, 4), std::invalid_argument); + + auto empty_list = cudf::test::lists_column_wrapper(); + auto lview = cudf::lists_column_view(empty_list); + EXPECT_THROW(nvtext::minhash_ngrams(lview, 0, 0, eview, eview), std::invalid_argument); + EXPECT_THROW(nvtext::minhash64_ngrams(lview, 0, 0, eview64, eview64), std::invalid_argument); + EXPECT_THROW(nvtext::minhash_ngrams(lview, 4, 0, eview, eview), std::invalid_argument); + EXPECT_THROW(nvtext::minhash64_ngrams(lview, 4, 0, eview64, eview64), std::invalid_argument); std::vector h_input(50000, ""); input = cudf::test::strings_column_wrapper(h_input.begin(), h_input.end()); @@ -212,16 +224,133 @@ TEST_F(MinHashTest, ErrorsTest) auto const zeroes = thrust::constant_iterator(0); auto params = cudf::test::fixed_width_column_wrapper(zeroes, zeroes + 50000); - EXPECT_THROW(nvtext::minhash(view, 0, cudf::column_view(params), cudf::column_view(params), 4), - std::overflow_error); + auto pview = cudf::column_view(params); + EXPECT_THROW(nvtext::minhash(view, 0, pview, pview, 4), std::overflow_error); auto params64 = cudf::test::fixed_width_column_wrapper(zeroes, zeroes + 50000); - EXPECT_THROW( - nvtext::minhash64(view, 0, cudf::column_view(params64), cudf::column_view(params64), 4), - std::overflow_error); - - EXPECT_THROW(nvtext::minhash(view, 0, cudf::column_view(params), cudf::column_view(empty), 4), - std::invalid_argument); - EXPECT_THROW( - nvtext::minhash64(view, 0, cudf::column_view(params64), cudf::column_view(empty64), 4), - std::invalid_argument); + auto pview64 = cudf::column_view(params64); + EXPECT_THROW(nvtext::minhash64(view, 0, pview64, pview64, 4), std::overflow_error); + + auto offsets = cudf::test::fixed_width_column_wrapper( + thrust::counting_iterator(0), + thrust::counting_iterator(h_input.size() + 1)); + auto input_ngrams = + cudf::make_lists_column(h_input.size(), offsets.release(), input.release(), 0, {}); + lview = cudf::lists_column_view(input_ngrams->view()); + EXPECT_THROW(nvtext::minhash_ngrams(lview, 4, 0, pview, pview), std::overflow_error); + EXPECT_THROW(nvtext::minhash64_ngrams(lview, 4, 0, pview64, pview64), std::overflow_error); +} + +TEST_F(MinHashTest, Ngrams) +{ + using LCWS = cudf::test::lists_column_wrapper; + auto input = + LCWS({LCWS{"The", "quick", "brown", "fox", "jumpéd", "over", "the", "lazy", "brown", "dog."}, + LCWS{"The", "quick", "brown", "fox", "jumpéd", "over", "the", "lazy", "", "dog."}, + LCWS{"short", "row"}}); + + auto view = cudf::lists_column_view(input); + + auto first = thrust::counting_iterator(10); + auto params = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results = + nvtext::minhash_ngrams(view, 4, 0, cudf::column_view(params), cudf::column_view(params)); + using LCW32 = cudf::test::lists_column_wrapper; + // clang-format off + LCW32 expected({ + LCW32{ 230924604u, 55492793u, 963436400u}, + LCW32{ 230924604u, 367515795u, 963436400u}, + LCW32{2380648568u, 1330223236u, 279797904u} + }); + // clang-format on + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + auto params64 = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results64 = + nvtext::minhash64_ngrams(view, 4, 0, cudf::column_view(params64), cudf::column_view(params64)); + using LCW64 = cudf::test::lists_column_wrapper; + // clang-format off + LCW64 expected64({ + LCW64{ 208926840193078200ul, 576399628675212695ul, 312927673584437419ul}, + LCW64{ 677038498284219393ul, 326338087730412201ul, 298455901014050223ul}, + LCW64{1493265692486268500ul, 720255058049417768ul, 2253087432826260995ul} + }); + // clang-format on + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); +} + +TEST_F(MinHashTest, NgramsWide) +{ + auto many = std::vector(1024, "hello"); + auto str_data = cudf::test::strings_column_wrapper(many.begin(), many.end()); + auto offsets = + cudf::test::fixed_width_column_wrapper({0ul, many.size() / 2, many.size()}); + auto input = cudf::make_lists_column(2, offsets.release(), str_data.release(), 0, {}); + + auto view = cudf::lists_column_view(input->view()); + + auto first = thrust::counting_iterator(10); + auto params = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results = + nvtext::minhash_ngrams(view, 4, 0, cudf::column_view(params), cudf::column_view(params)); + using LCW32 = cudf::test::lists_column_wrapper; + // clang-format off + LCW32 expected({ + LCW32{ 571536396u, 2346676954u, 4121817512u}, + LCW32{ 571536396u, 2346676954u, 4121817512u} + }); + // clang-format on + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + auto params64 = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results64 = + nvtext::minhash64_ngrams(view, 4, 0, cudf::column_view(params64), cudf::column_view(params64)); + using LCW64 = cudf::test::lists_column_wrapper; + // clang-format off + LCW64 expected64({ + LCW64{ 1947142336021414174ul, 1219519365938078011ul, 491896395854741840ul}, + LCW64{ 1947142336021414174ul, 1219519365938078011ul, 491896395854741840ul} + }); + // clang-format on + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); +} + +TEST_F(MinHashTest, NgramsSliced) +{ + using LCWS = cudf::test::lists_column_wrapper; + auto input = + LCWS({LCWS{"ignored", "row"}, + LCWS{"The", "quick", "brown", "fox", "jumpéd", "over", "the", "lazy", "brown", "dog."}, + LCWS{"The", "quick", "brown", "fox", "jumpéd", "over", "the", "lazy", "", "dog."}, + LCWS{"short", "row"}, + LCWS{"ignored", "row"}}); + + auto view = cudf::lists_column_view(cudf::slice(input, {1, 4}).front()); + auto first = thrust::counting_iterator(10); + + auto params = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results = + nvtext::minhash_ngrams(view, 4, 0, cudf::column_view(params), cudf::column_view(params)); + + using LCW32 = cudf::test::lists_column_wrapper; + // clang-format off + LCW32 expected({ + LCW32{ 230924604u, 55492793u, 963436400u}, + LCW32{ 230924604u, 367515795u, 963436400u}, + LCW32{2380648568u, 1330223236u, 279797904u} + }); + // clang-format on + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results, expected); + + auto params64 = cudf::test::fixed_width_column_wrapper(first, first + 3); + auto results64 = + nvtext::minhash64_ngrams(view, 4, 0, cudf::column_view(params64), cudf::column_view(params64)); + using LCW64 = cudf::test::lists_column_wrapper; + // clang-format off + LCW64 expected64({ + LCW64{ 208926840193078200ul, 576399628675212695ul, 312927673584437419ul}, + LCW64{ 677038498284219393ul, 326338087730412201ul, 298455901014050223ul}, + LCW64{1493265692486268500ul, 720255058049417768ul, 2253087432826260995ul} + }); + // clang-format on + CUDF_TEST_EXPECT_COLUMNS_EQUAL(*results64, expected64); } diff --git a/python/cudf/cudf/core/column/string.py b/python/cudf/cudf/core/column/string.py index 944f5cd6d26..b82ec1958fb 100644 --- a/python/cudf/cudf/core/column/string.py +++ b/python/cudf/cudf/core/column/string.py @@ -5533,6 +5533,120 @@ def minhash64( self._column.minhash64(seed, a_column, b_column, width) # type: ignore[arg-type] ) + def minhash_ngrams( + self, ngrams: int, seed: np.uint32, a: ColumnLike, b: ColumnLike + ) -> SeriesOrIndex: + """ + Compute the minhash of a list column of strings. + + This uses the MurmurHash3_x86_32 algorithm for the hash function. + + Calculation uses the formula (hv * a + b) % mersenne_prime + where hv is the hash of a ngrams of strings within each row, + a and b are provided values and mersenne_prime is 2^61-1. + + Parameters + ---------- + ngrams : int + Number of strings to hash within each row. + seed : uint32 + The seed used for the hash algorithm. + a : ColumnLike + Values for minhash calculation. + Must be of type uint32. + b : ColumnLike + Values for minhash calculation. + Must be of type uint32. + + Examples + -------- + >>> import cudf + >>> import numpy as np + >>> s = cudf.Series([['this', 'is', 'my'], ['favorite', 'book']]) + >>> a = cudf.Series([1, 2, 3], dtype=np.uint32) + >>> b = cudf.Series([4, 5, 6], dtype=np.uint32) + >>> s.str.minhash_ngrams(ngrams=2, seed=0, a=a, b=b) + 0 [416367551, 832735099, 1249102647] + 1 [1906668704, 3813337405, 1425038810] + dtype: list + """ + a_column = column.as_column(a) + if a_column.dtype != np.uint32: + raise ValueError( + f"Expecting a Series with dtype uint32, got {type(a)}" + ) + b_column = column.as_column(b) + if b_column.dtype != np.uint32: + raise ValueError( + f"Expecting a Series with dtype uint32, got {type(b)}" + ) + plc_column = plc.nvtext.minhash.minhash_ngrams( + self._column.to_pylibcudf(mode="read"), + ngrams, + seed, + a._column.to_pylibcudf(mode="read"), + b._column.to_pylibcudf(mode="read"), + ) + result = ColumnBase.from_pylibcudf(plc_column) + return self._return_or_inplace(result) + + def minhash64_ngrams( + self, ngrams: int, seed: np.uint64, a: ColumnLike, b: ColumnLike + ) -> SeriesOrIndex: + """ + Compute the minhash of a list column of strings. + + This uses the MurmurHash3_x64_128 algorithm for the hash function. + + Calculation uses the formula (hv * a + b) % mersenne_prime + where hv is the hash of a ngrams of strings within each row, + a and b are provided values and mersenne_prime is 2^61-1. + + Parameters + ---------- + ngrams : int + Number of strings to hash within each row. + seed : uint64 + The seed used for the hash algorithm. + a : ColumnLike + Values for minhash calculation. + Must be of type uint64. + b : ColumnLike + Values for minhash calculation. + Must be of type uint64. + + Examples + -------- + >>> import cudf + >>> import numpy as np + >>> s = cudf.Series([['this', 'is', 'my'], ['favorite', 'book']]) + >>> a = cudf.Series([2, 3], dtype=np.uint64) + >>> b = cudf.Series([5, 6], dtype=np.uint64) + >>> s.str.minhash64_ngrams(ngrams=2, seed=0, a=a, b=b) + 0 [1304293339825194559, 1956440009737791829] + 1 [472203876238918632, 1861227318965224922] + dtype: list + """ + a_column = column.as_column(a) + if a_column.dtype != np.uint64: + raise ValueError( + f"Expecting a Series with dtype uint64, got {type(a)}" + ) + b_column = column.as_column(b) + if b_column.dtype != np.uint64: + raise ValueError( + f"Expecting a Series with dtype uint64, got {type(b)}" + ) + plc_column = plc.nvtext.minhash.minhash64_ngrams( + self._column.to_pylibcudf(mode="read"), + ngrams, + seed, + a._column.to_pylibcudf(mode="read"), + b._column.to_pylibcudf(mode="read"), + ) + result = ColumnBase.from_pylibcudf(plc_column) + return self._return_or_inplace(result) + def jaccard_index(self, input: cudf.Series, width: int) -> SeriesOrIndex: """ Compute the Jaccard index between this column and the given diff --git a/python/cudf/cudf/tests/text/test_text_methods.py b/python/cudf/cudf/tests/text/test_text_methods.py index dc45827d2e8..47b41bd1e39 100644 --- a/python/cudf/cudf/tests/text/test_text_methods.py +++ b/python/cudf/cudf/tests/text/test_text_methods.py @@ -930,6 +930,48 @@ def test_minhash(): strings.str.minhash64(1, a=params, b=params, width=8) +def test_minhash_ngrams(): + strings = cudf.Series( + [["this", "is", "my"], ["favorite", "book", "today"]] + ) + + params = cudf.Series([1, 2, 3], dtype=np.uint32) + expected = cudf.Series( + [ + cudf.Series([416367548, 832735096, 1249102644], dtype=np.uint32), + cudf.Series([1408797893, 2817595786, 4226393679], dtype=np.uint32), + ] + ) + actual = strings.str.minhash_ngrams(ngrams=2, seed=0, a=params, b=params) + assert_eq(expected, actual) + + params = cudf.Series([1, 2, 3], dtype=np.uint64) + expected = cudf.Series( + [ + cudf.Series( + [652146669912597278, 1304293339825194556, 1956440009737791826], + dtype=np.uint64, + ), + cudf.Series( + [1776622609581023632, 1247402209948353305, 718181810315682986], + dtype=np.uint64, + ), + ] + ) + actual = strings.str.minhash64_ngrams(ngrams=2, seed=0, a=params, b=params) + assert_eq(expected, actual) + + # test wrong input types + with pytest.raises(ValueError): + strings.str.minhash_ngrams(ngrams=7, seed=1, a="a", b="b") + with pytest.raises(ValueError): + params = cudf.Series([0, 1, 2], dtype=np.int32) + strings.str.minhash_ngrams(ngrams=6, seed=1, a=params, b=params) + with pytest.raises(ValueError): + params = cudf.Series([0, 1, 2], dtype=np.uint32) + strings.str.minhash64_ngrams(ngrams=8, seed=1, a=params, b=params) + + def test_jaccard_index(): str1 = cudf.Series(["the brown dog", "jumped about"]) str2 = cudf.Series(["the black cat", "jumped around"]) diff --git a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd index 9d1e8cba425..bfbb99e8eb0 100644 --- a/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd +++ b/python/pylibcudf/pylibcudf/libcudf/nvtext/minhash.pxd @@ -1,4 +1,4 @@ -# Copyright (c) 2023-2024, NVIDIA CORPORATION. +# Copyright (c) 2023-2025, NVIDIA CORPORATION. from libc.stdint cimport uint32_t, uint64_t from libcpp.memory cimport unique_ptr from pylibcudf.exception_handler cimport libcudf_exception_handler @@ -25,3 +25,19 @@ cdef extern from "nvtext/minhash.hpp" namespace "nvtext" nogil: const column_view &b, const size_type width, ) except + + + cdef unique_ptr[column] minhash_ngrams( + const column_view &strings, + const size_type ngrams, + const uint32_t seed, + const column_view &a, + const column_view &b, + ) except + + + cdef unique_ptr[column] minhash64_ngrams( + const column_view &strings, + const size_type ngrams, + const uint64_t seed, + const column_view &a, + const column_view &b, + ) except + diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pxd b/python/pylibcudf/pylibcudf/nvtext/minhash.pxd index 0af53748cdc..f1e099ca7da 100644 --- a/python/pylibcudf/pylibcudf/nvtext/minhash.pxd +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pxd @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. from libc.stdint cimport uint32_t, uint64_t from pylibcudf.column cimport Column @@ -24,3 +24,19 @@ cpdef Column minhash64( Column b, size_type width ) + +cpdef Column minhash_ngrams( + Column input, + size_type width, + uint32_t seed, + Column a, + Column b +) + +cpdef Column minhash64_ngrams( + Column input, + size_type width, + uint64_t seed, + Column a, + Column b +) diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pyi b/python/pylibcudf/pylibcudf/nvtext/minhash.pyi index 5d88cfbbea0..bb50a150798 100644 --- a/python/pylibcudf/pylibcudf/nvtext/minhash.pyi +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pyi @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. from pylibcudf.column import Column @@ -8,3 +8,9 @@ def minhash( def minhash64( input: Column, seed: int, a: Column, b: Column, width: int ) -> Column: ... +def minhash_ngrams( + input: Column, ngrams: int, seed: int, a: Column, b: Column +) -> Column: ... +def minhash64_ngrams( + input: Column, ngrams: int, seed: int, a: Column, b: Column +) -> Column: ... diff --git a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx index 84811cda867..cdc4a4f3ac8 100644 --- a/python/pylibcudf/pylibcudf/nvtext/minhash.pyx +++ b/python/pylibcudf/pylibcudf/nvtext/minhash.pyx @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. from libc.stdint cimport uint32_t, uint64_t from libcpp.memory cimport unique_ptr @@ -8,12 +8,16 @@ from pylibcudf.libcudf.column.column cimport column from pylibcudf.libcudf.nvtext.minhash cimport ( minhash as cpp_minhash, minhash64 as cpp_minhash64, + minhash_ngrams as cpp_minhash_ngrams, + minhash64_ngrams as cpp_minhash64_ngrams, ) from pylibcudf.libcudf.types cimport size_type __all__ = [ "minhash", "minhash64", + "minhash_ngrams", + "minhash64_ngrams", ] cpdef Column minhash( @@ -103,3 +107,93 @@ cpdef Column minhash64( ) return Column.from_libcudf(move(c_result)) + +cpdef Column minhash_ngrams( + Column input, + size_type ngrams, + uint32_t seed, + Column a, + Column b +): + """ + Returns the minhash values for each input row of strings. + This function uses MurmurHash3_x86_32 for the hash algorithm. + + For details, see :cpp:func:`minhash_ngrams`. + + Parameters + ---------- + input : Column + List column of strings to compute minhash + ngrams : size_type + Number of consecutive strings to hash in each row + seed : uint32_t + Seed used for the hash function + a : Column + 1st parameter value used for the minhash algorithm. + b : Column + 2nd parameter value used for the minhash algorithm. + + Returns + ------- + Column + List column of minhash values for each row per + value in columns a and b. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_minhash_ngrams( + input.view(), + ngrams, + seed, + a.view(), + b.view() + ) + + return Column.from_libcudf(move(c_result)) + +cpdef Column minhash64_ngrams( + Column input, + size_type ngrams, + uint64_t seed, + Column a, + Column b +): + """ + Returns the minhash values for each input row of strings. + This function uses MurmurHash3_x64_128 for the hash algorithm. + + For details, see :cpp:func:`minhash64_ngrams`. + + Parameters + ---------- + input : Column + Strings column to compute minhash + ngrams : size_type + Number of consecutive strings to hash in each row + seed : uint64_t + Seed used for the hash function + a : Column + 1st parameter value used for the minhash algorithm. + b : Column + 2nd parameter value used for the minhash algorithm. + + Returns + ------- + Column + List column of minhash values for each row per + value in columns a and b. + """ + cdef unique_ptr[column] c_result + + with nogil: + c_result = cpp_minhash64_ngrams( + input.view(), + ngrams, + seed, + a.view(), + b.view() + ) + + return Column.from_libcudf(move(c_result)) diff --git a/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py index ad7a6f7a762..ff8545f0617 100644 --- a/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py +++ b/python/pylibcudf/pylibcudf/tests/test_nvtext_minhash.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024, NVIDIA CORPORATION. +# Copyright (c) 2024-2025, NVIDIA CORPORATION. import pyarrow as pa import pytest @@ -33,3 +33,49 @@ def test_minhash(minhash_input_data, width): assert pa_result.type == pa.list_( pa.field("element", seed_type, nullable=False) ) + + +@pytest.fixture(scope="module", params=[pa.uint32(), pa.uint64()]) +def minhash_ngrams_input_data(request): + input_arr = pa.array( + [ + ["foo", "bar", "foo foo", "bar bar", "foo bar", "bar foo"], + [ + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + ], + ] + ) + ab = pa.array([2, 3, 4, 5], request.param) + return input_arr, ab, request.param + + +@pytest.mark.parametrize("ngrams", [5, 10]) +def test_minhash_ngrams(minhash_ngrams_input_data, ngrams): + input_arr, ab, seed_type = minhash_ngrams_input_data + minhash_func = ( + plc.nvtext.minhash.minhash_ngrams + if seed_type == pa.uint32() + else plc.nvtext.minhash.minhash64_ngrams + ) + result = minhash_func( + plc.interop.from_arrow(input_arr), + ngrams, + 0, + plc.interop.from_arrow(ab), + plc.interop.from_arrow(ab), + ) + pa_result = plc.interop.to_arrow(result) + assert all(len(got) == len(ab) for got, s in zip(pa_result, input_arr)) + assert pa_result.type == pa.list_( + pa.field("element", seed_type, nullable=False) + ) From cf8938bc6b11de35337f6d4a04c73559420f3f4b Mon Sep 17 00:00:00 2001 From: Matthew Murray <41342305+Matt711@users.noreply.github.com> Date: Thu, 27 Feb 2025 16:42:46 -0500 Subject: [PATCH 45/45] Add a list of expected failures to narwhals tests (#18097) ## Description Adds an xfail list to the narwhals tests we run using cudf. Note: We can update/replace the dict when running Narwhals with cudf.pandas. xref #18031 ## Checklist - [ ] I am familiar with the [Contributing Guidelines](https://github.com/rapidsai/cudf/blob/HEAD/CONTRIBUTING.md). - [ ] New or existing tests cover these changes. - [ ] The documentation is up to date with these changes. --------- Co-authored-by: Vyas Ramasubramani Co-authored-by: GALI PREM SAGAR Co-authored-by: Bradley Dice --- ci/test_narwhals.sh | 1 + docs/cudf/source/conf.py | 1 + python/cudf/cudf/testing/__init__.py | 3 ++- .../cudf/cudf/testing/narwhals_test_plugin.py | 25 +++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 python/cudf/cudf/testing/narwhals_test_plugin.py diff --git a/ci/test_narwhals.sh b/ci/test_narwhals.sh index 4a32ff0b0fd..28eceff2f80 100755 --- a/ci/test_narwhals.sh +++ b/ci/test_narwhals.sh @@ -26,6 +26,7 @@ rapids-logger "Run narwhals tests for cuDF" python -m pytest \ --cache-clear \ --junitxml="${RAPIDS_TESTS_DIR}/junit-cudf-narwhals.xml" \ + -p cudf.testing.narwhals_test_plugin \ --numprocesses=8 \ --dist=worksteal \ --constructors=cudf diff --git a/docs/cudf/source/conf.py b/docs/cudf/source/conf.py index 8eea644363b..92b37c4b3f2 100644 --- a/docs/cudf/source/conf.py +++ b/docs/cudf/source/conf.py @@ -207,6 +207,7 @@ def clean_all_xml_files(path): exclude_patterns = [ "venv", "**/includes/**", + "narwhals_test_plugin", ] # The name of the Pygments (syntax highlighting) style to use. diff --git a/python/cudf/cudf/testing/__init__.py b/python/cudf/cudf/testing/__init__.py index 4e92b43b9f9..a4afa54f754 100644 --- a/python/cudf/cudf/testing/__init__.py +++ b/python/cudf/cudf/testing/__init__.py @@ -1,5 +1,6 @@ -# Copyright (c) 2020-2024, NVIDIA CORPORATION. +# Copyright (c) 2020-2025, NVIDIA CORPORATION. +from cudf.testing import narwhals_test_plugin from cudf.testing.testing import ( assert_eq, assert_frame_equal, diff --git a/python/cudf/cudf/testing/narwhals_test_plugin.py b/python/cudf/cudf/testing/narwhals_test_plugin.py new file mode 100644 index 00000000000..d794bd0120a --- /dev/null +++ b/python/cudf/cudf/testing/narwhals_test_plugin.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +"""Plugin for running narwhals test suite with cudf.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Mapping + +EXPECTED_FAILURES: Mapping[str, str] = { + "tests/frame/select_test.py::test_select_duplicates[cudf]": "cuDF doesn't support having multiple columns with same names", +} + + +def pytest_collection_modifyitems(session, config, items) -> None: + """Mark known failing tests.""" + import pytest + + for item in items: + if item.nodeid in EXPECTED_FAILURES: + exp_val = EXPECTED_FAILURES[item.nodeid] + item.add_marker(pytest.mark.xfail(reason=exp_val))