This is an automated email from the ASF dual-hosted git repository.

loneylee pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 62d9696339 [CORE-8569][CH] Support DeltaOptimizedWriterTransformer 
(#8570)
62d9696339 is described below

commit 62d96963399268179517d53c64e320dfc856003b
Author: Shuai li <[email protected]>
AuthorDate: Tue Feb 11 14:59:25 2025 +0800

    [CORE-8569][CH] Support DeltaOptimizedWriterTransformer (#8570)
    
    * [CORE-8569][CH] Support DeltaOptimizedWriterTransformer
---
 backends-clickhouse/pom.xml                        |  39 ++-
 .../org.apache.gluten.sql.shims.DeltaShimProvider  |   1 +
 .../sql/shims/delta20/Delta20ShimProvider.scala}   |  17 +-
 .../gluten/sql/shims/delta20/Delta20Shims.scala}   |  17 +-
 .../sql/delta/catalog/ClickHouseTableV2.scala      |   4 +-
 .../org.apache.gluten.sql.shims.DeltaShimProvider  |   1 +
 .../sql/shims/delta23/Delta23ShimProvider.scala}   |  17 +-
 .../gluten/sql/shims/delta23/Delta23Shims.scala}   |  17 +-
 .../sql/delta/catalog/ClickHouseTableV2.scala      |   4 +-
 .../org.apache.gluten.sql.shims.DeltaShimProvider  |   1 +
 .../sql/shims/delta32/Delta32ShimProvider.scala}   |  17 +-
 .../gluten/sql/shims/delta32/Delta32Shims.scala}   |  22 +-
 .../delta/ClickhouseOptimisticTransaction.scala    |   1 -
 .../sql/delta/catalog/ClickHouseTableV2.scala      |   4 +-
 .../sql/execution/FileDeltaColumnarWrite.scala     |   4 +-
 .../execution/datasources/DeltaV1WritesSuite.scala |  53 ++-
 .../org.apache.gluten.component.CHDeltaComponent   |   0
 .../gluten/component/CHDeltaComponent.scala}       |  18 +-
 .../gluten/execution/OffloadDeltaNode.scala}       |  20 +-
 .../apache/gluten/sql/shims/DeltaShimLoader.scala  |  56 +++
 .../gluten/sql/shims/DeltaShimProvider.scala}      |  18 +-
 .../org/apache/gluten/sql/shims/DeltaShims.scala}  |  22 +-
 .../gluten/component}/CHKafkaComponent.scala       |   0
 .../Storages/MergeTree/SparkMergeTreeSink.h        |   2 +
 gluten-delta/pom.xml                               |   6 +
 .../sql/perf/DeltaOptimizedWriterTransformer.scala | 386 +++++++++++++++++++++
 .../shuffle/sort/ColumnarShuffleManager.scala      |   2 +-
 27 files changed, 622 insertions(+), 127 deletions(-)

diff --git a/backends-clickhouse/pom.xml b/backends-clickhouse/pom.xml
index 9b954c2ccf..0aa22c426d 100644
--- a/backends-clickhouse/pom.xml
+++ b/backends-clickhouse/pom.xml
@@ -92,6 +92,28 @@
         </dependency>
       </dependencies>
     </profile>
+    <profile>
+      <id>delta</id>
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.gluten</groupId>
+          <artifactId>gluten-delta</artifactId>
+          <version>${project.version}</version>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.gluten</groupId>
+          <artifactId>gluten-delta</artifactId>
+          <version>${project.version}</version>
+          <type>test-jar</type>
+          <scope>test</scope>
+        </dependency>
+        <dependency>
+          <groupId>io.delta</groupId>
+          
<artifactId>${delta.package.name}_${scala.binary.version}</artifactId>
+          <scope>provided</scope>
+        </dependency>
+      </dependencies>
+    </profile>
   </profiles>
 
   <dependencies>
@@ -464,15 +486,18 @@
             <includes>
               <include>src/main/scala/**/*.scala</include>
               <include>src/test/scala/**/*.scala</include>
-              
<include>src-delta-${delta.binary.version}/main/delta/**/*.scala</include>
-              
<include>src-delta-${delta.binary.version}/test/delta/**/*.scala</include>
+              <include>src-delta/main/scala/**/*.scala</include>
+              <include>src-delta/test/scala/**/*.scala</include>
+              
<include>src-delta-${delta.binary.version}/main/scala/**/*.scala</include>
+              
<include>src-delta-${delta.binary.version}/test/scala/**/*.scala</include>
             </includes>
             <excludes>
