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 4876f4e551 [GLUTEN-13093][CORE] Fix two dynamic off-heap sizing memory 
bugs (#13094)
4876f4e551 is described below

commit 4876f4e551b21a2db4995c0d7f4023ca6481d88f
Author: YangJie <[email protected]>
AuthorDate: Wed Oct 21 03:09:20 2026 -0400

    [GLUTEN-13093][CORE] Fix two dynamic off-heap sizing memory bugs (#13094)
---
 .../DynamicOffHeapSizingMemoryTarget.java          | 21 ++++++--
 .../org/apache/spark/memory/SparkMemoryUtil.scala  | 23 +++++++--
 .../DynamicOffHeapSizingMemoryTargetTest.java      | 41 +++++++++++++++
 .../gluten/GlutenDynamicOffHeapSizingSuite.scala   | 59 +++++++++++++++++++++-
 4 files changed, 136 insertions(+), 8 deletions(-)

diff --git 
a/gluten-core/src/main/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTarget.java
 
b/gluten-core/src/main/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTarget.java
index ed5be45e8b..1341f18984 100644
--- 
a/gluten-core/src/main/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTarget.java
+++ 
b/gluten-core/src/main/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTarget.java
@@ -385,6 +385,7 @@ public class DynamicOffHeapSizingMemoryTarget implements 
MemoryTarget, KnownName
 
   public static long shrinkOnHeapMemory(long totalMemory, long freeMemory, 
boolean isAsyncGc) {
     boolean updateMaxHeapFreeRatio = false;
+    boolean updateMinHeapFreeRatio = false;
     Object hotSpotBean = null;
     String maxHeapFreeRatioName = "MaxHeapFreeRatio";
     String minHeapFreeRatioName = "MinHeapFreeRatio";
@@ -401,6 +402,7 @@ public class DynamicOffHeapSizingMemoryTarget implements 
MemoryTarget, KnownName
       if (newValue < ORIGINAL_MIN_HEAP_FREE_RATIO) {
         // Adjust the MinHeapFreeRatio to avoid the violation of the 
MaxHeapFreeRatio.
         setOption.invoke(hotSpotBean, minHeapFreeRatioName, 
Integer.toString(newValue));
+        updateMinHeapFreeRatio = true;
       }
       if (newValue < ORIGINAL_MAX_HEAP_FREE_RATIO) {
         setOption.invoke(hotSpotBean, maxHeapFreeRatioName, 
Integer.toString(newValue));
@@ -416,16 +418,29 @@ public class DynamicOffHeapSizingMemoryTarget implements 
MemoryTarget, KnownName
           "Failed to update JVM heap free ratio via HotSpotDiagnosticMXBean: 
{}", e.toString());
       return totalMemory;
     } finally {
-      // Reset the MaxHeapFreeRatio to the original values.
+      // Reset both heap free ratios to the original values. Each revert is
+      // best-effort on its own: a failing Max revert must not silently skip
+      // the Min revert, which would leave the same permanent mutation behind.
       if (hotSpotBean != null && updateMaxHeapFreeRatio) {
         try {
           Class<?> beanClass = 
Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
           Method setOption = beanClass.getMethod("setVMOption", String.class, 
String.class);
           setOption.invoke(
               hotSpotBean, maxHeapFreeRatioName, 
Integer.toString(ORIGINAL_MAX_HEAP_FREE_RATIO));
-          LOG.info("Reverted VM flags back.");
+          LOG.info("Reverted MaxHeapFreeRatio back.");
         } catch (Exception ignore) {
-          // best‐effort revert
+          // best-effort revert
+        }
+      }
+      if (hotSpotBean != null && updateMinHeapFreeRatio) {
+        try {
+          Class<?> beanClass = 
Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
+          Method setOption = beanClass.getMethod("setVMOption", String.class, 
String.class);
+          setOption.invoke(
+              hotSpotBean, minHeapFreeRatioName, 
Integer.toString(ORIGINAL_MIN_HEAP_FREE_RATIO));
+          LOG.info("Reverted MinHeapFreeRatio back.");
+        } catch (Exception ignore) {
+          // best-effort revert
         }
       }
     }
diff --git 
a/gluten-core/src/main/scala/org/apache/spark/memory/SparkMemoryUtil.scala 
b/gluten-core/src/main/scala/org/apache/spark/memory/SparkMemoryUtil.scala
index a902c448fe..372e27debf 100644
--- a/gluten-core/src/main/scala/org/apache/spark/memory/SparkMemoryUtil.scala
+++ b/gluten-core/src/main/scala/org/apache/spark/memory/SparkMemoryUtil.scala
@@ -16,6 +16,7 @@
  */
 package org.apache.spark.memory
 
+import org.apache.gluten.config.GlutenCoreConfig
 import org.apache.gluten.memory.memtarget._
 import org.apache.gluten.memory.memtarget.spark.{RegularMemoryConsumer, 
TreeMemoryConsumer}
 import org.apache.gluten.proto.MemoryUsageStats
@@ -35,8 +36,9 @@ object SparkMemoryUtil {
   private val mmClazz = classOf[MemoryManager]
   private val smpField = mmClazz.getDeclaredField("offHeapStorageMemoryPool")
   private val empField = mmClazz.getDeclaredField("offHeapExecutionMemoryPool")
-  smpField.setAccessible(true)
-  empField.setAccessible(true)
+  private val onHeapSmpField = 
mmClazz.getDeclaredField("onHeapStorageMemoryPool")
+  private val onHeapEmpField = 
mmClazz.getDeclaredField("onHeapExecutionMemoryPool")
+  Seq(smpField, empField, onHeapSmpField, 
onHeapEmpField).foreach(_.setAccessible(true))
 
   private val tmmClazz = classOf[TaskMemoryManager]
   private val consumersField = tmmClazz.getDeclaredField("consumers")
@@ -51,8 +53,21 @@ object SparkMemoryUtil {
   // We assume storage memory can be fully transferred to execution memory so 
far
   def getCurrentAvailableOffHeapMemory: Long = {
     val mm = SparkEnv.get.memoryManager
-    val smp = smpField.get(mm).asInstanceOf[StorageMemoryPool]
-    val emp = empField.get(mm).asInstanceOf[ExecutionMemoryPool]
+    // With dynamic off-heap sizing enabled, Gluten's global reservations are
+    // charged to the ON-heap pools (see GlobalOffHeapMemoryTarget), so the
+    // available figure must be read from the same pools. Read the flag the 
same
+    // way GlobalOffHeapMemoryTarget.mode does, so the two cannot pick 
different
+    // pool families.
+    val (smp, emp) =
+      if (GlutenCoreConfig.get.dynamicOffHeapSizingEnabled) {
+        (
+          onHeapSmpField.get(mm).asInstanceOf[StorageMemoryPool],
+          onHeapEmpField.get(mm).asInstanceOf[ExecutionMemoryPool])
+      } else {
+        (
+          smpField.get(mm).asInstanceOf[StorageMemoryPool],
+          empField.get(mm).asInstanceOf[ExecutionMemoryPool])
+      }
     smp.memoryFree + emp.memoryFree
   }
 
diff --git 
a/gluten-core/src/test/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTargetTest.java
 
b/gluten-core/src/test/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTargetTest.java
index c2f39b5fd0..bace71dbc6 100644
--- 
a/gluten-core/src/test/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTargetTest.java
+++ 
b/gluten-core/src/test/java/org/apache/gluten/memory/memtarget/DynamicOffHeapSizingMemoryTargetTest.java
@@ -16,11 +16,18 @@
  */
 package org.apache.gluten.memory.memtarget;
 
+import com.sun.management.VMOption;
 import org.junit.After;
 import org.junit.Assert;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 
+import javax.management.MBeanServer;
+
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Method;
+
 /**
  * Ledger-correctness tests for {@link DynamicOffHeapSizingMemoryTarget}.
  *
@@ -62,6 +69,40 @@ public class DynamicOffHeapSizingMemoryTargetTest {
         DynamicOffHeapSizingMemoryTarget.usedOffHeapBytesForTesting());
   }
 
+  @Test
+  public void shrinkOnHeapMemoryRestoresHeapFreeRatios() throws Exception {
+    final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
+    final Class<?> beanClass = 
Class.forName("com.sun.management.HotSpotDiagnosticMXBean");
+    final Object bean =
+        ManagementFactory.newPlatformMXBeanProxy(
+            mbs, "com.sun.management:type=HotSpotDiagnostic", beanClass);
+    final Method getOption = beanClass.getMethod("getVMOption", String.class);
+    final String minBefore = ((VMOption) getOption.invoke(bean, 
"MinHeapFreeRatio")).getValue();
+    final String maxBefore = ((VMOption) getOption.invoke(bean, 
"MaxHeapFreeRatio")).getValue();
+
+    // The shrink only lowers the ratios when the original MinHeapFreeRatio is
+    // above the target (5); skip on JVMs configured below it, where there is
+    // nothing to restore. On JVMs where the VM option is not writable (e.g.
+    // JDK 8) the production code leaves the flags untouched and the assertions
+    // below hold trivially.
+    Assume.assumeTrue(
+        "JVM launched with MinHeapFreeRatio <= 5; nothing to restore",
+        Integer.parseInt(minBefore) > 5);
+
+    // isAsyncGc=true runs a single System.gc() and may set the async-GC 
suspend
+    // flag for the remainder of this fork; no other test reads it.
+    DynamicOffHeapSizingMemoryTarget.shrinkOnHeapMemory(
+        Runtime.getRuntime().totalMemory(), Runtime.getRuntime().freeMemory(), 
true);
+
+    final String minAfter = ((VMOption) getOption.invoke(bean, 
"MinHeapFreeRatio")).getValue();
+    final String maxAfter = ((VMOption) getOption.invoke(bean, 
"MaxHeapFreeRatio")).getValue();
+    // The finally block lowered the heap free ratios for the shrink and must
+    // restore both; leaving MinHeapFreeRatio lowered makes the JVM shrink its
+    // heap aggressively forever after.
+    Assert.assertEquals(minBefore, minAfter);
+    Assert.assertEquals(maxBefore, maxAfter);
+  }
+
   @Test
   public void borrowRecordsOnlyTheGrantedAmountForFullGrants() {
     final CountingTarget wrapped = new CountingTarget(Long.MAX_VALUE);
diff --git 
a/gluten-core/src/test/scala/org/apache/gluten/GlutenDynamicOffHeapSizingSuite.scala
 
b/gluten-core/src/test/scala/org/apache/gluten/GlutenDynamicOffHeapSizingSuite.scala
index 6095eca27b..4337f6ed53 100644
--- 
a/gluten-core/src/test/scala/org/apache/gluten/GlutenDynamicOffHeapSizingSuite.scala
+++ 
b/gluten-core/src/test/scala/org/apache/gluten/GlutenDynamicOffHeapSizingSuite.scala
@@ -19,7 +19,9 @@ package org.apache.gluten
 import org.apache.gluten.component.WithDummyBackend
 import org.apache.gluten.config.GlutenCoreConfig
 
-import org.apache.spark.{SparkConf, SparkContext}
+import org.apache.spark.{SparkConf, SparkContext, SparkEnv}
+import org.apache.spark.memory.SparkMemoryUtil
+import org.apache.spark.sql.internal.SQLConf
 
 import org.scalatest.funsuite.AnyFunSuite
 
@@ -52,4 +54,59 @@ class GlutenDynamicOffHeapSizingSuite extends AnyFunSuite 
with WithDummyBackend
       sc.stop()
     }
   }
+
+  // Pure reflection: the pool classes are private[spark] and not referable
+  // from this package at compile time.
+  private def poolFamilyFreeSum(onHeap: Boolean): Long = {
+    val mm = SparkEnv.get.memoryManager
+    def poolFree(fieldName: String): Long = {
+      // scalastyle:off classforname
+      val mmClazz = Class.forName("org.apache.spark.memory.MemoryManager")
+      // scalastyle:on classforname
+      val field = mmClazz.getDeclaredField(fieldName)
+      field.setAccessible(true)
+      val pool = field.get(mm)
+      val memoryFree = pool.getClass.getMethod("memoryFree")
+      memoryFree.setAccessible(true)
+      memoryFree.invoke(pool).asInstanceOf[Long]
+    }
+    poolFree(if (onHeap) "onHeapStorageMemoryPool" else 
"offHeapStorageMemoryPool") +
+      poolFree(if (onHeap) "onHeapExecutionMemoryPool" else 
"offHeapExecutionMemoryPool")
+  }
+
+  test("available off-heap metric follows the pool family the dynamic mode 
reserves from") {
+    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("spark.memory.offHeap.enabled", "true")
+      .set("spark.memory.offHeap.size", "512m")
+    val sc = new SparkContext(conf)
+    try {
+      // Default mode: global reservations are charged to the off-heap pools, 
so
+      // the metric must equal exactly those pools' free memory.
+      assert(SparkMemoryUtil.getCurrentAvailableOffHeapMemory == 
poolFamilyFreeSum(false))
+    } finally {
+      sc.stop()
+    }
+
+    val dynamicConf = 
conf.clone().set(GlutenCoreConfig.DYNAMIC_OFFHEAP_SIZING_ENABLED.key, "true")
+    val dynamicSc = new SparkContext(dynamicConf)
+    try {
+      // Dynamic mode: reservations land in the ON-heap pools
+      // (GlobalOffHeapMemoryTarget), so the metric must equal the on-heap free
+      // sum - reading the off-heap pools (the old behavior) diverges as soon
+      // as any reservation is charged. Both read the flag through 
GlutenCoreConfig,
+      // i.e. the active SQLConf; with no session here, supply it in scope.
+      val sqlConf = new SQLConf
+      
sqlConf.setConfString(GlutenCoreConfig.DYNAMIC_OFFHEAP_SIZING_ENABLED.key, 
"true")
+      SQLConf.withExistingConf(sqlConf) {
+        assert(SparkMemoryUtil.getCurrentAvailableOffHeapMemory == 
poolFamilyFreeSum(true))
+      }
+    } finally {
+      dynamicSc.stop()
+    }
+  }
 }


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

Reply via email to