Skip to content

Commit 0208a8f

Browse files
committed
Prefetch Iceberg Parquet footers with a suffix read
1 parent 8e360c1 commit 0208a8f

6 files changed

Lines changed: 99 additions & 8 deletions

File tree

iceberg/common/src/main/java/org/apache/iceberg/aws/s3/IcebergS3InputFile.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.nvidia.spark.rapids.IcebergS3RangeCopier;
2121
import com.nvidia.spark.rapids.IcebergS3RangeCopier.IcebergS3Client;
2222
import com.nvidia.spark.rapids.fileio.RapidsInputFiles;
23+
import com.nvidia.spark.rapids.fileio.SizedTailInputFile;
2324
import com.nvidia.spark.rapids.fileio.iceberg.IcebergInputFile;
2425
import com.nvidia.spark.rapids.iceberg.ShimUtils;
2526
import com.nvidia.spark.rapids.jni.fileio.RapidsInputFile;
@@ -43,7 +44,7 @@
4344
*
4445
* <p>The package-private S3 file access is isolated in {@link IcebergS3InputFileAccess}.
4546
*/
46-
public final class IcebergS3InputFile implements RapidsInputFile {
47+
public final class IcebergS3InputFile extends IcebergInputFile implements SizedTailInputFile {
4748
private static final Logger LOG = LoggerFactory.getLogger(IcebergS3InputFile.class);
4849

4950
private final IcebergInputFile delegate;
@@ -52,12 +53,13 @@ public final class IcebergS3InputFile implements RapidsInputFile {
5253

5354
private IcebergS3InputFile(
5455
IcebergInputFile delegate, URI s3Uri, IcebergS3Client icebergS3Client) {
56+
super(delegate.getDelegate());
5557
this.delegate = delegate;
5658
this.s3Uri = s3Uri;
5759
this.icebergS3Client = icebergS3Client;
5860
}
5961

60-
public static RapidsInputFile maybeCreate(InputFile inputFile, FileIO fileIO) {
62+
public static IcebergInputFile maybeCreate(InputFile inputFile, FileIO fileIO) {
6163
// When the gating conf is off (or the file is not an S3 file), return the
6264
// default IcebergInputFile so the standard Iceberg SeekableInputStream path is used.
6365
IcebergInputFile delegate = new IcebergInputFile(inputFile);
@@ -127,12 +129,18 @@ public void readVectored(HostMemoryBuffer output, List<CopyRange> copyRanges)
127129
*/
128130
@Override
129131
public void readTail(long length, HostMemoryBuffer output) throws IOException {
132+
readTailAndGetSize(length, output);
133+
}
134+
135+
@Override
136+
public long readTailAndGetSize(long length, HostMemoryBuffer output) throws IOException {
130137
if (length == 0) {
131-
return;
138+
return 0;
132139
}
133140
if (length < 0) {
134141
throw new IllegalArgumentException("length must be non-negative");
135142
}
136-
IcebergS3RangeCopier.copyTailToHMB(icebergS3Client, output, s3Uri, length, /*dstOffset*/ 0L);
143+
return IcebergS3RangeCopier.copyTailToHMB(
144+
icebergS3Client, output, s3Uri, length, /*dstOffset*/ 0L);
137145
}
138146
}

iceberg/common/src/main/scala/org/apache/iceberg/spark/source/GpuIcebergPartitionReader.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ import scala.collection.JavaConverters._
2020

2121
import com.nvidia.spark.rapids.GpuMetric
2222
import com.nvidia.spark.rapids.MapUtil.toMapStrict
23-
import com.nvidia.spark.rapids.fileio.iceberg.{IcebergFileIO, IcebergInputFile}
23+
import com.nvidia.spark.rapids.fileio.iceberg.IcebergFileIO
2424
import com.nvidia.spark.rapids.iceberg.ShimUtils
2525
import com.nvidia.spark.rapids.iceberg.ShimUtils.locationOf
2626
import com.nvidia.spark.rapids.iceberg.data.GpuDeleteFilter
2727
import com.nvidia.spark.rapids.iceberg.parquet._
2828
import org.apache.iceberg._
29+
import org.apache.iceberg.aws.s3.IcebergS3InputFile
2930
import org.apache.iceberg.encryption.EncryptedFiles
3031
import org.apache.iceberg.mapping.NameMappingParser
3132

@@ -110,7 +111,7 @@ class GpuIcebergPartitionReader(private val task: GpuSparkInputPartition,
110111
val inputFiles = table.encryption()
111112
.decrypt(encryptedFiles.asJava)
112113
.asScala
113-
.map(f => f.location() -> new IcebergInputFile(f))
114+
.map(f => f.location() -> IcebergS3InputFile.maybeCreate(f, fileIO))
114115
.toMap
115116

116117
val taskMap = toMapStrict(tasks.map(t => {

iceberg/iceberg-1-10-x/src/main/scala/com/nvidia/spark/rapids/iceberg/iceberg110x/GpuParquetIOShim.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ object GpuParquetIOShim {
4141
metrics: Map[String, GpuMetric]): ParquetFileReader = {
4242
val metadata = withResource(ParquetFooterUtils.getFooterBuffer(
4343
inputFile, metrics,
44-
ParquetFooterUtils.readFooterBufferFromInputFile(inputFile, filePath))) { hmb =>
44+
ParquetFooterUtils.readFooterBufferFromInputFileWithSuffixPrefetch(inputFile, filePath))) {
45+
hmb =>
4546
val shadedHmbFile = ToIcebergShaded.shade(new HMBInputFile(hmb))
4647
withResource(shadedHmbFile.newStream()) { hmbStream =>
4748
ParquetFileReader.readFooter(shadedHmbFile, options, hmbStream)

iceberg/iceberg-1-11-x/src/main/scala/com/nvidia/spark/rapids/iceberg/iceberg111x/GpuParquetIOShim.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ object GpuParquetIOShim {
4141
metrics: Map[String, GpuMetric]): ParquetFileReader = {
4242
val metadata = withResource(ParquetFooterUtils.getFooterBuffer(
4343
inputFile, metrics,
44-
ParquetFooterUtils.readFooterBufferFromInputFile(inputFile, filePath))) { hmb =>
44+
ParquetFooterUtils.readFooterBufferFromInputFileWithSuffixPrefetch(inputFile, filePath))) {
45+
hmb =>
4546
val shadedHmbFile = ToIcebergShaded.shade(new HMBInputFile(hmb))
4647
withResource(shadedHmbFile.newStream()) { hmbStream =>
4748
ParquetFileReader.readFooter(shadedHmbFile, options, hmbStream)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright (c) 2026, NVIDIA CORPORATION.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.nvidia.spark.rapids.fileio;
18+
19+
import ai.rapids.cudf.HostMemoryBuffer;
20+
21+
import java.io.IOException;
22+
23+
/**
24+
* Optional capability for input files whose tail read can return fewer bytes than requested.
25+
* The bytes are written starting at offset zero in {@code output}.
26+
*/
27+
public interface SizedTailInputFile {
28+
long readTailAndGetSize(long length, HostMemoryBuffer output) throws IOException;
29+
}

sql-plugin/src/main/scala/com/nvidia/spark/rapids/parquet/ParquetFooterUtils.scala

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import ai.rapids.cudf.HostMemoryBuffer
2323
import com.nvidia.spark.rapids.{GpuMetric, NoopMetric, NvtxRegistry, RapidsConf}
2424
import com.nvidia.spark.rapids.Arm.{closeOnExcept, withResource}
2525
import com.nvidia.spark.rapids.filecache.FileCache
26+
import com.nvidia.spark.rapids.fileio.SizedTailInputFile
2627
import com.nvidia.spark.rapids.jni.fileio.RapidsInputFile
2728
import org.apache.hadoop.fs.Path
2829
import org.apache.hadoop.io.IOUtils
@@ -38,6 +39,9 @@ import org.apache.parquet.hadoop.ParquetFileWriter.MAGIC
3839
*/
3940
object ParquetFooterUtils {
4041
val FooterLengthSize: Int = java.lang.Integer.BYTES
42+
private val IcebergSuffixPrefetchSizeKey =
43+
"spark.rapids.iceberg.parquet.footer.suffixPrefetchSize"
44+
private val DefaultIcebergSuffixPrefetchSize = 0L
4145
private val ParquetMagicEncrypted = "PARE".getBytes(StandardCharsets.US_ASCII)
4246

4347
def verifyParquetMagic(filePath: Path, magic: Array[Byte]): Unit = {
@@ -113,6 +117,53 @@ object ParquetFooterUtils {
113117
}
114118
}
115119

120+
/**
121+
* Read one configurable suffix and parse the footer from it. For Iceberg S3 PerfIO this
122+
* replaces the footer-length request and following footer request with a single suffix GET.
123+
* Unsupported inputs and footers larger than the prefetched suffix use the existing path.
124+
*/
125+
def readFooterBufferFromInputFileWithSuffixPrefetch(
126+
inputFile: RapidsInputFile,
127+
filePath: Path): HostMemoryBuffer = {
128+
val configuredSize = Option(org.apache.spark.SparkEnv.get)
129+
.map(_.conf.getSizeAsBytes(
130+
IcebergSuffixPrefetchSizeKey, DefaultIcebergSuffixPrefetchSize.toString))
131+
.getOrElse(DefaultIcebergSuffixPrefetchSize)
132+
if (configuredSize <= 0 || !inputFile.isInstanceOf[SizedTailInputFile]) {
133+
return readFooterBufferFromInputFile(inputFile, filePath)
134+
}
135+
136+
val prefetchSize = Math.toIntExact(
137+
math.max(configuredSize, FooterLengthSize + MAGIC.length))
138+
try {
139+
withResource(HostMemoryBuffer.allocate(prefetchSize, false)) { suffix =>
140+
val actualSize = Math.toIntExact(
141+
inputFile.asInstanceOf[SizedTailInputFile].readTailAndGetSize(prefetchSize, suffix))
142+
if (actualSize < FooterLengthSize + MAGIC.length) {
143+
return readFooterBufferFromInputFile(inputFile, filePath)
144+
}
145+
146+
val footerLengthOffset = actualSize - FooterLengthSize - MAGIC.length
147+
val magic = readBytesFromBuffer(suffix, actualSize - MAGIC.length, MAGIC.length)
148+
verifyParquetMagic(filePath, magic)
149+
val footerLengthBytes = readBytesFromBuffer(suffix, footerLengthOffset, FooterLengthSize)
150+
val footerLength = readIntLittleEndian(footerLengthBytes, 0)
151+
val tailLength = footerLength + FooterLengthSize + MAGIC.length
152+
if (footerLength <= 0 || tailLength > actualSize) {
153+
return readFooterBufferFromInputFile(inputFile, filePath)
154+
}
155+
156+
closeOnExcept(HostMemoryBuffer.allocate(tailLength + MAGIC.length, false)) { out =>
157+
out.setBytes(0, MAGIC, 0, MAGIC.length)
158+
out.copyFromHostBuffer(MAGIC.length, suffix, actualSize - tailLength, tailLength)
159+
out
160+
}
161+
}
162+
} catch {
163+
case _: java.io.IOException => readFooterBufferFromInputFile(inputFile, filePath)
164+
}
165+
}
166+
116167
/**
117168
* Return a framed footer buffer (`MAGIC + footer + footerLen + MAGIC`) for `inputFile`,
118169
* serving from `FileCache` when present and populating the cache on miss. The `readFooterBuffer`

0 commit comments

Comments
 (0)