-              
<exclude>src-delta-${delta.binary.version}/main/delta/org/apache/spark/sql/delta/commands/*.scala</exclude>
-              
<exclude>src-delta-${delta.binary.version}/main/delta/org/apache/spark/sql/delta/commands/merge/*.scala</exclude>
-              
<exclude>src-delta-${delta.binary.version}/main/delta/org/apache/spark/sql/delta/stats/*.scala</exclude>
-              
<exclude>src-delta-${delta.binary.version}/main/delta/org/apache/spark/sql/delta/DeltaLog.scala</exclude>
-              
<exclude>src-delta-${delta.binary.version}/main/delta/org/apache/spark/sql/delta/Snapshot.scala</exclude>
+              
<exclude>src-delta-${delta.binary.version}/main/scala/org/apache/spark/sql/delta/commands/*.scala</exclude>
+              
<exclude>src-delta-${delta.binary.version}/main/scala/org/apache/spark/sql/delta/commands/merge/*.scala</exclude>
+              
<exclude>src-delta-${delta.binary.version}/main/scala/org/apache/spark/sql/delta/stats/*.scala</exclude>
+              
<exclude>src-delta-${delta.binary.version}/main/scala/org/apache/spark/sql/delta/DeltaLog.scala</exclude>
+              
<exclude>src-delta-${delta.binary.version}/main/scala/org/apache/spark/sql/delta/Snapshot.scala</exclude>
+              
<exclude>src-delta-${delta.binary.version}/main/scala/org/apache/spark/sql/delta/ClickhouseOptimisticTransaction.scala</exclude>
             </excludes>
           </scala>
         </configuration>
diff --git 
a/backends-clickhouse/src-delta-20/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
 
b/backends-clickhouse/src-delta-20/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
new file mode 100644
index 0000000000..2e9fc7f42f
--- /dev/null
+++ 
b/backends-clickhouse/src-delta-20/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
@@ -0,0 +1 @@
+org.apache.gluten.sql.shims.delta20.Delta20ShimProvider
\ No newline at end of file
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta-20/main/scala/org/apache/gluten/sql/shims/delta20/Delta20ShimProvider.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta-20/main/scala/org/apache/gluten/sql/shims/delta20/Delta20ShimProvider.scala
index 1e9f0f9d77..65efe745fb 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta-20/main/scala/org/apache/gluten/sql/shims/delta20/Delta20ShimProvider.scala
@@ -14,19 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims.delta20
 
-package org.apache.gluten.component
+import org.apache.gluten.sql.shims.{DeltaShimProvider, DeltaShims}
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
+class Delta20ShimProvider extends DeltaShimProvider {
 
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
+  override def createShim: DeltaShims = {
+    new Delta20Shims()
   }
+
 }
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta-20/main/scala/org/apache/gluten/sql/shims/delta20/Delta20Shims.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta-20/main/scala/org/apache/gluten/sql/shims/delta20/Delta20Shims.scala
index 1e9f0f9d77..2348586e40 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta-20/main/scala/org/apache/gluten/sql/shims/delta20/Delta20Shims.scala
@@ -14,19 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims.delta20
 
-package org.apache.gluten.component
+import org.apache.gluten.sql.shims.DeltaShims
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
-
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
-  }
-}
+class Delta20Shims extends DeltaShims {}
diff --git 
a/backends-clickhouse/src-delta-20/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
 
b/backends-clickhouse/src-delta-20/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
index 9e76a282a5..d2966eafee 100644
--- 
a/backends-clickhouse/src-delta-20/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
+++ 
b/backends-clickhouse/src-delta-20/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
@@ -29,12 +29,14 @@ import 
org.apache.spark.sql.execution.datasources.{HadoopFsRelation, PartitionDi
 import 
org.apache.spark.sql.execution.datasources.clickhouse.utils.MergeTreePartsPartitionsUtil
 import org.apache.spark.sql.execution.datasources.mergetree.StorageMeta
 import 
org.apache.spark.sql.execution.datasources.v2.clickhouse.source.DeltaMergeTreeFileFormat
+import org.apache.spark.sql.types.StructType
 import org.apache.spark.sql.util.CaseInsensitiveStringMap
 import org.apache.spark.util.collection.BitSet
+
 import org.apache.hadoop.fs.Path
-import org.apache.spark.sql.types.StructType
 
 import java.{util => ju}
+
 import scala.collection.JavaConverters._
 
 
@SuppressWarnings(Array("io.github.zhztheplayer.scalawarts.InheritFromCaseClass"))
diff --git 
a/backends-clickhouse/src-delta-23/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
 
b/backends-clickhouse/src-delta-23/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
new file mode 100644
index 0000000000..c512ce8a71
--- /dev/null
+++ 
b/backends-clickhouse/src-delta-23/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
@@ -0,0 +1 @@
+org.apache.gluten.sql.shims.delta23.Delta23ShimProvider
\ No newline at end of file
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta-23/main/scala/org/apache/gluten/sql/shims/delta23/Delta23ShimProvider.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta-23/main/scala/org/apache/gluten/sql/shims/delta23/Delta23ShimProvider.scala
index 1e9f0f9d77..854a996999 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta-23/main/scala/org/apache/gluten/sql/shims/delta23/Delta23ShimProvider.scala
@@ -14,19 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims.delta23
 
-package org.apache.gluten.component
+import org.apache.gluten.sql.shims.{DeltaShimProvider, DeltaShims}
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
+class Delta23ShimProvider extends DeltaShimProvider {
 
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
+  override def createShim: DeltaShims = {
+    new Delta23Shims()
   }
+
 }
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta-23/main/scala/org/apache/gluten/sql/shims/delta23/Delta23Shims.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta-23/main/scala/org/apache/gluten/sql/shims/delta23/Delta23Shims.scala
index 1e9f0f9d77..3528363a6e 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta-23/main/scala/org/apache/gluten/sql/shims/delta23/Delta23Shims.scala
@@ -14,19 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims.delta23
 
-package org.apache.gluten.component
+import org.apache.gluten.sql.shims.DeltaShims
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
-
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
-  }
-}
+class Delta23Shims extends DeltaShims {}
diff --git 
a/backends-clickhouse/src-delta-23/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
 
b/backends-clickhouse/src-delta-23/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
index 9e76a282a5..d2966eafee 100644
--- 
a/backends-clickhouse/src-delta-23/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
+++ 
b/backends-clickhouse/src-delta-23/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
@@ -29,12 +29,14 @@ import 
org.apache.spark.sql.execution.datasources.{HadoopFsRelation, PartitionDi
 import 
org.apache.spark.sql.execution.datasources.clickhouse.utils.MergeTreePartsPartitionsUtil
 import org.apache.spark.sql.execution.datasources.mergetree.StorageMeta
 import 
org.apache.spark.sql.execution.datasources.v2.clickhouse.source.DeltaMergeTreeFileFormat
+import org.apache.spark.sql.types.StructType
 import org.apache.spark.sql.util.CaseInsensitiveStringMap
 import org.apache.spark.util.collection.BitSet
+
 import org.apache.hadoop.fs.Path
-import org.apache.spark.sql.types.StructType
 
 import java.{util => ju}
+
 import scala.collection.JavaConverters._
 
 
@SuppressWarnings(Array("io.github.zhztheplayer.scalawarts.InheritFromCaseClass"))
diff --git 
a/backends-clickhouse/src-delta-32/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
 
b/backends-clickhouse/src-delta-32/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
new file mode 100644
index 0000000000..170775d428
--- /dev/null
+++ 
b/backends-clickhouse/src-delta-32/main/resources/META-INF/services/org.apache.gluten.sql.shims.DeltaShimProvider
@@ -0,0 +1 @@
+org.apache.gluten.sql.shims.delta32.Delta32ShimProvider
\ No newline at end of file
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/gluten/sql/shims/delta32/Delta32ShimProvider.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta-32/main/scala/org/apache/gluten/sql/shims/delta32/Delta32ShimProvider.scala
index 1e9f0f9d77..de4e9682bd 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/gluten/sql/shims/delta32/Delta32ShimProvider.scala
@@ -14,19 +14,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims.delta32
 
-package org.apache.gluten.component
+import org.apache.gluten.sql.shims.{DeltaShimProvider, DeltaShims}
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
+class Delta32ShimProvider extends DeltaShimProvider {
 
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
+  override def createShim: DeltaShims = {
+    new Delta32Shims()
   }
+
 }
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/gluten/sql/shims/delta32/Delta32Shims.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta-32/main/scala/org/apache/gluten/sql/shims/delta32/Delta32Shims.scala
index 1e9f0f9d77..77160f49ce 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/gluten/sql/shims/delta32/Delta32Shims.scala
@@ -14,19 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims.delta32
 
-package org.apache.gluten.component
+import org.apache.gluten.execution.GlutenPlan
+import org.apache.gluten.sql.shims.DeltaShims
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
+import org.apache.spark.sql.execution.SparkPlan
+import org.apache.spark.sql.perf.DeltaOptimizedWriterTransformer
 
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
+class Delta32Shims extends DeltaShims {
+  override def supportDeltaOptimizedWriterExec(plan: SparkPlan): Boolean =
+    DeltaOptimizedWriterTransformer.support(plan)
+
+  override def offloadDeltaOptimizedWriterExec(plan: SparkPlan): GlutenPlan = {
+    DeltaOptimizedWriterTransformer.from(plan)
   }
 }
diff --git 
a/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/ClickhouseOptimisticTransaction.scala
 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/ClickhouseOptimisticTransaction.scala
index f559be7997..611d011ee7 100644
--- 
a/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/ClickhouseOptimisticTransaction.scala
+++ 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/ClickhouseOptimisticTransaction.scala
@@ -267,7 +267,6 @@ class ClickhouseOptimisticTransaction(
       // TODO: val checkInvariants = DeltaInvariantCheckerExec(empty2NullPlan, 
constraints)
       val checkInvariants = empty2NullPlan
 
-      // TODO: DeltaOptimizedWriterExec
       // No need to plan optimized write if the write command is OPTIMIZE, 
which aims to produce
       // evenly-balanced data files already.
        val physicalPlan =
diff --git 
a/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
index 91cf31ba7c..2c7a57625b 100644
--- 
a/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
+++ 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/delta/catalog/ClickHouseTableV2.scala
@@ -33,11 +33,13 @@ import 
org.apache.spark.sql.execution.datasources.clickhouse.utils.MergeTreePart
 import org.apache.spark.sql.execution.datasources.mergetree.StorageMeta
 import 
org.apache.spark.sql.execution.datasources.v2.clickhouse.source.DeltaMergeTreeFileFormat
 import 
org.apache.spark.sql.execution.datasources.v2.clickhouse.utils.CHDataSourceUtils
+import org.apache.spark.sql.types.StructType
 import org.apache.spark.util.collection.BitSet
+
 import org.apache.hadoop.fs.Path
-import org.apache.spark.sql.types.StructType
 
 import java.{util => ju}
+
 import scala.collection.JavaConverters._
 
 
@SuppressWarnings(Array("io.github.zhztheplayer.scalawarts.InheritFromCaseClass"))
diff --git 
a/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/execution/FileDeltaColumnarWrite.scala
 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/execution/FileDeltaColumnarWrite.scala
index 4d6040bae8..871df856eb 100644
--- 
a/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/execution/FileDeltaColumnarWrite.scala
+++ 
b/backends-clickhouse/src-delta-32/main/scala/org/apache/spark/sql/execution/FileDeltaColumnarWrite.scala
@@ -19,7 +19,7 @@ package org.apache.spark.sql.execution
 import org.apache.gluten.backendsapi.clickhouse.RuntimeSettings
 import org.apache.gluten.exception.GlutenNotSupportException
 import org.apache.gluten.vectorized.NativeExpressionEvaluator
-import org.apache.hadoop.fs.Path
+
 import org.apache.spark.internal.Logging
 import org.apache.spark.internal.io.FileCommitProtocol
 import org.apache.spark.sql.catalyst.InternalRow
@@ -30,6 +30,8 @@ import org.apache.spark.sql.delta.stats.{DeltaFileStatistics, 
DeltaJobStatistics
 import org.apache.spark.sql.execution.datasources.{ExecutedWriteSummary, 
WriteJobDescription, WriteTaskResult}
 import org.apache.spark.util.Utils
 
+import org.apache.hadoop.fs.Path
+
 import scala.collection.mutable.ArrayBuffer
 
 case class DeltaFileCommitInfo(committer: FileDelayedCommitProtocol)
diff --git 
a/backends-clickhouse/src-delta-32/test/scala/org/apache/spark/sql/execution/datasources/DeltaV1WritesSuite.scala
 
b/backends-clickhouse/src-delta-32/test/scala/org/apache/spark/sql/execution/datasources/DeltaV1WritesSuite.scala
index 6db41f56c5..906b743549 100644
--- 
a/backends-clickhouse/src-delta-32/test/scala/org/apache/spark/sql/execution/datasources/DeltaV1WritesSuite.scala
+++ 
b/backends-clickhouse/src-delta-32/test/scala/org/apache/spark/sql/execution/datasources/DeltaV1WritesSuite.scala
@@ -18,9 +18,12 @@ package org.apache.spark.sql.execution.datasources
 
 import org.apache.gluten.config.GlutenConfig
 import 
org.apache.gluten.execution.{GlutenClickHouseWholeStageTransformerSuite, 
GlutenPlan, SortExecTransformer}
+
 import org.apache.spark.SparkConf
-import org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat
+import org.apache.spark.sql.DataFrame
+import org.apache.spark.sql.delta.{DeltaLog, DeltaOptions}
 import org.apache.spark.sql.execution.{SortExec, SparkPlan}
+import org.apache.spark.sql.execution.datasources.parquet.ParquetFileFormat
 
 class DeltaV1WritesSuite extends GlutenClickHouseWholeStageTransformerSuite {
 
@@ -29,6 +32,14 @@ class DeltaV1WritesSuite extends 
GlutenClickHouseWholeStageTransformerSuite {
   override protected def sparkConf: SparkConf = {
     super.sparkConf
       .set(GlutenConfig.NATIVE_WRITER_ENABLED.key, "true")
+      .set(
+        "spark.sql.catalog.spark_catalog",
+        
"org.apache.spark.sql.execution.datasources.v2.clickhouse.ClickHouseSparkCatalog")
+      .set("spark.databricks.delta.maxSnapshotLineageLength", "20")
+      .set("spark.databricks.delta.snapshotPartitions", "1")
+      .set("spark.databricks.delta.properties.defaults.checkpointInterval", 
"5")
+      .set("spark.databricks.delta.stalenessLimit", "3600000")
+      .set("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
   }
 
   override def beforeAll(): Unit = {
@@ -97,4 +108,44 @@ class DeltaV1WritesSuite extends 
GlutenClickHouseWholeStageTransformerSuite {
     check(df.orderBy("j", "k").queryExecution.sparkPlan)
   }
 
+  protected def checkResult(df: DataFrame, numFileCheck: Long => Boolean, dir: 
String): Unit = {
+    val (_, snapshot) = DeltaLog.forTableWithSnapshot(spark, dir)
+    val files = snapshot.numOfFiles
+    assert(numFileCheck(files), s"file check failed: received $files")
+
+    checkAnswer(
+      spark.read.format("delta").load(dir),
+      df
+    )
+  }
+
+  test("test optimize write") {
+    withTempDir {
+      dir =>
+        val df = sql("select * from t0").toDF()
+        df.write
+          .format("delta")
+          .option(DeltaOptions.OPTIMIZE_WRITE_OPTION, "true")
+          .save(dir.getPath)
+        checkResult(df, numFileCheck = _ === 1, dir.getPath)
+    }
+  }
+
+  test("optimize write - partitioned write") {
+    withTempDir {
+      dir =>
+        val df = spark
+          .range(0, 100, 1, 4)
+          .withColumn("part", 'id % 5)
+
+        df.write
+          .partitionBy("part")
+          .option(DeltaOptions.OPTIMIZE_WRITE_OPTION, "true")
+          .format("delta")
+          .save(dir.getPath)
+
+        checkResult(df, numFileCheck = _ <= 5, dir.getPath)
+    }
+  }
+
 }
diff --git 
a/backends-clickhouse/src-delta/main/resources/META-INF/gluten-components/org.apache.gluten.component.CHDeltaComponent
 
b/backends-clickhouse/src-delta/main/resources/META-INF/gluten-components/org.apache.gluten.component.CHDeltaComponent
new file mode 100644
index 0000000000..e69de29bb2
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/component/CHDeltaComponent.scala
similarity index 67%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta/main/scala/org/apache/gluten/component/CHDeltaComponent.scala
index 1e9f0f9d77..48a4c88518 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/component/CHDeltaComponent.scala
@@ -14,19 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.gluten.component
 
 import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
+import org.apache.gluten.execution.OffloadDeltaNode
+import org.apache.gluten.extension.columnar.heuristic.HeuristicTransform
+import org.apache.gluten.extension.columnar.validator.Validators
 import org.apache.gluten.extension.injector.Injector
 
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
+class CHDeltaComponent extends Component {
+  override def name(): String = "ch-delta"
   override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
+    Component.BuildInfo("CHDelta", "N/A", "N/A", "N/A")
   override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
   override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
+    val legacy = injector.gluten.legacy
+    legacy.injectTransform {
+      c =>
+        val offload = Seq(OffloadDeltaNode())
+        HeuristicTransform.Simple(Validators.newValidator(c.glutenConf, 
offload), offload)
+    }
   }
 }
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/execution/OffloadDeltaNode.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta/main/scala/org/apache/gluten/execution/OffloadDeltaNode.scala
index 1e9f0f9d77..bfde89733d 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/execution/OffloadDeltaNode.scala
@@ -14,19 +14,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.execution
 
-package org.apache.gluten.component
+import org.apache.gluten.extension.columnar.offload.OffloadSingleNode
+import org.apache.gluten.sql.shims.DeltaShimLoader
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
+import org.apache.spark.sql.execution.SparkPlan
 
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
+case class OffloadDeltaNode() extends OffloadSingleNode {
+  override def offload(plan: SparkPlan): SparkPlan = plan match {
+    case plan if 
DeltaShimLoader.getDeltaShims.supportDeltaOptimizedWriterExec(plan) =>
+      DeltaShimLoader.getDeltaShims.offloadDeltaOptimizedWriterExec(plan)
+    case other => other
   }
 }
diff --git 
a/backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShimLoader.scala
 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShimLoader.scala
new file mode 100644
index 0000000000..50634b97e9
--- /dev/null
+++ 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShimLoader.scala
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.sql.shims
+
+import org.apache.spark.internal.Logging
+
+import java.util.ServiceLoader
+
+import scala.collection.JavaConverters._
+
+object DeltaShimLoader extends Logging {
+  private var deltaShims: DeltaShims = null
+
+  def getDeltaShims: DeltaShims = {
+    if (deltaShims == null) {
+      val provider = getDeltaShimProvider
+      deltaShims = provider.createShim
+    }
+    deltaShims
+  }
+
+  private def loadDeltaShimProvider(): DeltaShimProvider = {
+    // Load and filter the providers based on version
+    val shimProviders =
+      ServiceLoader.load(classOf[DeltaShimProvider]).asScala
+    if (shimProviders.size > 1) {
+      throw new IllegalStateException(s"More than one DeltaShimProvider found: 
$shimProviders")
+    }
+
+    val shimProvider = shimProviders.headOption match {
+      case Some(shimProvider) => shimProvider
+      case None =>
+        throw new IllegalStateException(s"No Delta Shim Provider.")
+    }
+    logInfo(s"Using Shim provider: $shimProviders")
+    shimProvider
+  }
+
+  private def getDeltaShimProvider: DeltaShimProvider = {
+    loadDeltaShimProvider()
+  }
+}
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShimProvider.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShimProvider.scala
index 1e9f0f9d77..92900ecf71 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShimProvider.scala
@@ -14,19 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims
 
-package org.apache.gluten.component
+import org.apache.spark.internal.Logging
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
-
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
-  }
+/** Provider interface for matching and retrieving the Shims of a specific 
Spark version */
+trait DeltaShimProvider extends Logging {
+  def createShim: DeltaShims
 }
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShims.scala
similarity index 57%
copy from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
copy to 
backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShims.scala
index 1e9f0f9d77..032054c412 100644
--- 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
+++ 
b/backends-clickhouse/src-delta/main/scala/org/apache/gluten/sql/shims/DeltaShims.scala
@@ -14,19 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package org.apache.gluten.sql.shims
 
