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

jackylee-ch 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 44c6048252 [GLUTEN-12676][CORE] Read MiB-declared Spark memory configs 
with matching accessors (#12677)
44c6048252 is described below

commit 44c6048252a95239027a48dbfe5491b603cae8c0
Author: YangJie <[email protected]>
AuthorDate: Thu Aug 6 10:20:31 2026 +0800

    [GLUTEN-12676][CORE] Read MiB-declared Spark memory configs with matching 
accessors (#12677)
---
 .../scala/org/apache/gluten/GlutenPlugin.scala     |   8 +-
 .../apache/gluten/config/GlutenCoreConfig.scala    |   2 -
 .../org/apache/spark/util/SparkResourceUtil.scala  |  35 +++++-
 .../gluten/GlutenDynamicOffHeapSizingSuite.scala   |  55 +++++++++
 .../apache/spark/util/SparkResourceUtilSuite.scala |  85 ++++++++++++++
 .../GlutenAutoAdjustStageResourceProfile.scala     |  50 ++++++--
 ...GlutenAutoAdjustStageResourceProfileSuite.scala | 129 +++++++++++++++++++++
 7 files changed, 341 insertions(+), 23 deletions(-)

diff --git a/gluten-core/src/main/scala/org/apache/gluten/GlutenPlugin.scala 
b/gluten-core/src/main/scala/org/apache/gluten/GlutenPlugin.scala
index 9cc283bd25..81fea4727b 100644
--- a/gluten-core/src/main/scala/org/apache/gluten/GlutenPlugin.scala
+++ b/gluten-core/src/main/scala/org/apache/gluten/GlutenPlugin.scala
@@ -111,13 +111,7 @@ private object GlutenDriverPlugin extends Logging {
     // Get the off-heap size set by user.
     val offHeapSize =
       if (conf.getBoolean(GlutenCoreConfig.DYNAMIC_OFFHEAP_SIZING_ENABLED.key, 
false)) {
-        val onHeapSize: Long =
-          if (conf.contains(GlutenCoreConfig.SPARK_ONHEAP_SIZE_KEY)) {
-            conf.getSizeAsBytes(GlutenCoreConfig.SPARK_ONHEAP_SIZE_KEY)
-          } else {
-            // 1GB default
-            1024 * 1024 * 1024
-          }
+        val onHeapSize: Long = SparkResourceUtil.getExecutorMemorySize(conf)
 
         if (conf.contains(GlutenCoreConfig.SPARK_OFFHEAP_ENABLED_KEY)) {
           logWarning(
diff --git 
a/gluten-core/src/main/scala/org/apache/gluten/config/GlutenCoreConfig.scala 
b/gluten-core/src/main/scala/org/apache/gluten/config/GlutenCoreConfig.scala
index eb08396772..ff96b5e4b6 100644
--- a/gluten-core/src/main/scala/org/apache/gluten/config/GlutenCoreConfig.scala
+++ b/gluten-core/src/main/scala/org/apache/gluten/config/GlutenCoreConfig.scala
@@ -76,8 +76,6 @@ object GlutenCoreConfig extends ConfigRegistry {
   val SPARK_OFFHEAP_SIZE_KEY = "spark.memory.offHeap.size"
   val SPARK_OFFHEAP_ENABLED_KEY = "spark.memory.offHeap.enabled"
 
-  val SPARK_ONHEAP_SIZE_KEY = "spark.executor.memory"
-
   val GLUTEN_ENABLED =
     buildConf("spark.gluten.enabled")
       .doc(
diff --git 
a/gluten-core/src/main/scala/org/apache/spark/util/SparkResourceUtil.scala 
b/gluten-core/src/main/scala/org/apache/spark/util/SparkResourceUtil.scala
index b25a174c12..c27000beca 100644
--- a/gluten-core/src/main/scala/org/apache/spark/util/SparkResourceUtil.scala
+++ b/gluten-core/src/main/scala/org/apache/spark/util/SparkResourceUtil.scala
@@ -105,9 +105,40 @@ object SparkResourceUtil extends Logging {
       val executorMemMib = conf.get(EXECUTOR_MEMORY)
       val factor =
         conf.getDouble(MEMORY_OVERHEAD_FACTOR, 0.1d)
-      val minMib = conf.getLong(MIN_MEMORY_OVERHEAD, 384L)
+      // spark.executor.minMemoryOverhead is a size string, MiB unless 
suffixed, so getLong would
+      // fail to parse anything carrying a unit. getSizeAsMb keeps the value 
in MiB, which is what
+      // the max below compares against.
+      val minMib = conf.getSizeAsMb(MIN_MEMORY_OVERHEAD, "384m")
       (executorMemMib * factor).toLong.max(minMib)
     }
-    ByteUnit.MiB.toBytes(overheadMib)
+    mibToBytes(overheadMib)
+  }
+
+  /**
+   * Returns spark.executor.memory in bytes.
+   *
+   * Spark declares the config as bytesConf(ByteUnit.MiB), so a value without 
a size suffix means
+   * MiB, and SparkConf#getSizeAsBytes must not be used to read it. This 
matches how Spark itself
+   * reads the config on YARN and K8s. Standalone and local-cluster differ: 
there Spark resolves it
+   * through the executor-memory-in-MiB path (SparkContext#executorMemoryInMb 
on Spark 3.4+), which
+   * treats a suffix-less value as bytes.
+   */
+  def getExecutorMemorySize(conf: SparkConf): Long = {
+    val memoryMib = conf.get(EXECUTOR_MEMORY)
+    require(memoryMib >= 0, s"${EXECUTOR_MEMORY.key} should not be negative, 
but was $memoryMib")
+    mibToBytes(memoryMib)
+  }
+
+  /**
+   * Converts a MiB-valued Spark memory amount to bytes.
+   *
+   * ByteUnit#toBytes rejects a negative input but wraps silently on overflow, 
while
+   * ByteUnit#convertTo raises on overflow but passes a negative through, so 
guard the sign here and
+   * let convertTo guard the magnitude. Callers that can name the offending 
config add their own
+   * require first so that the more specific message wins.
+   */
+  def mibToBytes(memoryMib: Long): Long = {
+    require(memoryMib >= 0, s"Memory size in MiB should not be negative, but 
was $memoryMib")
+    ByteUnit.MiB.convertTo(memoryMib, ByteUnit.BYTE)
   }
 }
diff --git 
a/gluten-core/src/test/scala/org/apache/gluten/GlutenDynamicOffHeapSizingSuite.scala
 
b/gluten-core/src/test/scala/org/apache/gluten/GlutenDynamicOffHeapSizingSuite.scala
new file mode 100644
index 0000000000..6095eca27b
--- /dev/null
+++ 
b/gluten-core/src/test/scala/org/apache/gluten/GlutenDynamicOffHeapSizingSuite.scala
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+import org.apache.gluten.component.WithDummyBackend
+import org.apache.gluten.config.GlutenCoreConfig
+
+import org.apache.spark.{SparkConf, SparkContext}
+
+import org.scalatest.funsuite.AnyFunSuite
+
+class GlutenDynamicOffHeapSizingSuite extends AnyFunSuite with 
WithDummyBackend {
+
+  private val MIB = 1024L * 1024L
+
+  test("dynamic off-heap sizing derives the budget from a suffix-less executor 
memory") {
+    // spark.executor.memory is MiB-unless-suffixed. Reading 8192 as bytes 
made the budget
+    // negative, so drive the real driver-init path and assert the derived 
budget. spark.testing
+    // zeroes Spark's reserved memory; without it UnifiedMemoryManager rejects 
8192 bytes before
+    // the plugin runs, which is the very read this test pins down.
+    val conf = new SparkConf(false)
+      .setAppName("GlutenDynamicOffHeapSizingSuite")
+      .set("spark.master", "local[1]")
+      .set("spark.plugins", classOf[GlutenPlugin].getName)
+      .set("spark.testing", "true")
+      .set("spark.ui.enabled", "false")
+      .set(GlutenCoreConfig.DYNAMIC_OFFHEAP_SIZING_ENABLED.key, "true")
+      .set("spark.executor.memory", "8192")
+    val sc = new SparkContext(conf)
+    try {
+      val expected = ((8192L * MIB - 300L * MIB) * 0.6d).toLong
+      assert(expected == 4965217075L)
+      assert(
+        
sc.getConf.getLong(GlutenCoreConfig.COLUMNAR_OFFHEAP_SIZE_IN_BYTES.key, -1L) == 
expected)
+      assert(
+        
sc.getConf.getLong(GlutenCoreConfig.COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES.key, 
-1L) > 0L)
+    } finally {
+      sc.stop()
+    }
+  }
+}
diff --git 
a/gluten-core/src/test/scala/org/apache/spark/util/SparkResourceUtilSuite.scala 
b/gluten-core/src/test/scala/org/apache/spark/util/SparkResourceUtilSuite.scala
index bad2153ddc..48d10abe9a 100644
--- 
a/gluten-core/src/test/scala/org/apache/spark/util/SparkResourceUtilSuite.scala
+++ 
b/gluten-core/src/test/scala/org/apache/spark/util/SparkResourceUtilSuite.scala
@@ -65,4 +65,89 @@ class SparkResourceUtilSuite extends AnyFunSuite {
     val conf = new SparkConf(false).set("spark.master", "local[8]")
     assert(SparkResourceUtil.getTaskSlots(conf) == 8)
   }
+
+  test("getExecutorMemorySize reads a bare spark.executor.memory as MiB") {
+    // Spark defines spark.executor.memory as bytesConf(ByteUnit.MiB), so a 
value without a size
+    // suffix means MiB. Reading it with SparkConf#getSizeAsBytes would treat 
8192 as 8192 bytes.
+    val conf = new SparkConf(false).set("spark.executor.memory", "8192")
+    assert(SparkResourceUtil.getExecutorMemorySize(conf) == 8192L * 1024 * 
1024)
+  }
+
+  test("getExecutorMemorySize honours a size suffix on spark.executor.memory") 
{
+    val conf = new SparkConf(false).set("spark.executor.memory", "8g")
+    assert(SparkResourceUtil.getExecutorMemorySize(conf) == 8L * 1024 * 1024 * 
1024)
+  }
+
+  test("getExecutorMemorySize falls back to the Spark default when unset") {
+    // spark.executor.memory defaults to 1g in Spark.
+    val conf = new SparkConf(false)
+    assert(SparkResourceUtil.getExecutorMemorySize(conf) == 1024L * 1024 * 
1024)
+  }
+
+  test("getExecutorMemorySize rejects an executor memory that overflows on 
conversion") {
+    // The MiB-to-byte conversion multiplies by 2^20, so a suffix-less byte 
count large enough to
+    // overflow must fail rather than wrap to a negative budget.
+    val conf = new SparkConf(false).set("spark.executor.memory", 
"9000000000000")
+    val e = 
intercept[IllegalArgumentException](SparkResourceUtil.getExecutorMemorySize(conf))
+    assert(e.getMessage.contains("exceeds Long.MAX_VALUE"))
+  }
+
+  test("getExecutorMemorySize rejects a negative executor memory") {
+    // Spark's typed entry carries no positivity check, and the conversion 
would pass a negative
+    // through, so guard it here rather than propagate a negative budget.
+    val conf = new SparkConf(false).set("spark.executor.memory", "-8192")
+    val e = 
intercept[IllegalArgumentException](SparkResourceUtil.getExecutorMemorySize(conf))
+    assert(e.getMessage.contains("spark.executor.memory should not be 
negative"))
+  }
+
+  test("getMemoryOverheadSize honours a size suffix on the minimum overhead") {
+    // spark.executor.minMemoryOverhead is a size string, so reading it with 
conf.getLong throws
+    // NumberFormatException on any value carrying a unit. 
VeloxListenerApi#onDriverStart calls this
+    // unconditionally, so that would abort driver startup on a value Spark 
itself accepts.
+    val conf = new SparkConf(false)
+      .set("spark.executor.memory", "1g")
+      .set("spark.executor.minMemoryOverhead", "512m")
+    assert(SparkResourceUtil.getMemoryOverheadSize(conf) == 512L * 1024 * 1024)
+  }
+
+  test("getMemoryOverheadSize falls back to the 384m minimum overhead") {
+    val conf = new SparkConf(false).set("spark.executor.memory", "1g")
+    assert(SparkResourceUtil.getMemoryOverheadSize(conf) == 384L * 1024 * 1024)
+  }
+
+  test("getMemoryOverheadSize prefers the factor when it exceeds the minimum") 
{
+    // 8g * 0.1 = 819 MiB, above the 384 MiB floor.
+    val conf = new SparkConf(false)
+      .set("spark.executor.memory", "8g")
+      .set("spark.executor.minMemoryOverhead", "512m")
+    assert(SparkResourceUtil.getMemoryOverheadSize(conf) == 819L * 1024 * 1024)
+  }
+
+  test("getMemoryOverheadSize reads a suffix-less minimum overhead as MiB") {
+    // The value users are most likely to already have. It read the same 
before the switch to
+    // getSizeAsMb, so nothing pinned it; a future accessor change could 
silently reinterpret it.
+    val conf = new SparkConf(false)
+      .set("spark.executor.memory", "1g")
+      .set("spark.executor.minMemoryOverhead", "512")
+    assert(SparkResourceUtil.getMemoryOverheadSize(conf) == 512L * 1024 * 1024)
+  }
+
+  test("getMemoryOverheadSize rejects an overhead that overflows on 
conversion") {
+    // spark.executor.memoryOverhead is MiB-declared and carries no magnitude 
check, so the
+    // MiB-to-byte conversion must raise rather than wrap to a negative budget.
+    val conf = new SparkConf(false)
+      .set("spark.executor.memory", "1g")
+      .set("spark.executor.memoryOverhead", "9000000000000")
+    val e = 
intercept[IllegalArgumentException](SparkResourceUtil.getMemoryOverheadSize(conf))
+    assert(e.getMessage.contains("exceeds Long.MAX_VALUE"))
+  }
+
+  test("getMemoryOverheadSize rejects a negative explicit overhead") {
+    // An explicit spark.executor.memoryOverhead skips the floor below, so 
guard the conversion.
+    val conf = new SparkConf(false)
+      .set("spark.executor.memory", "1g")
+      .set("spark.executor.memoryOverhead", "-1")
+    val e = 
intercept[IllegalArgumentException](SparkResourceUtil.getMemoryOverheadSize(conf))
+    assert(e.getMessage.contains("should not be negative"))
+  }
 }
diff --git 
a/gluten-substrait/src/main/scala/org/apache/spark/sql/execution/GlutenAutoAdjustStageResourceProfile.scala
 
b/gluten-substrait/src/main/scala/org/apache/spark/sql/execution/GlutenAutoAdjustStageResourceProfile.scala
index 90ecb4cc59..5f80d99dee 100644
--- 
a/gluten-substrait/src/main/scala/org/apache/spark/sql/execution/GlutenAutoAdjustStageResourceProfile.scala
+++ 
b/gluten-substrait/src/main/scala/org/apache/spark/sql/execution/GlutenAutoAdjustStageResourceProfile.scala
@@ -32,7 +32,7 @@ import org.apache.spark.sql.execution.adaptive.QueryStageExec
 import org.apache.spark.sql.execution.command.{DataWritingCommandExec, 
ExecutedCommandExec}
 import org.apache.spark.sql.execution.exchange.Exchange
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.util.SparkTestUtil
+import org.apache.spark.util.{SparkResourceUtil, SparkTestUtil}
 
 import scala.collection.mutable
 import scala.collection.mutable.ArrayBuffer
@@ -66,7 +66,8 @@ case class GlutenAutoAdjustStageResourceProfile(glutenConf: 
GlutenConfig, spark:
     // profile is applied, the settings will be updated accordingly.
     GlutenResourceProfile.updateResourceSetting(
       ResourceProfile.getOrCreateDefaultProfile(sparkConf),
-      sparkConf)
+      sparkConf,
+      isDefaultProfile = true)
     if (!plan.isInstanceOf[Exchange]) {
       // todo: support set resource profile for final stage
       return plan
@@ -183,19 +184,44 @@ object GlutenAutoAdjustStageResourceProfile extends 
Logging {
   }
 
   /**
-   * Reflects resource changes in some configurations that will be passed to 
the native side. It
-   * only affects the current thread.
+   * Reflects resource changes in some configurations that will be passed to 
the native side.
+   *
+   * The values are written into the active SQLConf. On the driver, outside a 
task and outside
+   * SQLConf#withExistingConf, that is the session's own conf, so the writes 
are visible to every
+   * thread using the session and outlive the query that triggered them.
    */
-  def updateResourceSetting(rp: ResourceProfile, sparkConf: SparkConf): Unit = 
{
-    val coresPerExecutor = 
rp.getExecutorCores.getOrElse(sparkConf.get(EXECUTOR_CORES))
-    val coresPerTask = rp.getTaskCpus.getOrElse(sparkConf.get(CPUS_PER_TASK))
-    val taskSlots = coresPerExecutor / coresPerTask
+  def updateResourceSetting(
+      rp: ResourceProfile,
+      sparkConf: SparkConf,
+      isDefaultProfile: Boolean = false): Unit = {
+    // Resource profiles never take effect in local mode, where a profile 
reports
+    // spark.executor.cores (1 by default) rather than the local[N] thread 
count that
+    // SparkResourceUtil and GlutenPlugin resolve. Defer to the shared 
resolver there so the rule
+    // and the plugin agree on the slot count; elsewhere the profile's own 
values are authoritative.
+    val taskSlots = if (SparkResourceUtil.isLocalMaster(sparkConf)) {
+      SparkResourceUtil.getTaskSlots(sparkConf)
+    } else {
+      val coresPerExecutor = 
rp.getExecutorCores.getOrElse(sparkConf.get(EXECUTOR_CORES))
+      val coresPerTask = rp.getTaskCpus.getOrElse(sparkConf.get(CPUS_PER_TASK))
+      require(coresPerTask > 0, s"${CPUS_PER_TASK.key} should be positive, but 
was $coresPerTask")
+      // Floor at one slot so the division below cannot throw on a combination 
Spark itself rejects
+      // later with a dedicated message.
+      Math.max(coresPerExecutor / coresPerTask, 1)
+    }
     val conf = SQLConf.get
     conf.setConfString(GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.key, 
taskSlots.toString)
-    val offHeapSize = rp.executorResources
-      .get(ResourceProfile.OFFHEAP_MEM)
-      .map(_.amount)
-      .getOrElse(sparkConf.get(MEMORY_OFFHEAP_SIZE))
+    // A resource profile records executor memory amounts in MiB, while the 
two configs written
+    // below are declared as bytesConf(ByteUnit.BYTE). The unmodified default 
profile carries the
+    // same off-heap size the conf does, only truncated to MiB, so read the 
conf directly there to
+    // keep this in step with what GlutenPlugin wrote at driver init.
+    val offHeapSize = if (isDefaultProfile) {
+      sparkConf.get(MEMORY_OFFHEAP_SIZE)
+    } else {
+      rp.executorResources
+        .get(ResourceProfile.OFFHEAP_MEM)
+        .map(request => SparkResourceUtil.mibToBytes(request.amount))
+        .getOrElse(sparkConf.get(MEMORY_OFFHEAP_SIZE))
+    }
     conf.setConfString(GlutenCoreConfig.COLUMNAR_OFFHEAP_SIZE_IN_BYTES.key, 
offHeapSize.toString)
     conf.setConfString(
       GlutenCoreConfig.COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES.key,
diff --git 
a/gluten-substrait/src/test/scala/org/apache/spark/sql/execution/GlutenAutoAdjustStageResourceProfileSuite.scala
 
b/gluten-substrait/src/test/scala/org/apache/spark/sql/execution/GlutenAutoAdjustStageResourceProfileSuite.scala
new file mode 100644
index 0000000000..7dc4321261
--- /dev/null
+++ 
b/gluten-substrait/src/test/scala/org/apache/spark/sql/execution/GlutenAutoAdjustStageResourceProfileSuite.scala
@@ -0,0 +1,129 @@
+/*
+ * 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.execution
+
+import org.apache.gluten.config.GlutenCoreConfig
+
+import org.apache.spark.SparkConf
+import org.apache.spark.resource.{ExecutorResourceRequests, ResourceProfile, 
TaskResourceRequests}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.util.SparkResourceUtil
+
+import org.scalatest.funsuite.AnyFunSuite
+
+class GlutenAutoAdjustStageResourceProfileSuite extends AnyFunSuite {
+
+  private val MIB = 1024L * 1024L
+
+  private def clusterConf(offHeap: String): SparkConf = new SparkConf(false)
+    .set("spark.master", "yarn")
+    .set("spark.executor.cores", "4")
+    .set("spark.task.cpus", "1")
+    .set("spark.memory.offHeap.enabled", "true")
+    .set("spark.memory.offHeap.size", offHeap)
+
+  private def profileWith(cores: Int, taskCpus: Int, offHeap: Option[String]): 
ResourceProfile = {
+    val ereqs = new ExecutorResourceRequests()
+    ereqs.cores(cores)
+    offHeap.foreach(ereqs.offHeapMemory)
+    val treqs = new TaskResourceRequests()
+    treqs.cpus(taskCpus)
+    new ResourceProfile(ereqs.requests, treqs.requests)
+  }
+
+  test("updateResourceSetting converts the profile's MiB amount to bytes") {
+    // A ResourceProfile records executor memory in MiB, while the configs 
written here are declared
+    // as bytesConf(ByteUnit.BYTE). Writing the amount verbatim shrank the 
off-heap budget by 2^20,
+    // so 20g became 20480 bytes.
+    val rp = profileWith(cores = 4, taskCpus = 1, offHeap = Some("20g"))
+    SQLConf.withExistingConf(new SQLConf) {
+      GlutenAutoAdjustStageResourceProfile.updateResourceSetting(rp, 
clusterConf("20g"))
+      val conf = SQLConf.get
+      
assert(conf.getConfString(GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.key) == 
"4")
+      assert(
+        
conf.getConfString(GlutenCoreConfig.COLUMNAR_OFFHEAP_SIZE_IN_BYTES.key) ==
+          (20480L * MIB).toString)
+      assert(
+        
conf.getConfString(GlutenCoreConfig.COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES.key) ==
+          (20480L * MIB / 4).toString)
+    }
+  }
+
+  test("updateResourceSetting reads the exact off-heap size for the default 
profile") {
+    // The default profile's amount is the conf value truncated to MiB, so 
going through the profile
+    // would shrink a size that is not a whole number of MiB. 
spark.memory.offHeap.size=1536k must
+    // stay at 1572864, the value GlutenPlugin already wrote at driver init.
+    val sparkConf = clusterConf("1536k")
+    val rp = ResourceProfile.getOrCreateDefaultProfile(sparkConf)
+    SQLConf.withExistingConf(new SQLConf) {
+      GlutenAutoAdjustStageResourceProfile
+        .updateResourceSetting(rp, sparkConf, isDefaultProfile = true)
+      assert(
+        
SQLConf.get.getConfString(GlutenCoreConfig.COLUMNAR_OFFHEAP_SIZE_IN_BYTES.key) 
== "1572864")
+    }
+  }
+
+  test("updateResourceSetting agrees with getTaskSlots in local mode") {
+    // A profile reports spark.executor.cores, 1 by default, while 
GlutenPlugin resolves local[4] to
+    // four slots. Deriving the count from the profile there made every task 
believe it owned the
+    // whole off-heap budget.
+    val sparkConf = new SparkConf(false)
+      .set("spark.master", "local[4]")
+      .set("spark.memory.offHeap.enabled", "true")
+      .set("spark.memory.offHeap.size", "20g")
+    val rp = profileWith(cores = 1, taskCpus = 1, offHeap = Some("20g"))
+    SQLConf.withExistingConf(new SQLConf) {
+      GlutenAutoAdjustStageResourceProfile.updateResourceSetting(rp, sparkConf)
+      val conf = SQLConf.get
+      val expectedSlots = SparkResourceUtil.getTaskSlots(sparkConf)
+      assert(expectedSlots == 4)
+      
assert(conf.getConfString(GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.key) == 
"4")
+      assert(
+        
conf.getConfString(GlutenCoreConfig.COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES.key) ==
+          (20480L * MIB / 4).toString)
+    }
+  }
+
+  test("updateResourceSetting floors the slot count at one") {
+    // spark.task.cpus greater than the executor cores is a combination Spark 
rejects later with a
+    // dedicated message, but the division below would throw first on a zero 
quotient.
+    val rp = profileWith(cores = 1, taskCpus = 2, offHeap = Some("20g"))
+    SQLConf.withExistingConf(new SQLConf) {
+      GlutenAutoAdjustStageResourceProfile.updateResourceSetting(rp, 
clusterConf("20g"))
+      val conf = SQLConf.get
+      
assert(conf.getConfString(GlutenCoreConfig.NUM_TASK_SLOTS_PER_EXECUTOR.key) == 
"1")
+      assert(
+        
conf.getConfString(GlutenCoreConfig.COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES.key) ==
+          (20480L * MIB).toString)
+    }
+  }
+
+  test("updateResourceSetting rejects a non-positive task cpus") {
+    // The profile's own task cpus wins when present, as the two tests above 
rely on, so the profile
+    // here carries none and the conf fallback decides.
+    val ereqs = new ExecutorResourceRequests()
+    ereqs.cores(4)
+    ereqs.offHeapMemory("20g")
+    val rp = new ResourceProfile(ereqs.requests, Map.empty)
+    val sparkConf = clusterConf("20g").set("spark.task.cpus", "0")
+    SQLConf.withExistingConf(new SQLConf) {
+      val e = intercept[IllegalArgumentException](
+        GlutenAutoAdjustStageResourceProfile.updateResourceSetting(rp, 
sparkConf))
+      assert(e.getMessage.contains("spark.task.cpus should be positive"))
+    }
+  }
+}


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

Reply via email to