This is an automated email from the ASF dual-hosted git repository.
zhouyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new 999a949984 [VL] feat(iceberg): Added dictionary size bytes write
config (#12434)
999a949984 is described below
commit 999a949984d700e27cb9f782f8725bc5fddcaa98
Author: inf <[email protected]>
AuthorDate: Sat Jul 4 09:29:08 2026 +0000
[VL] feat(iceberg): Added dictionary size bytes write config (#12434)
Added a new config:
spark.gluten.sql.columnar.backend.velox.parquet.dictionaryPageSizeBytes to
control the write.parquet.dict-size-bytes write confi
---
.../execution/AbstractIcebergWriteExec.scala | 5 +-
.../execution/enhanced/VeloxIcebergSuite.scala | 110 +++++++++++++++++++++
.../org/apache/gluten/config/VeloxConfig.scala | 6 ++
cpp/velox/config/VeloxConfig.h | 2 +
cpp/velox/utils/ConfigExtractor.cc | 2 +
docs/velox-configuration.md | 1 +
.../apache/gluten/execution/IcebergWriteExec.scala | 9 +-
7 files changed, 132 insertions(+), 3 deletions(-)
diff --git
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
index 2b4d50a73f..37e6f30937 100644
---
a/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
+++
b/backends-velox/src-iceberg/main/scala/org/apache/gluten/execution/AbstractIcebergWriteExec.scala
@@ -17,7 +17,7 @@
package org.apache.gluten.execution
import org.apache.gluten.IcebergNestedFieldVisitor
-import org.apache.gluten.config.VeloxConfig.{MAX_TARGET_FILE_SIZE_SESSION,
PARQUET_PAGE_SIZE_BYTES}
+import org.apache.gluten.config.VeloxConfig.{MAX_TARGET_FILE_SIZE_SESSION,
PARQUET_DICT_SIZE_BYTES, PARQUET_PAGE_SIZE_BYTES}
import org.apache.gluten.connector.write.{ColumnarBatchDataWriterFactory,
ColumnarStreamingDataWriterFactory, IcebergDataWriteFactory}
import org.apache.spark.sql.internal.SQLConf
@@ -48,7 +48,8 @@ abstract class AbstractIcebergWriteExec extends
IcebergWriteExec {
Seq(
PARQUET_PAGE_SIZE_BYTES.key -> getParquetPageSizeBytes,
- MAX_TARGET_FILE_SIZE_SESSION.key -> getTargetFileSizeBytes
+ MAX_TARGET_FILE_SIZE_SESSION.key -> getTargetFileSizeBytes,
+ PARQUET_DICT_SIZE_BYTES.key -> getDictSizeBytes
).foreach {
case (key, value) =>
if (SQLConf.get.getConfString(key, null) == null) {
diff --git
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
index 0cc6cddda2..de48549993 100644
---
a/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
+++
b/backends-velox/src-iceberg/test/scala/org/apache/gluten/execution/enhanced/VeloxIcebergSuite.scala
@@ -29,6 +29,8 @@ import org.apache.spark.sql.gluten.TestUtils
import org.apache.hadoop.fs.Path
import org.apache.iceberg.shaded.org.apache.parquet.ParquetReadOptions
+import org.apache.iceberg.shaded.org.apache.parquet.column.Encoding
+import org.apache.iceberg.shaded.org.apache.parquet.column.page.{DataPage,
DataPageV1, DataPageV2}
import org.apache.iceberg.shaded.org.apache.parquet.hadoop.ParquetFileReader
import org.apache.iceberg.shaded.org.apache.parquet.hadoop.util.HadoopInputFile
@@ -543,6 +545,114 @@ class VeloxIcebergSuite extends IcebergSuite {
}
}
}
+
+ test("iceberg parquet writer respects dictionary page size bytes") {
+ val table = "iceberg_dict_page_size_tbl"
+
+ def parquetFiles(table: String): Seq[String] = {
+ spark.sql(s"""
+ |SELECT file_path
+ |FROM default.$table.files
+ |""".stripMargin).collect().map(_.getString(0)).toSeq
+ }
+
+ def pageEncoding(page: DataPage): Encoding = {
+ page.accept(new DataPage.Visitor[Encoding] {
+ override def visit(dataPageV1: DataPageV1): Encoding =
dataPageV1.getValueEncoding
+ override def visit(dataPageV2: DataPageV2): Encoding =
dataPageV2.getDataEncoding
+ })
+ }
+
+ def dataPageEncodings(table: String, columnName: String): Seq[Encoding] = {
+ val conf = spark.sparkContext.hadoopConfiguration
+
+ parquetFiles(table).flatMap {
+ file =>
+ val inputFile = HadoopInputFile.fromPath(new Path(file), conf)
+ val reader = ParquetFileReader.open(inputFile,
ParquetReadOptions.builder().build())
+
+ try {
+ val column = reader
+ .getFooter
+ .getFileMetaData
+ .getSchema
+ .getColumns
+ .asScala
+ .find(_.getPath.toSeq == Seq(columnName))
+ .getOrElse {
+ fail(s"Column $columnName was not found in Parquet file $file")
+ }
+
+ val encodings =
scala.collection.mutable.ArrayBuffer.empty[Encoding]
+
+ var rowGroup = reader.readNextRowGroup()
+ while (rowGroup != null) {
+ val pageReader = rowGroup.getPageReader(column)
+ pageReader.readDictionaryPage()
+
+ var page = pageReader.readPage()
+ while (page != null) {
+ encodings += pageEncoding(page)
+ page = pageReader.readPage()
+ }
+
+ rowGroup = reader.readNextRowGroup()
+ }
+
+ encodings
+ } finally {
+ reader.close()
+ }
+ }
+ }
+
+ withSQLConf(
+ "spark.sql.shuffle.partitions" -> "1"
+ ) {
+ withTable(table) {
+ spark.sql(s"""
+ |CREATE TABLE $table (
+ | value SMALLINT
+ |) USING iceberg
+ |TBLPROPERTIES (
+ | 'write.format.default' = 'parquet',
+ | 'write.parquet.compression-codec' = 'uncompressed',
+ | 'write.parquet.dict-size-bytes' = '1B'
+ |)
+ |""".stripMargin)
+
+ val df = spark.sql(s"""
+ |INSERT INTO $table
+ |SELECT CAST(id + 1 AS SMALLINT)
+ |FROM range(0, 10000, 1, 1)
+ |""".stripMargin)
+
+ assert(
+ df.queryExecution.executedPlan
+ .asInstanceOf[CommandResultExec]
+ .commandPhysicalPlan
+ .isInstanceOf[VeloxIcebergAppendDataExec])
+
+ checkAnswer(
+ spark.sql(s"SELECT count(*) FROM $table"),
+ Seq(Row(10000L)))
+
+ val encodings = dataPageEncodings(table, "value")
+
+ assert(encodings.nonEmpty, "Expected at least one Parquet data page")
+ assert(
+ encodings.head == Encoding.RLE_DICTIONARY,
+ s"Expected the first data page to use dictionary encoding, " +
+ s"but got encodings=${encodings.mkString("[", ", ", "]")}"
+ )
+ assert(
+ encodings.contains(Encoding.PLAIN),
+ s"Expected write.parquet.dict-size-bytes=1B to make later data pages
fall back " +
+ s"to PLAIN, but got encodings=${encodings.mkString("[", ", ",
"]")}"
+ )
+ }
+ }
+ }
test("iceberg parquet writer default row group size test") {
val table = "iceberg_default_row_group_size"
val defaultRowGroupBytes = 128L * 1024 * 1024
diff --git
a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
index dacb835b8c..5864529058 100644
--- a/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
+++ b/backends-velox/src/main/scala/org/apache/gluten/config/VeloxConfig.scala
@@ -862,6 +862,12 @@ object VeloxConfig extends ConfigRegistry {
.bytesConf(ByteUnit.BYTE)
.createWithDefaultString("1MB")
+ val PARQUET_DICT_SIZE_BYTES =
+
buildConf("spark.gluten.sql.columnar.backend.velox.parquet.dictionaryPageSizeBytes")
+ .doc("The maximum size in bytes for a Parquet dictionary page")
+ .bytesConf(ByteUnit.BYTE)
+ .createWithDefaultString("2MB")
+
val ENABLE_TIMESTAMP_NTZ_VALIDATION =
buildConf("spark.gluten.sql.columnar.backend.velox.enableTimestampNtzValidation")
.doc(
diff --git a/cpp/velox/config/VeloxConfig.h b/cpp/velox/config/VeloxConfig.h
index f16f48c40d..5b5e129c34 100644
--- a/cpp/velox/config/VeloxConfig.h
+++ b/cpp/velox/config/VeloxConfig.h
@@ -191,6 +191,8 @@ const uint32_t kGlogSeverityLevelDefault = 1;
// Iceberg write configs
const std::string kWriteParquetPageSizeBytes =
"spark.gluten.sql.columnar.backend.velox.parquet.pageSizeBytes";
+const std::string kWriteParquetDictSizeBytes =
+ "spark.gluten.sql.columnar.backend.velox.parquet.dictionaryPageSizeBytes";
// Query trace
/// Enable query tracing flag.
diff --git a/cpp/velox/utils/ConfigExtractor.cc
b/cpp/velox/utils/ConfigExtractor.cc
index f398c2588d..ecb3d9e7ad 100644
--- a/cpp/velox/utils/ConfigExtractor.cc
+++ b/cpp/velox/utils/ConfigExtractor.cc
@@ -252,6 +252,8 @@ std::shared_ptr<facebook::velox::config::ConfigBase>
createHiveConnectorSessionC
conf->get<bool>(kOrcUseColumnNames, true) ? "true" : "false";
configs[parquetSessionProperty(facebook::velox::parquet::ParquetConfig::kWriterPageSizeSession)]
=
conf->get<std::string>(kWriteParquetPageSizeBytes, "1MB");
+
configs[parquetSessionProperty(facebook::velox::parquet::ParquetConfig::kWriterDictionaryPageSizeLimitSession)]
=
+ conf->get<std::string>(kWriteParquetDictSizeBytes, "2MB");
configs[parquetSessionProperty(facebook::velox::parquet::ParquetConfig::kNullStructIfAllFieldsMissingSession)]
=
"true";
diff --git a/docs/velox-configuration.md b/docs/velox-configuration.md
index 9772080887..5f7c49afc4 100644
--- a/docs/velox-configuration.md
+++ b/docs/velox-configuration.md
@@ -56,6 +56,7 @@ nav_order: 16
| spark.gluten.sql.columnar.backend.velox.memoryUseHugePages
| 🔄 Dynamic | false | Use explicit huge pages for Velox
memory allocation.
[...]
| spark.gluten.sql.columnar.backend.velox.orc.scan.enabled
| 🔄 Dynamic | true | Enable velox orc scan. If disabled,
vanilla spark orc scan will be used.
[...]
| spark.gluten.sql.columnar.backend.velox.orcUseColumnNames
| 🔄 Dynamic | true | Maps table field names to file field
names using names, not indices for ORC files.
[...]
+| spark.gluten.sql.columnar.backend.velox.parquet.dictionaryPageSizeBytes
| 🔄 Dynamic | 2MB | The maximum size in bytes for a
Parquet dictionary page
[...]
| spark.gluten.sql.columnar.backend.velox.parquet.pageSizeBytes
| 🔄 Dynamic | 1MB | The page size in bytes is for
compression.
[...]
| spark.gluten.sql.columnar.backend.velox.parquetMaxTargetFileSize
| 🔄 Dynamic | 0b | The target file size for each output
file when writing data. 0 means no limit on target file size, and the actual
file size will be determined by other factors such as max partition number and
shuffle batch size.
[...]
| spark.gluten.sql.columnar.backend.velox.parquetUseColumnNames
| 🔄 Dynamic | true | Maps table field names to file field
names using names, not indices for Parquet files.
[...]
diff --git
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
index 0544e47c1d..270745063a 100644
---
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
+++
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergWriteExec.scala
@@ -19,7 +19,7 @@ package org.apache.gluten.execution
import org.apache.gluten.backendsapi.BackendsApiManager
import org.apache.iceberg.{FileFormat, PartitionField, PartitionSpec, Schema,
TableProperties}
-import org.apache.iceberg.TableProperties.{ORC_COMPRESSION,
ORC_COMPRESSION_DEFAULT, PARQUET_COMPRESSION, PARQUET_COMPRESSION_DEFAULT,
PARQUET_PAGE_SIZE_BYTES, PARQUET_PAGE_SIZE_BYTES_DEFAULT}
+import org.apache.iceberg.TableProperties.{ORC_COMPRESSION,
ORC_COMPRESSION_DEFAULT, PARQUET_COMPRESSION, PARQUET_COMPRESSION_DEFAULT,
PARQUET_DICT_SIZE_BYTES, PARQUET_DICT_SIZE_BYTES_DEFAULT,
PARQUET_PAGE_SIZE_BYTES, PARQUET_PAGE_SIZE_BYTES_DEFAULT}
import org.apache.iceberg.avro.AvroSchemaUtil
import org.apache.iceberg.spark.source.IcebergWriteUtil
import org.apache.iceberg.types.Type.TypeID
@@ -60,6 +60,13 @@ trait IcebergWriteExec extends ColumnarV2TableWriteExec {
IcebergWriteUtil.getWriteConf(write).targetDataFileSize().toString
}
+ protected def getDictSizeBytes: String = {
+ val tableProps = IcebergWriteUtil.getTable(write).properties()
+ tableProps.getOrDefault(
+ normalizeCapacityString(PARQUET_DICT_SIZE_BYTES),
+ normalizeCapacityString(PARQUET_DICT_SIZE_BYTES_DEFAULT.toString))
+ }
+
protected def getPartitionSpec: PartitionSpec = {
IcebergWriteUtil.getPartitionSpec(write)
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]