@@ -481,23 +481,43 @@ object GpuDeviceManager extends Logging {
481481 }
482482 // Host memory limits must be set after the pinned memory pool is initialized
483483 HostAlloc .initialize(nonPinnedLimit)
484- // Fill the MULTITHREAD_READ_MEMORY_LIMIT_SIZE with the 90% of the total OFF_HEAP memory
485- // if it is not set already.
484+ // Fill the MULTITHREAD_READ_MEMORY_LIMIT_SIZE with 90% of the total off-heap memory
485+ // if it is not set already. When off-heap limit tracking is disabled (nonPinnedLimit == -1),
486+ // falls back to a hardware-derived estimate via computeEffectiveOffHeapLimit.
486487 if (conf.multiThreadReadMemoryLimit == 0 ) {
487488 sparkConf.set(RapidsConf .MULTITHREAD_READ_MEMORY_LIMIT_SIZE .key,
488- (0.9 * (pinnedSize + nonPinnedLimit)).toLong.toString)
489+ computeMtReadLimit(pinnedSize, nonPinnedLimit, conf, sparkConf,
490+ Cuda .getDeviceCount).toString)
489491 }
490492 }
491493
492494 // visible for testing
493- def getPinnedPoolAndOffHeapLimits (conf : RapidsConf , sparkConf : SparkConf , deviceCount : Int ,
494- memCheck : MemoryChecker =
495- MemoryCheckerImpl ): (Long , Long ) = {
496- val perTaskOverhead = conf.perTaskOverhead
497- val totalOverhead = perTaskOverhead * GpuDeviceManager .numCores
498- val confPinnedSize = conf.pinnedPoolSize
495+ def computeMtReadLimit (pinnedSize : Long , nonPinnedLimit : Long ,
496+ conf : RapidsConf , sparkConf : SparkConf , deviceCount : Int ,
497+ memCheck : MemoryChecker = MemoryCheckerImpl ): Long = {
498+ val totalOffHeap = if (nonPinnedLimit >= 0 ) {
499+ pinnedSize + nonPinnedLimit
500+ } else {
501+ // nonPinnedLimit == -1 means off-heap limit tracking is disabled (unlimited).
502+ // Derive a concrete total from hardware so the MT read limit is still meaningful.
503+ // Note: this feature is off by default
504+ // (spark.rapids.sql.multiThreadedRead.memoryLimit.enabled defaults to false), so the risk
505+ // of this approximation affecting users is low. The
506+ // underlying hardware-derived calculation has known limitations — in particular, it may not
507+ // account correctly for spark.memory.offHeap.size in all environments (see
508+ // https://github.com/NVIDIA/spark-rapids/issues/13628). For now we reuse the same
509+ // best-effort estimate rather than special-casing this path.
510+ computeEffectiveOffHeapLimit(conf, sparkConf, deviceCount, memCheck)
511+ }
512+ val limit = (0.9 * totalOffHeap).toLong
513+ logInfo(s " Setting MT read memory limit to ${limit / 1024 / 1024.0 } MiB " +
514+ s " (90% of ${totalOffHeap / 1024 / 1024.0 } MiB total off-heap) " )
515+ limit
516+ }
517+
518+ private def computeEffectiveOffHeapLimit (conf : RapidsConf , sparkConf : SparkConf ,
519+ deviceCount : Int , memCheck : MemoryChecker ): Long = {
499520 val confLimit = conf.offHeapLimit
500- val confLimitEnabled = conf.offHeapLimitEnabled
501521 // This min limit of 4GB is somewhat arbitrary, but based on some testing which showed
502522 // that the previous minimum of 15 MB * num cores was too little for certain benchmark
503523 // queries to complete, whereas this limit was sufficient.
@@ -521,68 +541,87 @@ object GpuDeviceManager extends Logging {
521541 0L
522542 }
523543
524- if (confLimitEnabled) {
525- val memoryLimit = if (confLimit.isDefined) {
526- if (executorOverhead.isEmpty) {
527- logWarning(s " $executorOverheadKey is not set " )
528- }
529- logInfo(s " using configured ${RapidsConf .OFF_HEAP_LIMIT_SIZE } of ${confLimit.get}" )
530- confLimit.get
544+ if (confLimit.isDefined) {
545+ if (executorOverhead.isEmpty) {
546+ logWarning(s " $executorOverheadKey is not set " )
547+ }
548+ logInfo(s " using configured ${RapidsConf .OFF_HEAP_LIMIT_SIZE } of ${confLimit.get}" )
549+ confLimit.get
550+ } else {
551+ // in case we cannot query the host for available memory due to environmental
552+ // constraints, getAvailableMemoryBytes returns None and we fall back to 0, which causes
553+ // basedOnHostMemory to go negative and trip the minMemoryLimit floor below.
554+ lazy val availableHostMemory = if (isIntegratedGpu) {
555+ (memCheck.getAvailableMemoryBytes(conf).getOrElse(0L ) * (1.0 -
556+ conf.integratedGpuMemoryFraction)).toLong
531557 } else {
532- // in case we cannot query the host for available memory due to environmental
533- // constraints, we can fall back to minMemoryLimit via saying there's no available
534- lazy val availableHostMemory = if (isIntegratedGpu) {
535- (memCheck.getAvailableMemoryBytes(conf).getOrElse(0L ) * (1.0 -
536- conf.integratedGpuMemoryFraction)).toLong
558+ memCheck.getAvailableMemoryBytes(conf).getOrElse(0L )
559+ }
560+ val hostMemUsageFraction = .8
561+ // Spark calculates the total mem to allocate to the job as
562+ // val totalMemMiB =
563+ // executorMemoryMiB + memoryOverheadMiB + memoryOffHeapMiB + pysparkMemToUseMiB
564+ // and RAPIDS uses memory from the overhead portion here. Therefore, if the overhead is
565+ // set we can just use that, otherwise we can infer it from the above as
566+ // val memoryOverheadMiB =
567+ // totalMemMiB - executorMemoryMiB - memoryOffHeapMiB - pysparkMemToUseMiB
568+ // where totalMemMiB is instead derived from the actual mem limits we can observe
569+ // directly from the system.
570+ // Note: subtracting sparkOffHeapSize here is an approximation. In some environments
571+ // (e.g. Databricks) spark.memory.offHeap.size is set to a large value and RAPIDS is
572+ // expected to operate within that allocation rather than outside it, so subtracting it
573+ // double-counts the memory and under-estimates the available limit. Fixing this properly
574+ // may require hooking into Spark's MemoryManager. See
575+ // https://github.com/NVIDIA/spark-rapids/issues/13628
576+ lazy val basedOnHostMemory = (hostMemUsageFraction * ((1.0 * availableHostMemory /
577+ deviceCount) - heapSize - pysparkOverhead - sparkOffHeapSize)).toLong
578+ if (executorOverhead.isDefined) {
579+ val basedOnConfiguredOverhead = executorOverhead.get
580+ logWarning(s " ${RapidsConf .OFF_HEAP_LIMIT_SIZE } is not set; we derived " +
581+ s " a memory limit from ( $executorOverheadKey = ${executorOverhead.get}" )
582+ if (basedOnConfiguredOverhead < minMemoryLimit) {
583+ logWarning(s " memory limit $basedOnConfiguredOverhead is less than the minimum of " +
584+ s " $minMemoryLimit; using the latter " )
585+ if (minMemoryLimit > basedOnHostMemory) {
586+ logWarning(s " the amount of available memory detected on the host is " +
587+ s " $availableHostMemory, based off of which we computed a limit of " +
588+ s " $basedOnHostMemory, which is less than the minimum $minMemoryLimit, " +
589+ s " so we are using the minimum $minMemoryLimit" )
590+ }
591+ minMemoryLimit
537592 } else {
538- memCheck.getAvailableMemoryBytes(conf).getOrElse( 0L )
593+ basedOnConfiguredOverhead
539594 }
540- val hostMemUsageFraction = .8
541- // Spark calculates the total mem to allocate to the job as
542- // val totalMemMiB =
543- // executorMemoryMiB + memoryOverheadMiB + memoryOffHeapMiB + pysparkMemToUseMiB
544- // and RAPIDS uses memory from the overhead portion here. Therefore, if the overhead is
545- // set we can just use that, otherwise we can infer it from the above as
546- // val memoryOverheadMiB =
547- // totalMemMiB - executorMemoryMiB - memoryOffHeapMiB - pysparkMemToUseMiB
548- // where totalMemMiB is instead derived from the actual mem limits we can observe
549- // directly from the system
550- lazy val basedOnHostMemory = (hostMemUsageFraction * ((1.0 * availableHostMemory /
551- deviceCount) - heapSize - pysparkOverhead - sparkOffHeapSize)).toLong
552- if (executorOverhead.isDefined) {
553- val basedOnConfiguredOverhead = executorOverhead.get
554- logWarning(s " ${RapidsConf .OFF_HEAP_LIMIT_SIZE } is not set; we derived " +
555- s " a memory limit from ( $executorOverheadKey = ${executorOverhead.get}" )
556- if (basedOnConfiguredOverhead < minMemoryLimit) {
557- logWarning(s " memory limit $basedOnConfiguredOverhead is less than the minimum of " +
558- s " $minMemoryLimit; using the latter " )
559- if (minMemoryLimit > basedOnHostMemory) {
560- logWarning(s " the amount of available memory detected on the host is " +
561- s " $availableHostMemory, based off of which we computed a limit of " +
562- s " $basedOnHostMemory, which is less than the minimum $minMemoryLimit, " +
563- s " so we are using the minimum $minMemoryLimit" )
564- }
565- minMemoryLimit
566- } else {
567- basedOnConfiguredOverhead
568- }
595+ } else {
596+ logWarning(s " ${RapidsConf .OFF_HEAP_LIMIT_SIZE } is not set; we used " +
597+ s " memory limit derived from ( $hostMemUsageFraction * (estimated available " +
598+ s " host memory / device count) - $heapSizeKey - $pysparkOverheadKey - " +
599+ s " $sparkOffHeapSizeKey) = ( $hostMemUsageFraction * ( $availableHostMemory / " +
600+ s " $deviceCount) - $heapSize - $pysparkOverhead - $sparkOffHeapSize) = " +
601+ s " $basedOnHostMemory" )
602+ if (basedOnHostMemory < minMemoryLimit) {
603+ logWarning(s " the memory limit, $basedOnHostMemory, based on the available " +
604+ s " host memory of $availableHostMemory, is less than the minimum of " +
605+ s " $minMemoryLimit; using the latter $minMemoryLimit" )
606+ minMemoryLimit
569607 } else {
570- logWarning(s " ${RapidsConf .OFF_HEAP_LIMIT_SIZE } is not set; we used " +
571- s " memory limit derived from ( $hostMemUsageFraction * (estimated available " +
572- s " host memory / device count) - $heapSizeKey - $pysparkOverheadKey - " +
573- s " $sparkOffHeapSizeKey) = ( $hostMemUsageFraction * ( $availableHostMemory / " +
574- s " $deviceCount) - $heapSize - $pysparkOverhead - $sparkOffHeapSize) = " +
575- s " $basedOnHostMemory" )
576- if (basedOnHostMemory < minMemoryLimit) {
577- logWarning(s " the memory limit, $basedOnHostMemory, based on the available " +
578- s " host memory of $availableHostMemory, is less than the minimum of " +
579- s " $minMemoryLimit; using the latter $minMemoryLimit" )
580- minMemoryLimit
581- } else {
582- basedOnHostMemory
583- }
608+ basedOnHostMemory
584609 }
585610 }
611+ }
612+ }
613+
614+ // visible for testing
615+ def getPinnedPoolAndOffHeapLimits (conf : RapidsConf , sparkConf : SparkConf , deviceCount : Int ,
616+ memCheck : MemoryChecker =
617+ MemoryCheckerImpl ): (Long , Long ) = {
618+ val perTaskOverhead = conf.perTaskOverhead
619+ val totalOverhead = perTaskOverhead * GpuDeviceManager .numCores
620+ val confPinnedSize = conf.pinnedPoolSize
621+ val confLimitEnabled = conf.offHeapLimitEnabled
622+
623+ if (confLimitEnabled) {
624+ val memoryLimit = computeEffectiveOffHeapLimit(conf, sparkConf, deviceCount, memCheck)
586625
587626 // Now we need to know the pinned vs non-pinned limits
588627 val pinnedLimit = if (confPinnedSize + totalOverhead <= memoryLimit) {
0 commit comments