11/*
2- * Copyright (c) 2023-2025 , NVIDIA CORPORATION.
2+ * Copyright (c) 2023-2026 , NVIDIA CORPORATION.
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1616
1717package com .nvidia .spark .rapids .window
1818
19- import ai .rapids .cudf .{ ColumnVector => CudfColumnVector , Table => CudfTable }
19+ import ai .rapids .cudf .Table
2020import com .nvidia .spark .rapids ._
21- import com .nvidia .spark .rapids .Arm .withResource
21+ import com .nvidia .spark .rapids .Arm .{closeOnExcept , withResource }
22+ import com .nvidia .spark .rapids .RapidsPluginImplicits ._
23+ import com .nvidia .spark .rapids .RmmRapidsRetryIterator .withRetryNoSplit
2224import com .nvidia .spark .rapids .ScalableTaskCompletion .onTaskCompletion
2325
2426import org .apache .spark .TaskContext
@@ -46,7 +48,8 @@ class GpuBatchedBoundedWindowIterator(
4648
4749 override def hasNext : Boolean = numUnprocessedInCache > 0 || input.hasNext
4850
49- var cached : Option [Array [CudfColumnVector ]] = None // For processing with the next batch.
51+ // For processing with the next batch.
52+ private var cached : Option [SpillableColumnarBatch ] = None
5053
5154 private var numUnprocessedInCache : Int = 0 // numRows at the bottom not processed completely.
5255 private var numPrecedingRowsAdded : Int = 0 // numRows at the top, added for preceding context.
@@ -59,151 +62,140 @@ class GpuBatchedBoundedWindowIterator(
5962 }
6063
6164 // Caches input column schema on first read.
62- var inputTypes : Option [Array [DataType ]] = None
65+ private var inputTypes : Option [Array [DataType ]] = None
6366
64- // Clears cached column vectors, after consumption.
65- private def clearCached (): Unit = {
66- cached.foreach(_.foreach(_. close))
67+ // Clears the cache after consumption.
68+ private [rapids] def clearCached (): Unit = {
69+ cached.foreach(_.close( ))
6770 cached = None
6871 }
6972
70- private def getNextInputBatch : SpillableColumnarBatch = {
71- // Sets column batch types using the types cached from the
72- // first input column read.
73- def optionallySetInputTypes (inputCB : ColumnarBatch ): Unit = {
74- if (inputTypes.isEmpty) {
75- inputTypes = Some (GpuColumnVector .extractTypes(inputCB))
76- }
77- }
78-
79- // Reads fresh batch from iterator, initializes input data-types if necessary.
80- def getFreshInputBatch : ColumnarBatch = {
81- val fresh_batch = input.next()
82- optionallySetInputTypes(fresh_batch)
83- fresh_batch
84- }
85-
86- def concatenateColumns (cached : Array [CudfColumnVector ],
87- freshBatchTable : CudfTable )
88- : Array [CudfColumnVector ] = {
89-
90- if (cached.length != freshBatchTable.getNumberOfColumns) {
91- throw new IllegalArgumentException (" Expected the same number of columns " +
92- " in input batch and cached batch." )
93- }
94- cached.zipWithIndex.map { case (cachedCol, idx) =>
95- CudfColumnVector .concatenate(cachedCol, freshBatchTable.getColumn(idx))
96- }
97- }
73+ protected final def hasCache : Boolean = cached.isDefined // for unit test
9874
75+ protected def getNextInputBatchWithRetry : SpillableColumnarBatch = {
9976 // Either cached has unprocessed rows, or input.hasNext().
10077 if (input.hasNext) {
101- if (cached.isDefined) {
78+ val freshSCB = closeOnExcept(input.next()) { freshCB =>
79+ if (inputTypes.isEmpty) {
80+ // initializes input data-types if necessary.
81+ inputTypes = Some (GpuColumnVector .extractTypes(freshCB))
82+ }
83+ SpillableColumnarBatch (freshCB, SpillPriorities .ACTIVE_BATCHING_PRIORITY )
84+ }
85+ if (hasCache) {
10286 // Cached input AND new input rows exist. Return concat-ed rows.
103- withResource(getFreshInputBatch) { freshBatchCB =>
104- withResource(GpuColumnVector .from(freshBatchCB)) { freshBatchTable =>
105- withResource(concatenateColumns(cached.get, freshBatchTable)) { concat =>
106- clearCached()
107- SpillableColumnarBatch (convertToBatch(inputTypes.get, concat),
108- SpillPriorities .ACTIVE_BATCHING_PRIORITY )
109- }
87+ // The two input batches will be closed by `withRetryNoSplit`.
88+ val concatedTbl = withRetryNoSplit(Seq (cached.get, freshSCB)) { toConcat =>
89+ val cbs = toConcat.safeMap(_.getColumnarBatch())
90+ val tbls = withResource(cbs)(_ => cbs.safeMap(GpuColumnVector .from))
91+ withResource(tbls)(_ => Table .concatenate(tbls : _* ))
92+ }
93+ withResource(concatedTbl) { _ =>
94+ cached = None
95+ closeOnExcept(GpuColumnVector .from(concatedTbl, inputTypes.get)) { concatedCB =>
96+ SpillableColumnarBatch (concatedCB, SpillPriorities .ACTIVE_BATCHING_PRIORITY )
11097 }
11198 }
112- } else {
113- // No cached input available. Return fresh input rows, only.
114- SpillableColumnarBatch (getFreshInputBatch,
115- SpillPriorities .ACTIVE_BATCHING_PRIORITY )
99+ } else { // no cache, return the input batch directly
100+ freshSCB
116101 }
117- }
118- else {
119- // No fresh input available. Return cached input.
120- val cachedCB = convertToBatch(inputTypes.get, cached.get)
121- clearCached()
122- SpillableColumnarBatch (cachedCB,
123- SpillPriorities .ACTIVE_BATCHING_PRIORITY )
102+ } else { // No fresh input available. Return cached input.
103+ val cachedSCB = cached.get
104+ cached = None
105+ cachedSCB
124106 }
125107 }
126108
127109 /**
128110 * Helper to trim specified number of rows off the top and bottom,
129111 * of all specified columns.
130112 */
131- private def trim (columns : Array [CudfColumnVector ],
132- offTheTop : Int ,
133- offTheBottom : Int ): Array [CudfColumnVector ] = {
113+ protected def trimWithRetry (scb : SpillableColumnarBatch , offTheTop : Int ,
114+ offTheBottom : Int ): ColumnarBatch = {
115+ val batchRowsNum = scb.numRows()
116+ if ((offTheTop + offTheBottom) > batchRowsNum) {
117+ throw new IllegalArgumentException (s " Cannot trim batch of size ${batchRowsNum} by " +
118+ s " $offTheTop rows at the top, and $offTheBottom rows at the bottom. " )
119+ }
134120
135- def checkValidSizes (col : CudfColumnVector ): Unit =
136- if ((offTheTop + offTheBottom) > col.getRowCount) {
137- throw new IllegalArgumentException (s " Cannot trim column of size ${col.getRowCount} by " +
138- s " $offTheTop rows at the top, and $offTheBottom rows at the bottom. " )
121+ withRetryNoSplit[ColumnarBatch ] {
122+ withResource(scb.getColumnarBatch()) { cb =>
123+ val types = GpuColumnVector .extractTypes(cb)
124+ val baseCols = GpuColumnVector .extractBases(cb)
125+ withResource(baseCols.safeMap(_.subVector(offTheTop, batchRowsNum - offTheBottom))) {
126+ convertToBatch(types, _)
127+ }
139128 }
129+ }
130+ }
140131
141- columns.map{ col =>
142- checkValidSizes(col)
143- col.subVector(offTheTop, col.getRowCount.toInt - offTheBottom)
132+ /**
133+ * Compute the window aggregations on the input batch with retry support.
134+ * It does not close the input batch.
135+ */
136+ protected def computeWindowWithRetry (
137+ scb : SpillableColumnarBatch ): SpillableColumnarBatch = {
138+ val outputCB = withRetryNoSplit[ColumnarBatch ] {
139+ val outputCols = withResource(scb.getColumnarBatch())(computeBasicWindow)
140+ withResource(outputCols)(convertToBatch(outputTypes, _))
144141 }
142+ SpillableColumnarBatch (outputCB, SpillPriorities .ACTIVE_BATCHING_PRIORITY )
145143 }
146144
147- private def resetInputCache (newCache : Option [Array [ CudfColumnVector ] ],
145+ private def resetInputCache (newCache : Option [ColumnarBatch ],
148146 newPrecedingAdded : Int ): Unit = {
149- cached.foreach(_.foreach(_.close))
150- cached = newCache
147+ clearCached()
148+ cached = newCache.map(
149+ SpillableColumnarBatch (_, SpillPriorities .ACTIVE_BATCHING_PRIORITY )
150+ )
151151 numPrecedingRowsAdded = newPrecedingAdded
152152 }
153153
154154 override def next (): ColumnarBatch = {
155155 var outputBatch : ColumnarBatch = null
156- while (outputBatch == null && hasNext) {
157- withResource(getNextInputBatch) { inputCbSpillable =>
158- withResource(inputCbSpillable.getColumnarBatch()) { inputCB =>
159-
160- val inputRowCount = inputCB.numRows()
161- val noMoreInput = ! input.hasNext
162- numUnprocessedInCache = if (noMoreInput) {
163- // If there are no more input rows expected,
164- // this is the last output batch.
165- // Consider all rows in the batch as processed.
156+ while (outputBatch == null && hasNext) {
157+ withResource(getNextInputBatchWithRetry) { inputSCB =>
158+ val inputRowCount = inputSCB.numRows()
159+ val noMoreInput = ! input.hasNext
160+ numUnprocessedInCache = if (noMoreInput) {
161+ // If there are no more input rows expected,
162+ // this is the last output batch.
163+ // Consider all rows in the batch as processed.
164+ 0
165+ } else {
166+ // More input rows expected. The last `maxFollowing` rows can't be finalized.
167+ // Cannot exceed `inputRowCount`.
168+ if (maxFollowing < 0 ) { // E.g. LAG(3) => [ preceding=-3, following=-3 ]
169+ // -ve following => No need to wait for more following rows.
170+ // All "following" context is already available in the current batch.
166171 0
167172 } else {
168- // More input rows expected. The last `maxFollowing` rows can't be finalized.
169- // Cannot exceed `inputRowCount`.
170- if (maxFollowing < 0 ) { // E.g. LAG(3) => [ preceding=-3, following=-3 ]
171- // -ve following => No need to wait for more following rows.
172- // All "following" context is already available in the current batch.
173- 0
174- } else {
175- maxFollowing min inputRowCount
176- }
173+ maxFollowing min inputRowCount
177174 }
175+ }
178176
179- if (numPrecedingRowsAdded + numUnprocessedInCache >= inputRowCount) {
180- // No point calling windowing kernel: the results will simply be ignored.
181- logWarning(" Not enough rows! Cannot output a batch." )
182- } else {
183- NvtxIdWithMetrics (NvtxRegistry .WINDOW_EXEC , opTime) {
184- withResource(computeBasicWindow(inputCB)) { outputCols =>
185- outputBatch = withResource(
186- trim(outputCols,
187- numPrecedingRowsAdded, numUnprocessedInCache)) { trimmed =>
188- convertToBatch(outputTypes, trimmed)
189- }
190- }
177+ if (numPrecedingRowsAdded + numUnprocessedInCache >= inputRowCount) {
178+ // No point calling windowing kernel: the results will simply be ignored.
179+ logWarning(" Not enough rows! Cannot output a batch." )
180+ } else {
181+ NvtxIdWithMetrics (NvtxRegistry .WINDOW_EXEC , opTime) {
182+ outputBatch = withResource(computeWindowWithRetry(inputSCB)) {
183+ trimWithRetry(_, numPrecedingRowsAdded, numUnprocessedInCache)
191184 }
192185 }
186+ }
193187
194- // Compute new cache using current input.
195- numPrecedingRowsAdded = if (minPreceding > 0 ) { // E.g. LEAD(3) => [prec=3, foll=3]
196- // preceding > 0 => No "preceding" rows need be carried forward.
197- // Only the rows that need to be recomputed.
198- 0
199- } else {
200- Math .abs(minPreceding) min (inputRowCount - numUnprocessedInCache)
201- }
202- val inputCols = Range (0 , inputCB.numCols()).map {
203- inputCB.column(_).asInstanceOf [GpuColumnVector ].getBase
204- }.toArray
188+ // Compute new cache using current input.
189+ numPrecedingRowsAdded = if (minPreceding > 0 ) { // E.g. LEAD(3) => [prec=3, foll=3]
190+ // preceding > 0 => No "preceding" rows need be carried forward.
191+ // Only the rows that need to be recomputed.
192+ 0
193+ } else {
194+ Math .abs(minPreceding) min (inputRowCount - numUnprocessedInCache)
195+ }
205196
206- val newCached = trim(inputCols,
197+ if (! noMoreInput) { // cache is needed only when have more input data
198+ val newCached = trimWithRetry(inputSCB,
207199 inputRowCount - (numPrecedingRowsAdded + numUnprocessedInCache),
208200 0 )
209201 resetInputCache(Some (newCached), numPrecedingRowsAdded)
0 commit comments