-package org.apache.gluten.component
+import org.apache.gluten.execution.GlutenPlan
 
-import org.apache.gluten.backendsapi.clickhouse.CHBackend
-import org.apache.gluten.execution.OffloadKafkaScan
-import org.apache.gluten.extension.injector.Injector
+import org.apache.spark.sql.execution.SparkPlan
 
-class CHKafkaComponent extends Component {
-  override def name(): String = "clickhouse-kafka"
-  override def buildInfo(): Component.BuildInfo =
-    Component.BuildInfo("ClickHouseKafka", "N/A", "N/A", "N/A")
-  override def dependencies(): Seq[Class[_ <: Component]] = classOf[CHBackend] 
:: Nil
-  override def injectRules(injector: Injector): Unit = {
-    OffloadKafkaScan.inject(injector)
+sealed abstract class ShimDescriptor
+
+trait DeltaShims {
+  def supportDeltaOptimizedWriterExec(plan: SparkPlan): Boolean = false
+
+  def offloadDeltaOptimizedWriterExec(plan: SparkPlan): GlutenPlan = {
+    throw new UnsupportedOperationException(
+      s"Can't transform ColumnarDeltaOptimizedWriterExec from 
${plan.getClass.getSimpleName}")
   }
 }
diff --git 
a/backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
 
b/backends-clickhouse/src-kafka/main/scala/org/apache/gluten/component/CHKafkaComponent.scala
similarity index 100%
rename from 
backends-clickhouse/src-kafka/main/scala/org.apache.gluten.component/CHKafkaComponent.scala
rename to 
backends-clickhouse/src-kafka/main/scala/org/apache/gluten/component/CHKafkaComponent.scala
diff --git a/cpp-ch/local-engine/Storages/MergeTree/SparkMergeTreeSink.h 
b/cpp-ch/local-engine/Storages/MergeTree/SparkMergeTreeSink.h
index a48a903b4c..0d3baf0457 100644
--- a/cpp-ch/local-engine/Storages/MergeTree/SparkMergeTreeSink.h
+++ b/cpp-ch/local-engine/Storages/MergeTree/SparkMergeTreeSink.h
@@ -204,6 +204,8 @@ public:
 
     void collectStats(const std::deque<PartWithStats> & parts, const 
std::string & partition_dir) const
     {
+        if (parts.empty())
+            return;
         const std::string & partition = partition_dir.empty() ? 
WriteStatsBase::NO_PARTITION_ID : partition_dir;
         size_t columnSize = parts[0].delta_stats->min.size();
         assert(columns_.size() == stats_column_start + columnSize * 3);
diff --git a/gluten-delta/pom.xml b/gluten-delta/pom.xml
index ef30a4f563..a78f91ea28 100755
--- a/gluten-delta/pom.xml
+++ b/gluten-delta/pom.xml
@@ -66,6 +66,12 @@
       <version>${spark.version}</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-common</artifactId>
+      <version>${hadoop.version}</version>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.hadoop</groupId>
       <artifactId>hadoop-client</artifactId>
diff --git 
a/gluten-delta/src-delta-32/main/scala/org/apache/spark/sql/perf/DeltaOptimizedWriterTransformer.scala
 
b/gluten-delta/src-delta-32/main/scala/org/apache/spark/sql/perf/DeltaOptimizedWriterTransformer.scala
new file mode 100644
index 0000000000..9f601d8466
--- /dev/null
+++ 
b/gluten-delta/src-delta-32/main/scala/org/apache/spark/sql/perf/DeltaOptimizedWriterTransformer.scala
@@ -0,0 +1,386 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.perf
+
+import org.apache.gluten.backendsapi.BackendsApiManager
+import org.apache.gluten.execution.GlutenPlan
+import org.apache.gluten.extension.columnar.transition.Convention
+
+import org.apache.spark._
+import org.apache.spark.internal.config
+import org.apache.spark.internal.config.ConfigEntry
+import org.apache.spark.network.util.ByteUnit
+import org.apache.spark.rdd.RDD
+import org.apache.spark.serializer.SerializerManager
+import org.apache.spark.shuffle.{FetchFailedException, ShuffleReader, 
ShuffleReadMetricsReporter}
+import org.apache.spark.shuffle.sort.{ColumnarShuffleHandle, 
ColumnarShuffleManager}
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.catalyst.plans.physical.{HashPartitioning, 
Partitioning}
+import org.apache.spark.sql.delta.{DeltaErrors, DeltaLog}
+import org.apache.spark.sql.delta.metering.DeltaLogging
+import org.apache.spark.sql.delta.perf.{DeltaOptimizedWriterExec, 
OptimizedWriterBlocks}
+import org.apache.spark.sql.delta.sources.DeltaSQLConf
+import org.apache.spark.sql.delta.util.BinPackingUtils
+import org.apache.spark.sql.execution._
+import org.apache.spark.sql.execution.exchange.ENSURE_REQUIREMENTS
+import org.apache.spark.sql.execution.metric.{SQLMetric, 
SQLShuffleWriteMetricsReporter}
+import org.apache.spark.sql.metric.SQLColumnarShuffleReadMetricsReporter
+import org.apache.spark.sql.vectorized.ColumnarBatch
+import org.apache.spark.storage.{BlockId, BlockManagerId, 
ShuffleBlockFetcherIterator, ShuffleBlockId}
+import org.apache.spark.util.ThreadUtils
+
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+import scala.concurrent.duration.Duration
+
+/**
+ * An execution node which shuffles data to a target output of 
`DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS`
+ * blocks, hash partitioned on the table partition columns. We group all 
blocks by their
+ * reducer_id's and bin-pack into `DELTA_OPTIMIZE_WRITE_BIN_SIZE` bins. Then 
we launch a Spark task
+ * per bin to write out a single file for each bin.
+ *
+ * @param child
+ *   The execution plan
+ * @param partitionColumns
+ *   The partition columns of the table. Used for hash partitioning the write
+ * @param deltaLog
+ *   The DeltaLog for the table. Used for logging only
+ */
+
+case class DeltaOptimizedWriterTransformer(
+    child: SparkPlan,
+    partitionColumns: Seq[String],
+    @transient deltaLog: DeltaLog
+) extends UnaryExecNode
+  with GlutenPlan
+  with DeltaLogging {
+
+  lazy val useSortBasedShuffle: Boolean =
+    
BackendsApiManager.getSparkPlanExecApiInstance.useSortBasedShuffle(outputPartitioning,
 output)
+
+  override def output: Seq[Attribute] = child.output
+
+  private lazy val readMetrics =
+    
SQLColumnarShuffleReadMetricsReporter.createShuffleReadMetrics(sparkContext)
+
+  private[sql] lazy val writeMetrics =
+    SQLShuffleWriteMetricsReporter.createShuffleWriteMetrics(sparkContext)
+
+  // Note: "metrics" is made transient to avoid sending driver-side metrics to 
tasks.
+  @transient override lazy val metrics =
+    BackendsApiManager.getMetricsApiInstance
+      .genColumnarShuffleExchangeMetrics(
+        sparkContext,
+        useSortBasedShuffle) ++ readMetrics ++ writeMetrics
+
+  @transient lazy val inputColumnarRDD: RDD[ColumnarBatch] = 
child.executeColumnar()
+
+  private lazy val childNumPartitions = inputColumnarRDD.getNumPartitions
+
+  private lazy val numPartitions: Int = {
+    if (childNumPartitions > 0) {
+      val targetShuffleBlocks = 
getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS)
+      math.min(
+        math.max(targetShuffleBlocks / childNumPartitions, 1),
+        getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_MAX_SHUFFLE_PARTITIONS))
+    } else {
+      childNumPartitions
+    }
+  }
+
+  @transient private var cachedShuffleRDD: ShuffledColumnarBatchRDD = _
+
+  @transient override def outputPartitioning: Partitioning = {
+    val resolver = 
org.apache.spark.sql.catalyst.analysis.caseInsensitiveResolution
+    val saltedPartitioning = HashPartitioning(
+      partitionColumns.map(
+        p =>
+          output
+            .find(o => resolver(p, o.name))
+            .getOrElse(throw 
DeltaErrors.failedFindPartitionColumnInOutputPlan(p))),
+      numPartitions)
+    saltedPartitioning
+  }
+
+  @transient private lazy val mapTracker = SparkEnv.get.mapOutputTracker
+
+  private def getShuffleRDD: ShuffledColumnarBatchRDD = {
+    if (cachedShuffleRDD == null) {
+      val resolver = 
org.apache.spark.sql.catalyst.analysis.caseInsensitiveResolution
+      val saltedPartitioning = HashPartitioning(
+        partitionColumns.map(
+          p =>
+            output
+              .find(o => resolver(p, o.name))
+              .getOrElse(throw 
DeltaErrors.failedFindPartitionColumnInOutputPlan(p))),
+        numPartitions)
+      val shuffledRDD =
+        ColumnarShuffleExchangeExec(saltedPartitioning, child, 
ENSURE_REQUIREMENTS, null)
+          .executeColumnar()
+          .asInstanceOf[ShuffledColumnarBatchRDD]
+      cachedShuffleRDD = shuffledRDD
+    }
+    cachedShuffleRDD
+  }
+
+  private def computeBins(): Array[List[(BlockManagerId, ArrayBuffer[(BlockId, 
Long, Int)])]] = {
+    // Get all shuffle information
+    val shuffleStats = getShuffleStats()
+
+    // Group by blockId instead of block manager
+    val blockInfo = shuffleStats.flatMap {
+      case (bmId, blocks) =>
+        blocks.map {
+          case (blockId, size, index) =>
+            (blockId, (bmId, size, index))
+        }
+    }.toMap
+
+    val maxBinSize =
+      
ByteUnit.BYTE.convertFrom(getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_BIN_SIZE), 
ByteUnit.MiB)
+
+    val bins = shuffleStats.toSeq
+      .flatMap(_._2)
+      .groupBy(_._1.asInstanceOf[ShuffleBlockId].reduceId)
+      .flatMap {
+        case (_, blocks) =>
+          BinPackingUtils.binPackBySize[(BlockId, Long, Int), BlockId](
+            blocks,
+            _._2, // size
+            _._1, // blockId
+            maxBinSize)
+      }
+
+    bins
+      .map {
+        bin =>
+          var binSize = 0L
+          val blockLocations =
+            new mutable.HashMap[BlockManagerId, ArrayBuffer[(BlockId, Long, 
Int)]]()
+          for (blockId <- bin) {
+            val (bmId, size, index) = blockInfo(blockId)
+            binSize += size
+            val blocksAtBM =
+              blockLocations.getOrElseUpdate(bmId, new ArrayBuffer[(BlockId, 
Long, Int)]())
+            blocksAtBM.append((blockId, size, index))
+          }
+          (binSize, blockLocations.toList)
+      }
+      .toArray
+      .sortBy(_._1)(Ordering[Long].reverse) // submit largest blocks first
+      .map(_._2)
+  }
+
+  private def getShuffleStats(): Array[(BlockManagerId, 
collection.Seq[(BlockId, Long, Int)])] = {
+    val dep = getShuffleRDD.dependency
+
+    // Gets the shuffle output stats
+    def getStats =
+      mapTracker.getMapSizesByExecutorId(dep.shuffleId, 0, Int.MaxValue, 0, 
numPartitions).toArray
+
+    // Executes the shuffle map stage in case we are missing output stats
+    def awaitShuffleMapStage(): Unit = {
+      assert(dep != null, "Shuffle dependency should not be null")
+      // hack to materialize the shuffle files in a fault tolerant way
+      ThreadUtils.awaitResult(sparkContext.submitMapStage(dep), Duration.Inf)
+    }
+
+    try {
+      val res = getStats
+      if (res.isEmpty) awaitShuffleMapStage()
+      getStats
+    } catch {
+      case e: FetchFailedException =>
+        logWarning("Failed to fetch shuffle blocks for the optimized writer. 
Retrying", e)
+        awaitShuffleMapStage()
+        getStats
+    }
+  }
+
+  override protected def doExecuteColumnar(): RDD[ColumnarBatch] = {
+    if (childNumPartitions <= 1) return inputColumnarRDD
+    val shuffledRDD = getShuffleRDD
+    val partitions = computeBins()
+
+    recordDeltaEvent(
+      deltaLog,
+      "delta.optimizeWrite.planned",
+      data = Map(
+        "originalPartitions" -> childNumPartitions,
+        "outputPartitions" -> partitions.length,
+        "shufflePartitions" -> numPartitions,
+        "numShuffleBlocks" -> 
getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_SHUFFLE_BLOCKS),
+        "binSize" -> getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_BIN_SIZE),
+        "maxShufflePartitions" ->
+          getConf(DeltaSQLConf.DELTA_OPTIMIZE_WRITE_MAX_SHUFFLE_PARTITIONS)
+      )
+    )
+    new ColumnarDeltaOptimizedWriterRDD(
+      sparkContext,
+      shuffledRDD.dependency,
+      readMetrics,
+      new OptimizedWriterBlocks(partitions))
+  }
+
+  private def getConf[T](entry: ConfigEntry[T]): T = {
+    conf.getConf(entry)
+  }
+
+  override protected def withNewChildInternal(
+      newChild: SparkPlan): DeltaOptimizedWriterTransformer =
+    copy(child = newChild)
+
+  override def batchType(): Convention.BatchType = 
BackendsApiManager.getSettings.primaryBatchType
+
+  override def rowType0(): Convention.RowType = Convention.RowType.None
+
+  override protected def doExecute(): RDD[InternalRow] = {
+    child.execute()
+  }
+}
+
+private class ColumnarDeltaOptimizedWriterRDD(
+    @transient sparkContext: SparkContext,
+    var dep: ShuffleDependency[Int, ColumnarBatch, ColumnarBatch],
+    metrics: Map[String, SQLMetric],
+    @transient blocks: OptimizedWriterBlocks
+) extends RDD[ColumnarBatch](sparkContext, Seq(dep))
+  with DeltaLogging {
+
+  override protected def getPartitions: Array[Partition] = 
Array.tabulate(blocks.bins.length) {
+    i => ColumnarShuffleBlockRDDPartition(i, blocks.bins(i))
+  }
+
+  override def compute(split: Partition, context: TaskContext): 
Iterator[ColumnarBatch] = {
+    val tempMetrics = context.taskMetrics().createTempShuffleReadMetrics()
+    val sqlMetricsReporter = new 
SQLColumnarShuffleReadMetricsReporter(tempMetrics, metrics)
+
+    val blocks = if (context.stageAttemptNumber() > 0) {
+      // We lost shuffle blocks, so we need to now get new manager addresses
+      val executorTracker = SparkEnv.get.mapOutputTracker
+      val oldBlockLocations = 
split.asInstanceOf[ColumnarShuffleBlockRDDPartition].blocks
+
+      // assumes we bin-pack by reducerId
+      val reducerId = 
oldBlockLocations.head._2.head._1.asInstanceOf[ShuffleBlockId].reduceId
+      // Get block addresses
+      val newLocations = executorTracker
+        .getMapSizesByExecutorId(dep.shuffleId, reducerId)
+        .flatMap {
+          case (bmId, newBlocks) =>
+            newBlocks.map(blockInfo => (blockInfo._3, (bmId, blockInfo)))
+        }
+        .toMap
+
+      val blockLocations = new mutable.HashMap[BlockManagerId, 
ArrayBuffer[(BlockId, Long, Int)]]()
+      oldBlockLocations.foreach {
+        case (_, oldBlocks) =>
+          oldBlocks.foreach {
+            oldBlock =>
+              val (bmId, blockInfo) = newLocations(oldBlock._3)
+              val blocksAtBM =
+                blockLocations.getOrElseUpdate(bmId, new ArrayBuffer[(BlockId, 
Long, Int)]())
+              blocksAtBM.append(blockInfo)
+          }
+      }
+
+      blockLocations.iterator
+    } else {
+      split.asInstanceOf[ColumnarShuffleBlockRDDPartition].blocks.iterator
+    }
+
+    val serializerManager =
+      if (dep.shuffleHandle.isInstanceOf[ColumnarShuffleHandle[_, _]]) {
+        ColumnarShuffleManager.bypassDecompressionSerializerManger
+      } else {
+        SparkEnv.get.serializerManager
+      }
+
+    val reader =
+      new OptimizedWriterShuffleReader(dep, context, blocks, 
sqlMetricsReporter, serializerManager)
+    reader.read().map(_._2)
+  }
+}
+
+private case class ColumnarShuffleBlockRDDPartition(
+    index: Int,
+    blocks: List[(BlockManagerId, ArrayBuffer[(BlockId, Long, Int)])])
+  extends Partition
+
+/** A simplified implementation of the `BlockStoreShuffleReader` for reading 
shuffle blocks. */
+private class OptimizedWriterShuffleReader(
+    dep: ShuffleDependency[Int, _, ColumnarBatch],
+    context: TaskContext,
+    blocks: Iterator[(BlockManagerId, ArrayBuffer[(BlockId, Long, Int)])],
+    readMetrics: ShuffleReadMetricsReporter,
+    serializerManager: SerializerManager)
+  extends ShuffleReader[Int, ColumnarBatch] {
+
+  /** Read the combined key-values for this reduce task */
+  override def read(): Iterator[Product2[Int, ColumnarBatch]] = {
+
+    val wrappedStreams = new ShuffleBlockFetcherIterator(
+      context,
+      SparkEnv.get.blockManager.blockStoreClient,
+      SparkEnv.get.blockManager,
+      SparkEnv.get.mapOutputTracker,
+      blocks,
+      serializerManager.wrapStream,
+      // Note: we use getSizeAsMb when no suffix is provided for backwards 
compatibility
+      SparkEnv.get.conf.getSizeAsMb("spark.reducer.maxSizeInFlight", "48m") * 
1024 * 1024,
+      SparkEnv.get.conf.getInt("spark.reducer.maxReqsInFlight", Int.MaxValue),
+      SparkEnv.get.conf.get(config.REDUCER_MAX_BLOCKS_IN_FLIGHT_PER_ADDRESS),
+      SparkEnv.get.conf.get(config.MAX_REMOTE_BLOCK_SIZE_FETCH_TO_MEM),
+      SparkEnv.get.conf.get(config.SHUFFLE_MAX_ATTEMPTS_ON_NETTY_OOM),
+      SparkEnv.get.conf.getBoolean("spark.shuffle.detectCorrupt", true),
+      
SparkEnv.get.conf.getBoolean("spark.shuffle.detectCorrupt.useExtraMemory", 
false),
+      SparkEnv.get.conf.getBoolean("spark.shuffle.checksum.enabled", true),
+      SparkEnv.get.conf.get("spark.shuffle.checksum.algorithm", "ADLER32"),
+      readMetrics,
+      false
+    )
+
+    val serializerInstance = dep.serializer.newInstance()
+
+    // Create a key/value iterator for each stream
+    val recordIter = wrappedStreams
+      .flatMap {
+        case (_, wrappedStream) =>
+          // Note: the asKeyValueIterator below wraps a key/value iterator 
inside of a
+          // NextIterator. The NextIterator makes sure that close() is called 
on the
+          // underlying InputStream when all records have been read.
+          
serializerInstance.deserializeStream(wrappedStream).asKeyValueIterator
+      }
+      .asInstanceOf[Iterator[Product2[Int, ColumnarBatch]]]
+
+    new InterruptibleIterator[Product2[Int, ColumnarBatch]](context, 
recordIter)
+  }
+}
+
+object DeltaOptimizedWriterTransformer {
+  def support(plan: SparkPlan): Boolean = 
plan.isInstanceOf[DeltaOptimizedWriterExec]
+
+  def from(plan: SparkPlan): DeltaOptimizedWriterTransformer = {
+    plan match {
+      case p: DeltaOptimizedWriterExec =>
+        DeltaOptimizedWriterTransformer(p.child, p.partitionColumns, 
p.deltaLog)
+      case _ =>
+        throw new UnsupportedOperationException(
+          s"Can't transform ColumnarDeltaOptimizedWriterExec from 
${plan.getClass.getSimpleName}")
+    }
+  }
+}
diff --git 
a/gluten-substrait/src/main/scala/org/apache/spark/shuffle/sort/ColumnarShuffleManager.scala
 
b/gluten-substrait/src/main/scala/org/apache/spark/shuffle/sort/ColumnarShuffleManager.scala
index 18e8b985b8..798dfcb3a4 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/spark/shuffle/sort/ColumnarShuffleManager.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/spark/shuffle/sort/ColumnarShuffleManager.scala
@@ -186,7 +186,7 @@ object ColumnarShuffleManager extends Logging {
     executorComponents
   }
 
-  private def bypassDecompressionSerializerManger =
+  def bypassDecompressionSerializerManger: SerializerManager =
     new SerializerManager(
       SparkEnv.get.serializer,
       SparkEnv.get.conf,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to