-
Notifications
You must be signed in to change notification settings - Fork 1.1k
io: zero parquet buffers that can hold inherited nulls #23777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
de19fb0
545c497
0c65811
79ce433
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,9 +16,11 @@ | |
| #include <cudf_test/table_utilities.hpp> | ||
|
|
||
| #include <cudf/column/column.hpp> | ||
| #include <cudf/column/column_factories.hpp> | ||
| #include <cudf/copying.hpp> | ||
| #include <cudf/io/parquet.hpp> | ||
| #include <cudf/io/parquet_metadata.hpp> | ||
| #include <cudf/null_mask.hpp> | ||
| #include <cudf/reshape.hpp> | ||
| #include <cudf/stream_compaction.hpp> | ||
| #include <cudf/table/table.hpp> | ||
|
|
@@ -36,6 +38,7 @@ | |
| #include <cstring> | ||
| #include <limits> | ||
| #include <memory> | ||
| #include <numeric> | ||
| #include <optional> | ||
| #include <stdexcept> | ||
| #include <utility> | ||
|
|
@@ -1759,6 +1762,154 @@ TEST_F(ParquetReaderTest, StructByteArray) | |
| CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); | ||
| } | ||
|
|
||
| TEST_F(ParquetReaderTest, RequiredBinaryUnderNullStruct) | ||
| { | ||
| // A required BYTE_ARRAY leaf under an optional struct has no leaf validity of its own; rows | ||
| // where the struct is null are absent entirely. The reader must not leave the corresponding | ||
| // string length slots uninitialized before scanning them into offsets. | ||
| constexpr auto num_rows = 10; | ||
| constexpr auto null_row = 5; | ||
| constexpr auto row_chars = 4; | ||
|
|
||
| std::vector<uint8_t> payload_values(num_rows * row_chars); | ||
| for (cudf::size_type row = 0; row < num_rows; ++row) { | ||
| std::fill_n( | ||
| payload_values.data() + row * row_chars, row_chars, static_cast<uint8_t>('a' + row % 26)); | ||
| } | ||
| auto const offsets_iter = cudf::detail::make_counting_transform_iterator( | ||
| 0, [](cudf::size_type i) { return i * row_chars; }); | ||
| cudf::test::fixed_width_column_wrapper<cudf::size_type> offsets(offsets_iter, | ||
| offsets_iter + num_rows + 1); | ||
| auto payload_values_col = | ||
| cudf::test::fixed_width_column_wrapper<uint8_t>(payload_values.begin(), payload_values.end()); | ||
| auto payload = | ||
| cudf::make_lists_column(num_rows, offsets.release(), payload_values_col.release(), 0, {}); | ||
| // the struct wrapper superimposes parent nulls onto children, so build the hierarchy directly | ||
| // to keep the payload child non-nullable, which is what makes it a required leaf in parquet | ||
| auto struct_mask = cudf::create_null_mask(num_rows, cudf::mask_state::ALL_VALID); | ||
| cudf::set_null_mask( | ||
| static_cast<cudf::bitmask_type*>(struct_mask.data()), null_row, null_row + 1, false); | ||
|
|
||
| std::vector<std::unique_ptr<cudf::column>> write_children; | ||
| write_children.push_back(std::move(payload)); | ||
| auto write_struct = | ||
| cudf::create_structs_hierarchy(num_rows, std::move(write_children), 1, std::move(struct_mask)); | ||
| auto const write_table = table_view{{write_struct->view()}}; | ||
|
|
||
| cudf::io::table_input_metadata output_metadata(write_table); | ||
| output_metadata.column_metadata[0] | ||
| .set_name("s") | ||
| .child(0) | ||
| .set_name("payload") | ||
| .set_nullability(false) | ||
| .set_output_as_binary(true); | ||
|
|
||
| auto filepath = temp_env->get_temp_filepath("RequiredBinaryUnderNullStruct.parquet"); | ||
| cudf::io::parquet_writer_options out_opts = | ||
| cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, write_table) | ||
| .metadata(std::move(output_metadata)) | ||
| .dictionary_policy(cudf::io::dictionary_policy::NEVER) | ||
| .compression(cudf::io::compression_type::NONE); | ||
| cudf::io::write_parquet(out_opts); | ||
|
|
||
| // the leaf itself is required so it carries no validity of its own; only the struct is null | ||
| std::vector<std::string> expected_values(num_rows); | ||
| for (cudf::size_type row = 0; row < num_rows; ++row) { | ||
| expected_values[row] = std::string(row_chars, static_cast<char>('a' + row % 26)); | ||
| } | ||
| expected_values[null_row].clear(); | ||
| auto expected_strings = | ||
| cudf::test::strings_column_wrapper{expected_values.begin(), expected_values.end()}; | ||
|
|
||
| auto expected_mask = cudf::create_null_mask(num_rows, cudf::mask_state::ALL_VALID); | ||
| cudf::set_null_mask( | ||
| static_cast<cudf::bitmask_type*>(expected_mask.data()), null_row, null_row + 1, false); | ||
| std::vector<std::unique_ptr<cudf::column>> expected_children; | ||
| expected_children.push_back(expected_strings.release()); | ||
| auto expected_struct = cudf::create_structs_hierarchy( | ||
| num_rows, std::move(expected_children), 1, std::move(expected_mask)); | ||
| auto const expected = table_view{{expected_struct->view()}}; | ||
|
|
||
| cudf::io::parquet_reader_options in_opts = | ||
| cudf::io::parquet_reader_options::builder(cudf::io::source_info{filepath}); | ||
| for (int read = 0; read < 2; ++read) { | ||
| // churn the pool with garbage so an unwritten slot holds stale bytes instead of a fresh | ||
| // zeroed page, which would let the test pass without the fix | ||
| for (auto size : {size_t{64}, size_t{4096}, size_t{1} << 16}) { | ||
| void* poison = nullptr; | ||
| CUDF_CUDA_TRY(cudaMalloc(&poison, size)); | ||
| CUDF_CUDA_TRY(cudaMemset(poison, 0xEE, size)); | ||
| CUDF_CUDA_TRY(cudaFree(poison)); | ||
| } | ||
|
Comment on lines
+1836
to
+1843
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target test sections ---'
sed -n '1780,1935p' cpp/tests/io/parquet_reader_test.cpp
printf '%s\n' '--- test utility resource helpers ---'
sed -n '1,150p' cpp/tests/io/io_test_utils.hpp
printf '%s\n' '--- relevant resource and parquet test setup references ---'
rg -n -S \
'get_current_device_resource|mr::|memory_resource|set_current_device_resource|cudaMalloc|cudaMallocAsync|cudaMemset|read_parquet|CUDF_TEST_EXPECT_TABLES_EQUAL' \
cpp/tests/io/parquet_reader_test.cpp cpp/tests/io/io_test_utils.hppRepository: NVIDIA/cudf Length of output: 43561 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- fixture definition and setup ---'
rg -n -S -A35 -B15 \
'class ParquetReaderTest|struct ParquetReaderTest|TEST_F\(ParquetReaderTest|temp_env|set_current_device_resource|mr::cuda_async_memory_resource|cuda_async_memory_resource|pool_memory_resource|cuda_pool_memory_resource|managed_memory_resource' \
cpp/tests/io/parquet_reader_test.cpp
printf '%s\n' '--- resource APIs and test fixture helpers ---'
rg -n -S \
'set_current_device_resource|get_current_device_resource_ref|cuda_async_memory_resource|pool_memory_resource|rmm::mr::.*resource|memory_resource' \
cpp/tests cpp/include cpp/src \
-g '*.cpp' -g '*.cu' -g '*.hpp' -g '*.h' | head -300
printf '%s\n' '--- relevant includes ---'
sed -n '1,120p' cpp/tests/io/parquet_reader_test.cppRepository: NVIDIA/cudf Length of output: 50368 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exact fixture declaration ---'
rg -n -S -A45 -B10 \
'struct ParquetReaderTest|class ParquetReaderTest' \
cpp/tests/io/parquet_reader_test.cpp
printf '%s\n' '--- BaseFixture and temporary-environment definitions ---'
rg -n -S \
'struct BaseFixture|class BaseFixture|temp_env|TempEnv|temp_env->' \
cpp/tests \
-g '*.hpp' -g '*.h' -g '*.cpp' | head -160
printf '%s\n' '--- targeted resource references in the fixture file ---'
rg -n -S \
'set_current_device_resource|get_current_device_resource_ref|current_device_resource|memory_resource|cuda_async|pool_memory' \
cpp/tests/io/parquet_reader_test.cppRepository: NVIDIA/cudf Length of output: 183 🏁 Script executed: #!/bin/bash
set +e
printf '%s\n' '--- all fixture-name references ---'
rg -n -S 'ParquetReaderTest|temp_env' cpp/tests/io/parquet_reader_test.cpp cpp/tests/io cpp/tests \
-g '*.cpp' -g '*.hpp' -g '*.h' | head -200
printf '%s\n' '--- file header ---'
sed -n '1,180p' cpp/tests/io/parquet_reader_test.cpp
printf '%s\n' '--- BaseFixture declarations ---'
rg -n -S -A50 -B10 'BaseFixture' cpp/tests -g '*.hpp' -g '*.h' -g '*.cpp' | head -240Repository: NVIDIA/cudf Length of output: 50367 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- parquet common fixture ---'
rg -n -S -A45 -B15 \
'ParquetReaderTest|DecompressionTest' \
cpp/tests/io/parquet_common.hpp cpp/tests/io/parquet_common.cpp
printf '%s\n' '--- BaseFixture source locations ---'
rg -l -S 'class BaseFixture|struct BaseFixture' cpp | head -30Repository: NVIDIA/cudf Length of output: 5454 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- BaseFixture definition ---'
sed -n '1,220p' cpp/include/cudf_test/base_fixture.hpp
printf '%s\n' '--- test resource setup in the repository ---'
rg -n -S \
'set_current_device_resource|set_current_device_resource_ref|cuda_async_memory_resource|pool_memory_resource|cuda_memory_resource|temporary_device_resource|BaseFixture::' \
cpp/include cpp/tests \
-g '*.hpp' -g '*.h' -g '*.cpp' -g '*.cu' | head -240Repository: NVIDIA/cudf Length of output: 7698 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- test-process memory-resource setup ---'
sed -n '1,235p' cpp/include/cudf_test/testing_main.hpp
printf '%s\n' '--- current-resource implementation and default resource ---'
sed -n '90,175p' cpp/include/cudf/utilities/memory_resource.hppRepository: NVIDIA/cudf Length of output: 12170 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
parquet = Path("cpp/tests/io/parquet_reader_test.cpp").read_text()
common = Path("cpp/tests/io/parquet_common.hpp").read_text()
fixture = Path("cpp/include/cudf_test/base_fixture.hpp").read_text()
main = Path("cpp/include/cudf_test/testing_main.hpp").read_text()
assert "struct ParquetReaderTest : public cudf::test::BaseFixture {};" in common
assert "_mr{cudf::get_current_device_resource_ref()}" in fixture
assert "default_rmm_mode = env_rmm_mode ? env_rmm_mode : \"pool\"" in main
for mode in ("make_cuda()", "make_async()", "make_pool()", "make_arena()", "make_binning()", "make_managed()"):
assert mode in main
assert parquet.count("CUDF_CUDA_TRY(cudaMalloc(&poison, size));") == 2
assert parquet.count("CUDF_CUDA_TRY(cudaMemset(poison, 0xEE, size));") == 2
print("ParquetReaderTest captures the current resource without replacing it.")
print("The test default is pool, and the test harness supports async and other resource modes.")
print("Both target tests poison memory through direct cudaMalloc/cudaFree.")
print("The allocator-poisoning concern remains applicable to the target tests.")
PYRepository: NVIDIA/cudf Length of output: 447 Use the active RMM resource to poison reader buffers.
🤖 Prompt for AI AgentsSource: MCP tools |
||
| auto result = cudf::io::read_parquet(in_opts); | ||
| CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); | ||
| } | ||
| } | ||
|
|
||
| TEST_F(ParquetReaderTest, RequiredIntUnderNullStruct) | ||
| { | ||
| // same shape as RequiredBinaryUnderNullStruct with a fixed-width leaf: the value slot of an | ||
| // inherited-null row is never written by decode, so the buffer must come up zeroed | ||
| constexpr auto num_rows = 10; | ||
| constexpr auto null_row = 5; | ||
|
|
||
| auto const value_iter = cudf::detail::make_counting_transform_iterator( | ||
| 0, [](cudf::size_type i) { return static_cast<int32_t>(i + 1); }); | ||
| auto values = cudf::test::fixed_width_column_wrapper<int32_t>(value_iter, value_iter + num_rows); | ||
|
|
||
| auto struct_mask = cudf::create_null_mask(num_rows, cudf::mask_state::ALL_VALID); | ||
| cudf::set_null_mask( | ||
| static_cast<cudf::bitmask_type*>(struct_mask.data()), null_row, null_row + 1, false); | ||
| std::vector<std::unique_ptr<cudf::column>> write_children; | ||
| write_children.push_back(values.release()); | ||
| auto write_struct = | ||
| cudf::create_structs_hierarchy(num_rows, std::move(write_children), 1, std::move(struct_mask)); | ||
| auto const write_table = table_view{{write_struct->view()}}; | ||
|
|
||
| cudf::io::table_input_metadata output_metadata(write_table); | ||
| output_metadata.column_metadata[0].set_name("s").child(0).set_name("n").set_nullability(false); | ||
|
|
||
| auto filepath = temp_env->get_temp_filepath("RequiredIntUnderNullStruct.parquet"); | ||
| cudf::io::parquet_writer_options out_opts = | ||
| cudf::io::parquet_writer_options::builder(cudf::io::sink_info{filepath}, write_table) | ||
| .metadata(std::move(output_metadata)) | ||
| .dictionary_policy(cudf::io::dictionary_policy::NEVER) | ||
| .compression(cudf::io::compression_type::NONE); | ||
| cudf::io::write_parquet(out_opts); | ||
|
|
||
| // the leaf is required so the read-back child carries no validity; its slot at the | ||
| // inherited-null row is zero | ||
| std::vector<int32_t> expected_values(num_rows); | ||
| std::iota(expected_values.begin(), expected_values.end(), 1); | ||
| expected_values[null_row] = 0; | ||
| auto expected_values_col = | ||
| cudf::test::fixed_width_column_wrapper<int32_t>(expected_values.begin(), expected_values.end()); | ||
|
|
||
| auto expected_mask = cudf::create_null_mask(num_rows, cudf::mask_state::ALL_VALID); | ||
| cudf::set_null_mask( | ||
| static_cast<cudf::bitmask_type*>(expected_mask.data()), null_row, null_row + 1, false); | ||
| std::vector<std::unique_ptr<cudf::column>> expected_children; | ||
| expected_children.push_back(expected_values_col.release()); | ||
| auto expected_struct = cudf::create_structs_hierarchy( | ||
| num_rows, std::move(expected_children), 1, std::move(expected_mask)); | ||
| auto const expected = table_view{{expected_struct->view()}}; | ||
|
|
||
| cudf::io::parquet_reader_options in_opts = | ||
| cudf::io::parquet_reader_options::builder(cudf::io::source_info{filepath}); | ||
| for (int read = 0; read < 2; ++read) { | ||
| // churn the pool with garbage so an unwritten slot holds stale bytes instead of a fresh | ||
| // zeroed page, which would let the test pass without the fix | ||
| for (auto size : {size_t{64}, size_t{4096}, size_t{1} << 16}) { | ||
| void* poison = nullptr; | ||
| CUDF_CUDA_TRY(cudaMalloc(&poison, size)); | ||
| CUDF_CUDA_TRY(cudaMemset(poison, 0xEE, size)); | ||
| CUDF_CUDA_TRY(cudaFree(poison)); | ||
| } | ||
| auto result = cudf::io::read_parquet(in_opts); | ||
| CUDF_TEST_EXPECT_TABLES_EQUAL(expected, result.tbl->view()); | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| TEST_F(ParquetReaderTest, NestingOptimizationTest) | ||
| { | ||
| // test nesting levels > cudf::io::parquet::detail::max_cacheable_nesting_decode_info deep. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.