[Offloading] Offload Binary Format V2: Support Multiple Entries (#169425)

This PR updates the OffloadBinary format from version 1 to version 2,
enabling support for multiple offloading entries in a single binary.
This allows combining multiple device images into a single binary with
common global metadata while maintaining backwards compatibility with
version 1 binaries.

# Key Changes
## Binary Format Enhancements
  **Version 2 Format Changes:**
  - Changed from single-entry to multi-entry design
  - Updated `Header` structure:
    - Renamed `EntryOffset` → `EntriesOffset` (offset to entries array)
    - Renamed `EntrySize` → `EntriesCount` (number of entries)
- Added `StringEntry::ValueSize` field to support explicit string value
sizes (enables non-null-terminated strings)
- Introduced `OffloadEntryFlags` enum with `OIF_Metadata` flag for
metadata-only entries (entries without binary images)

  **API Changes:**
- `OffloadBinary::create()` now returns
`Expected<SmallVector<std::unique_ptr<OffloadBinary>>>` instead of
single binary
- Added optional `Index` parameter to extract specific entry:
`create(Buffer, std::optional<uint64_t> Index)`
- `OffloadBinary::write()` now accepts `ArrayRef<OffloadingImage>`
instead of single image
  - Added `OffloadBinary::extractHeader()` for header extraction

  **Memory Management:**
- Implemented `SharedMemoryBuffer` class to enable memory sharing across
multiple `OffloadBinary` instances from the same file
- Multiple entries from a single serialized binary share the underlying
buffer

## Testing
  **Unit Tests (`unittests/Object/OffloadingTest.cpp`):**
- `checkMultiEntryBinaryExtraction`: Tests extracting all entries from a
multi-entry binary
- `checkIndexBasedExtraction`: Tests extracting specific entries by
index, including out-of-bounds validation
  - `checkEdgeCases`: Tests edge cases including:
    - Empty string metadata
    - Empty image data
    - Large string values (4KB)

  **Other Tests:**
- Updated `test/ObjectYAML/Offload/multiple_members.yaml` to include
metadata-only entry

---------

Co-authored-by: Joseph Huber <huberjn@outlook.com>
This commit is contained in:
Yury Plyakhin 2026-02-05 07:46:57 -08:00 committed by GitHub
parent 10910771e8
commit 4d27530c69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 527 additions and 158 deletions

View File

@ -25,7 +25,7 @@
// OPENMP-REL: @.omp_offloading.device_image = internal unnamed_addr constant [[[SIZE:[0-9]+]] x i8] c"\10\FF\10\AD{{.*}}", section ".llvm.offloading.relocatable", align 8
// OPENMP: @.omp_offloading.device_image = internal unnamed_addr constant [[[SIZE:[0-9]+]] x i8] c"\10\FF\10\AD{{.*}}", section ".llvm.offloading", align 8
// OPENMP-NEXT: @.omp_offloading.device_images = internal unnamed_addr constant [1 x %__tgt_device_image] [%__tgt_device_image { ptr getelementptr ([[[BEGIN:[0-9]+]] x i8], ptr @.omp_offloading.device_image, i64 0, i64 144), ptr getelementptr ([[[END:[0-9]+]] x i8], ptr @.omp_offloading.device_image, i64 0, i64 144), ptr @__start_llvm_offload_entries, ptr @__stop_llvm_offload_entries }]
// OPENMP-NEXT: @.omp_offloading.device_images = internal unnamed_addr constant [1 x %__tgt_device_image] [%__tgt_device_image { ptr getelementptr ([[[IMG_OFF:[0-9]+]] x i8], ptr @.omp_offloading.device_image, i64 0, i64 [[IMG_OFF]]), ptr getelementptr ([[[IMG_OFF]] x i8], ptr @.omp_offloading.device_image, i64 0, i64 [[IMG_OFF]]), ptr @__start_llvm_offload_entries, ptr @__stop_llvm_offload_entries }]
// OPENMP-NEXT: @.omp_offloading.descriptor = internal constant %__tgt_bin_desc { i32 1, ptr @.omp_offloading.device_images, ptr @__start_llvm_offload_entries, ptr @__stop_llvm_offload_entries }
// OPENMP-NEXT: @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 101, ptr @.omp_offloading.descriptor_reg, ptr null }]

View File

@ -6,7 +6,7 @@
//
//===----------------------------------------------------------------------===//
//
// This file contains the binary format used for budingling device metadata with
// This file contains the binary format used for bundling device metadata with
// an associated device image. The data can then be stored inside a host object
// file to create a fat binary and read by the linker. This is intended to be a
// thin wrapper around the image itself. If this format becomes sufficiently
@ -52,6 +52,13 @@ enum ImageKind : uint16_t {
IMG_LAST,
};
/// Flags associated with the Entry.
enum OffloadEntryFlags : uint32_t {
OIF_None = 0,
// Entry doesn't contain an image. Used to keep metadata only entries.
OIF_Metadata = (1 << 0),
};
/// A simple binary serialization of an offloading file. We use this format to
/// embed the offloading image into the host executable so it can be extracted
/// and used by the linker.
@ -67,7 +74,7 @@ public:
using string_iterator_range = iterator_range<string_iterator>;
/// The current version of the binary used for backwards compatibility.
static const uint32_t Version = 1;
static const uint32_t Version = 2;
/// The offloading metadata that will be serialized to a memory buffer.
struct OffloadingImage {
@ -78,12 +85,57 @@ public:
std::unique_ptr<MemoryBuffer> Image;
};
/// Attempt to parse the offloading binary stored in \p Data.
LLVM_ABI static Expected<std::unique_ptr<OffloadBinary>>
create(MemoryBufferRef);
struct Header {
uint8_t Magic[4] = {0x10, 0xFF, 0x10, 0xAD}; // 0x10FF10AD magic bytes.
uint32_t Version = OffloadBinary::Version; // Version identifier.
uint64_t Size; // Size in bytes of this entire binary.
uint64_t EntriesOffset; // Offset in bytes to the start of entries block.
uint64_t EntriesCount; // Number of metadata entries in the binary.
};
/// Serialize the contents of \p File to a binary buffer to be read later.
LLVM_ABI static SmallString<0> write(const OffloadingImage &);
struct Entry {
ImageKind TheImageKind; // The kind of the image stored.
OffloadKind TheOffloadKind; // The producer of this image.
uint32_t Flags; // Additional flags associated with the entry.
uint64_t StringOffset; // Offset in bytes to the string map.
uint64_t NumStrings; // Number of entries in the string map.
uint64_t ImageOffset; // Offset in bytes of the actual binary image.
uint64_t ImageSize; // Size in bytes of the binary image.
};
struct StringEntry {
uint64_t KeyOffset;
uint64_t ValueOffset;
uint64_t ValueSize; // Size of the value in bytes.
};
struct StringEntryV1 {
uint64_t KeyOffset;
uint64_t ValueOffset;
};
/// Attempt to extract and validate the header from the offloading binary in
/// \p Buf.
LLVM_ABI
static Expected<const Header *> extractHeader(MemoryBufferRef Buf);
/// Attempt to parse the offloading binary stored in \p Buf.
/// For version 1 binaries, always returns a single OffloadBinary.
/// For version 2+ binaries:
/// - If \p Index is provided, returns the OffloadBinary at that index.
/// - If \p Index is std::nullopt, returns all OffloadBinary entries.
/// \param Buf The memory buffer containing the offload binary.
/// \param Index Optional index to select a specific entry. If not provided,
/// all entries are returned (version 2+ only).
/// \returns An array of unique pointers to OffloadBinary objects, or an
/// error.
LLVM_ABI static Expected<SmallVector<std::unique_ptr<OffloadBinary>>>
create(MemoryBufferRef Buf, std::optional<uint64_t> Index = std::nullopt);
/// Serialize the contents of \p OffloadingData to a binary buffer to be read
/// later.
LLVM_ABI static SmallString<0>
write(ArrayRef<OffloadingImage> OffloadingData);
static uint64_t getAlignment() { return 8; }
@ -92,6 +144,7 @@ public:
uint32_t getVersion() const { return TheHeader->Version; }
uint32_t getFlags() const { return TheEntry->Flags; }
uint64_t getSize() const { return TheHeader->Size; }
uint64_t getIndex() const { return Index; }
StringRef getTriple() const { return getString("triple"); }
StringRef getArch() const { return getString("arch"); }
@ -106,39 +159,29 @@ public:
static bool classof(const Binary *V) { return V->isOffloadFile(); }
struct Header {
uint8_t Magic[4] = {0x10, 0xFF, 0x10, 0xAD}; // 0x10FF10AD magic bytes.
uint32_t Version = OffloadBinary::Version; // Version identifier.
uint64_t Size; // Size in bytes of this entire binary.
uint64_t EntryOffset; // Offset of the metadata entry in bytes.
uint64_t EntrySize; // Size of the metadata entry in bytes.
};
struct Entry {
ImageKind TheImageKind; // The kind of the image stored.
OffloadKind TheOffloadKind; // The producer of this image.
uint32_t Flags; // Additional flags associated with the image.
uint64_t StringOffset; // Offset in bytes to the string map.
uint64_t NumStrings; // Number of entries in the string map.
uint64_t ImageOffset; // Offset in bytes of the actual binary image.
uint64_t ImageSize; // Size in bytes of the binary image.
};
struct StringEntry {
uint64_t KeyOffset;
uint64_t ValueOffset;
};
private:
OffloadBinary(MemoryBufferRef Source, const Header *TheHeader,
const Entry *TheEntry)
const Entry *TheEntry, const uint64_t Index = 0)
: Binary(Binary::ID_Offload, Source), Buffer(Source.getBufferStart()),
TheHeader(TheHeader), TheEntry(TheEntry) {
const StringEntry *StringMapBegin =
reinterpret_cast<const StringEntry *>(&Buffer[TheEntry->StringOffset]);
TheHeader(TheHeader), TheEntry(TheEntry), Index(Index) {
// StringEntryV1 and StringEntry have ABI compatible Key/ValueOffset fields,
// but different sizes, so we need to manually calculate offset.
const char *StringMapBegin = &Buffer[TheEntry->StringOffset];
const size_t StringEntrySize =
TheHeader->Version == 1 ? sizeof(StringEntryV1) : sizeof(StringEntry);
for (uint64_t I = 0, E = TheEntry->NumStrings; I != E; ++I) {
StringRef Key = &Buffer[StringMapBegin[I].KeyOffset];
StringData[Key] = &Buffer[StringMapBegin[I].ValueOffset];
const char *StringEntryPtr = StringMapBegin + I * StringEntrySize;
const StringEntryV1 *EntryV1 =
reinterpret_cast<const StringEntryV1 *>(StringEntryPtr);
StringRef Key = &Buffer[EntryV1->KeyOffset];
if (TheHeader->Version == 1) {
StringData[Key] = &Buffer[EntryV1->ValueOffset];
} else {
const StringEntry *Entry =
reinterpret_cast<const StringEntry *>(StringEntryPtr);
StringData[Key] =
StringRef(&Buffer[Entry->ValueOffset], Entry->ValueSize);
}
}
}
@ -152,10 +195,13 @@ private:
const Header *TheHeader;
/// Location of the metadata entries within the binary.
const Entry *TheEntry;
/// Index of the entry in the list of entries serialized in the Buffer.
const uint64_t Index;
};
/// A class to contain the binary information for a single OffloadBinary that
/// owns its memory.
/// A class to contain the binary information for a single OffloadBinary.
/// Memory is shared between multiple OffloadBinary instances read from
/// the single serialized offload binary.
class OffloadFile : public OwningBinary<OffloadBinary> {
public:
using TargetID = std::pair<StringRef, StringRef>;
@ -171,11 +217,12 @@ public:
getBinary()->getMemoryBufferRef().getBufferIdentifier());
// This parsing should never fail because it has already been parsed.
auto NewBinaryOrErr = OffloadBinary::create(*Buffer);
auto NewBinaryOrErr =
OffloadBinary::create(*Buffer, getBinary()->getIndex());
assert(NewBinaryOrErr && "Failed to parse a copy of the binary?");
if (!NewBinaryOrErr)
llvm::consumeError(NewBinaryOrErr.takeError());
return OffloadFile(std::move(*NewBinaryOrErr), std::move(Buffer));
return OffloadFile(std::move((*NewBinaryOrErr)[0]), std::move(Buffer));
}
/// We use the Triple and Architecture pair to group linker inputs together.

View File

@ -39,8 +39,8 @@ struct Binary {
std::optional<uint32_t> Version;
std::optional<uint64_t> Size;
std::optional<uint64_t> EntryOffset;
std::optional<uint64_t> EntrySize;
std::optional<uint64_t> EntriesOffset;
std::optional<uint64_t> EntriesCount;
std::vector<Member> Members;
};

View File

@ -161,7 +161,7 @@ GlobalVariable *createBinDesc(Module &M, ArrayRef<ArrayRef<char>> Bufs,
Binary.bytes_begin());
const auto *Entry =
reinterpret_cast<const object::OffloadBinary::Entry *>(
Binary.bytes_begin() + Header->EntryOffset);
Binary.bytes_begin() + Header->EntriesOffset);
BeginOffset = Entry->ImageOffset;
EndOffset = Entry->ImageOffset + Entry->ImageSize;
}

View File

@ -93,8 +93,12 @@ Expected<std::unique_ptr<Binary>> object::createBinary(MemoryBufferRef Buffer,
case file_magic::spirv_object:
// Unrecognized object file format.
return errorCodeToError(object_error::invalid_file_type);
case file_magic::offload_binary:
return OffloadBinary::create(Buffer);
case file_magic::offload_binary: {
auto OffloadBinaryOrErr = OffloadBinary::create(Buffer);
if (!OffloadBinaryOrErr)
return OffloadBinaryOrErr.takeError();
return std::move((*OffloadBinaryOrErr)[0]);
}
case file_magic::minidump:
return MinidumpFile::create(Buffer);
case file_magic::tapi_file:

View File

@ -28,6 +28,26 @@ using namespace llvm::object;
namespace {
/// A MemoryBuffer that shares ownership of the underlying memory.
/// This allows multiple OffloadBinary instances to share the same buffer.
class SharedMemoryBuffer : public MemoryBuffer {
public:
SharedMemoryBuffer(std::shared_ptr<MemoryBuffer> Buf)
: SharedBuf(std::move(Buf)) {
init(SharedBuf->getBufferStart(), SharedBuf->getBufferEnd(),
/*RequiresNullTerminator=*/false);
}
BufferKind getBufferKind() const override { return MemoryBuffer_Malloc; }
StringRef getBufferIdentifier() const override {
return SharedBuf->getBufferIdentifier();
}
private:
const std::shared_ptr<MemoryBuffer> SharedBuf;
};
/// Attempts to extract all the embedded device images contained inside the
/// buffer \p Contents. The buffer is expected to contain a valid offloading
/// binary format.
@ -35,7 +55,7 @@ Error extractOffloadFiles(MemoryBufferRef Contents,
SmallVectorImpl<OffloadFile> &Binaries) {
uint64_t Offset = 0;
// There could be multiple offloading binaries stored at this section.
while (Offset < Contents.getBuffer().size()) {
while (Offset < Contents.getBufferSize()) {
std::unique_ptr<MemoryBuffer> Buffer =
MemoryBuffer::getMemBuffer(Contents.getBuffer().drop_front(Offset), "",
/*RequiresNullTerminator*/ false);
@ -43,21 +63,32 @@ Error extractOffloadFiles(MemoryBufferRef Contents,
Buffer->getBufferStart()))
Buffer = MemoryBuffer::getMemBufferCopy(Buffer->getBuffer(),
Buffer->getBufferIdentifier());
auto BinaryOrErr = OffloadBinary::create(*Buffer);
if (!BinaryOrErr)
return BinaryOrErr.takeError();
OffloadBinary &Binary = **BinaryOrErr;
// Create a new owned binary with a copy of the original memory.
auto HeaderOrErr = OffloadBinary::extractHeader(*Buffer);
if (!HeaderOrErr)
return HeaderOrErr.takeError();
const OffloadBinary::Header *Header = *HeaderOrErr;
// Create a copy of original memory containing only the current binary.
std::unique_ptr<MemoryBuffer> BufferCopy = MemoryBuffer::getMemBufferCopy(
Binary.getData().take_front(Binary.getSize()),
Buffer->getBuffer().take_front(Header->Size),
Contents.getBufferIdentifier());
auto NewBinaryOrErr = OffloadBinary::create(*BufferCopy);
if (!NewBinaryOrErr)
return NewBinaryOrErr.takeError();
Binaries.emplace_back(std::move(*NewBinaryOrErr), std::move(BufferCopy));
Offset += Binary.getSize();
auto BinariesOrErr = OffloadBinary::create(*BufferCopy);
if (!BinariesOrErr)
return BinariesOrErr.takeError();
// Share ownership among multiple OffloadFiles.
std::shared_ptr<MemoryBuffer> SharedBuffer =
std::shared_ptr<MemoryBuffer>(std::move(BufferCopy));
for (auto &Binary : *BinariesOrErr) {
std::unique_ptr<SharedMemoryBuffer> SharedBufferPtr =
std::make_unique<SharedMemoryBuffer>(SharedBuffer);
Binaries.emplace_back(std::move(Binary), std::move(SharedBufferPtr));
}
Offset += Header->Size;
}
return Error::success();
@ -167,8 +198,8 @@ Error extractFromArchive(const Archive &Library,
} // namespace
Expected<std::unique_ptr<OffloadBinary>>
OffloadBinary::create(MemoryBufferRef Buf) {
Expected<const OffloadBinary::Header *>
OffloadBinary::extractHeader(MemoryBufferRef Buf) {
if (Buf.getBufferSize() < sizeof(Header) + sizeof(Entry))
return errorCodeToError(object_error::parse_failed);
@ -182,83 +213,146 @@ OffloadBinary::create(MemoryBufferRef Buf) {
const char *Start = Buf.getBufferStart();
const Header *TheHeader = reinterpret_cast<const Header *>(Start);
if (TheHeader->Version != OffloadBinary::Version)
if (TheHeader->Version == 0 || TheHeader->Version > OffloadBinary::Version)
return errorCodeToError(object_error::parse_failed);
if (TheHeader->Size > Buf.getBufferSize() ||
TheHeader->Size < sizeof(Entry) || TheHeader->Size < sizeof(Header))
return errorCodeToError(object_error::unexpected_eof);
if (TheHeader->EntryOffset > TheHeader->Size - sizeof(Entry) ||
TheHeader->EntrySize > TheHeader->Size - sizeof(Header))
uint64_t EntriesCount =
(TheHeader->Version == 1) ? 1 : TheHeader->EntriesCount;
uint64_t EntriesSize = sizeof(Entry) * EntriesCount;
if (TheHeader->EntriesOffset > TheHeader->Size - EntriesSize ||
EntriesSize > TheHeader->Size - sizeof(Header))
return errorCodeToError(object_error::unexpected_eof);
const Entry *TheEntry =
reinterpret_cast<const Entry *>(&Start[TheHeader->EntryOffset]);
if (TheEntry->ImageOffset > Buf.getBufferSize() ||
TheEntry->StringOffset > Buf.getBufferSize() ||
TheEntry->StringOffset + TheEntry->NumStrings * sizeof(StringEntry) >
Buf.getBufferSize())
return errorCodeToError(object_error::unexpected_eof);
return std::unique_ptr<OffloadBinary>(
new OffloadBinary(Buf, TheHeader, TheEntry));
return TheHeader;
}
SmallString<0> OffloadBinary::write(const OffloadingImage &OffloadingData) {
Expected<SmallVector<std::unique_ptr<OffloadBinary>>>
OffloadBinary::create(MemoryBufferRef Buf, std::optional<uint64_t> Index) {
auto HeaderOrErr = OffloadBinary::extractHeader(Buf);
if (!HeaderOrErr)
return HeaderOrErr.takeError();
const Header *TheHeader = *HeaderOrErr;
const char *Start = Buf.getBufferStart();
const Entry *Entries =
reinterpret_cast<const Entry *>(&Start[TheHeader->EntriesOffset]);
auto validateEntry = [&](const Entry *TheEntry) -> Error {
if (TheEntry->ImageOffset > Buf.getBufferSize() ||
TheEntry->StringOffset > Buf.getBufferSize() ||
TheEntry->StringOffset + TheEntry->NumStrings * sizeof(StringEntry) >
Buf.getBufferSize())
return errorCodeToError(object_error::unexpected_eof);
return Error::success();
};
SmallVector<std::unique_ptr<OffloadBinary>> Binaries;
if (TheHeader->Version > 1 && Index.has_value()) {
if (*Index >= TheHeader->EntriesCount)
return errorCodeToError(object_error::parse_failed);
const Entry *TheEntry = &Entries[*Index];
if (auto Err = validateEntry(TheEntry))
return std::move(Err);
Binaries.emplace_back(new OffloadBinary(Buf, TheHeader, TheEntry, *Index));
return Binaries;
}
uint64_t EntriesCount = TheHeader->Version == 1 ? 1 : TheHeader->EntriesCount;
for (uint64_t I = 0; I < EntriesCount; ++I) {
const Entry *TheEntry = &Entries[I];
if (auto Err = validateEntry(TheEntry))
return std::move(Err);
Binaries.emplace_back(new OffloadBinary(Buf, TheHeader, TheEntry, I));
}
return Binaries;
}
SmallString<0> OffloadBinary::write(ArrayRef<OffloadingImage> OffloadingData) {
uint64_t EntriesCount = OffloadingData.size();
assert(EntriesCount > 0 && "At least one offloading image is required");
// Create a null-terminated string table with all the used strings.
// Also calculate total size of images.
StringTableBuilder StrTab(StringTableBuilder::ELF);
for (auto &KeyAndValue : OffloadingData.StringData) {
StrTab.add(KeyAndValue.first);
StrTab.add(KeyAndValue.second);
uint64_t TotalStringEntries = 0;
uint64_t TotalImagesSize = 0;
for (const OffloadingImage &Img : OffloadingData) {
for (auto &KeyAndValue : Img.StringData) {
StrTab.add(KeyAndValue.first);
StrTab.add(KeyAndValue.second);
}
TotalStringEntries += Img.StringData.size();
TotalImagesSize += Img.Image->getBufferSize();
}
StrTab.finalize();
uint64_t StringEntrySize =
sizeof(StringEntry) * OffloadingData.StringData.size();
uint64_t StringEntrySize = sizeof(StringEntry) * TotalStringEntries;
uint64_t EntriesSize = sizeof(Entry) * EntriesCount;
uint64_t StrTabOffset = sizeof(Header) + EntriesSize + StringEntrySize;
// Make sure the image we're wrapping around is aligned as well.
uint64_t BinaryDataSize = alignTo(sizeof(Header) + sizeof(Entry) +
StringEntrySize + StrTab.getSize(),
getAlignment());
uint64_t BinaryDataSize =
alignTo(StrTabOffset + StrTab.getSize(), getAlignment());
// Create the header and fill in the offsets. The entry will be directly
// Create the header and fill in the offsets. The entries will be directly
// placed after the header in memory. Align the size to the alignment of the
// header so this can be placed contiguously in a single section.
Header TheHeader;
TheHeader.Size = alignTo(
BinaryDataSize + OffloadingData.Image->getBufferSize(), getAlignment());
TheHeader.EntryOffset = sizeof(Header);
TheHeader.EntrySize = sizeof(Entry);
// Create the entry using the string table offsets. The string table will be
// placed directly after the entry in memory, and the image after that.
Entry TheEntry;
TheEntry.TheImageKind = OffloadingData.TheImageKind;
TheEntry.TheOffloadKind = OffloadingData.TheOffloadKind;
TheEntry.Flags = OffloadingData.Flags;
TheEntry.StringOffset = sizeof(Header) + sizeof(Entry);
TheEntry.NumStrings = OffloadingData.StringData.size();
TheEntry.ImageOffset = BinaryDataSize;
TheEntry.ImageSize = OffloadingData.Image->getBufferSize();
TheHeader.Size = alignTo(BinaryDataSize + TotalImagesSize, getAlignment());
TheHeader.EntriesOffset = sizeof(Header);
TheHeader.EntriesCount = EntriesCount;
SmallString<0> Data;
Data.reserve(TheHeader.Size);
raw_svector_ostream OS(Data);
OS << StringRef(reinterpret_cast<char *>(&TheHeader), sizeof(Header));
OS << StringRef(reinterpret_cast<char *>(&TheEntry), sizeof(Entry));
for (auto &KeyAndValue : OffloadingData.StringData) {
uint64_t Offset = sizeof(Header) + sizeof(Entry) + StringEntrySize;
StringEntry Map{Offset + StrTab.getOffset(KeyAndValue.first),
Offset + StrTab.getOffset(KeyAndValue.second)};
OS << StringRef(reinterpret_cast<char *>(&Map), sizeof(StringEntry));
// Create the entries using the string table offsets. The string table will be
// placed directly after the set of entries in memory, and all the images are
// after that.
uint64_t StringEntryOffset = sizeof(Header) + EntriesSize;
uint64_t ImageOffset = BinaryDataSize;
for (const OffloadingImage &Img : OffloadingData) {
Entry TheEntry;
TheEntry.TheImageKind = Img.TheImageKind;
TheEntry.TheOffloadKind = Img.TheOffloadKind;
TheEntry.Flags = Img.Flags;
TheEntry.StringOffset = StringEntryOffset;
StringEntryOffset += sizeof(StringEntry) * Img.StringData.size();
TheEntry.NumStrings = Img.StringData.size();
TheEntry.ImageOffset = ImageOffset;
ImageOffset += Img.Image->getBufferSize();
TheEntry.ImageSize = Img.Image->getBufferSize();
OS << StringRef(reinterpret_cast<char *>(&TheEntry), sizeof(Entry));
}
// Create the string map entries.
for (const OffloadingImage &Img : OffloadingData) {
for (auto &KeyAndValue : Img.StringData) {
StringEntry Map{StrTabOffset + StrTab.getOffset(KeyAndValue.first),
StrTabOffset + StrTab.getOffset(KeyAndValue.second),
KeyAndValue.second.size()};
OS << StringRef(reinterpret_cast<char *>(&Map), sizeof(StringEntry));
}
}
StrTab.write(OS);
// Add padding to required image alignment.
OS.write_zeros(TheEntry.ImageOffset - OS.tell());
OS << OffloadingData.Image->getBuffer();
OS.write_zeros(BinaryDataSize - OS.tell());
for (const OffloadingImage &Img : OffloadingData)
OS << Img.Image->getBuffer();
// Add final padding to required alignment.
assert(TheHeader.Size >= OS.tell() && "Too much data written?");

View File

@ -18,6 +18,7 @@ namespace llvm {
namespace yaml {
bool yaml2offload(Binary &Doc, raw_ostream &Out, ErrorHandler EH) {
SmallVector<object::OffloadBinary::OffloadingImage> Images;
for (const auto &Member : Doc.Members) {
object::OffloadBinary::OffloadingImage Image{};
if (Member.ImageKind)
@ -36,23 +37,24 @@ bool yaml2offload(Binary &Doc, raw_ostream &Out, ErrorHandler EH) {
if (Member.Content)
Member.Content->writeAsBinary(OS);
Image.Image = MemoryBuffer::getMemBufferCopy(OS.str());
// Copy the data to a new buffer so we can modify the bytes directly.
auto Buffer = object::OffloadBinary::write(Image);
auto *TheHeader =
reinterpret_cast<object::OffloadBinary::Header *>(&Buffer[0]);
if (Doc.Version)
TheHeader->Version = *Doc.Version;
if (Doc.Size)
TheHeader->Size = *Doc.Size;
if (Doc.EntryOffset)
TheHeader->EntryOffset = *Doc.EntryOffset;
if (Doc.EntrySize)
TheHeader->EntrySize = *Doc.EntrySize;
Out.write(Buffer.begin(), Buffer.size());
Images.push_back(std::move(Image));
}
// Copy the data to a new buffer so we can modify the bytes directly.
auto Buffer = object::OffloadBinary::write(Images);
auto *TheHeader =
reinterpret_cast<object::OffloadBinary::Header *>(&Buffer[0]);
if (Doc.Version)
TheHeader->Version = *Doc.Version;
if (Doc.Size)
TheHeader->Size = *Doc.Size;
if (Doc.EntriesOffset)
TheHeader->EntriesOffset = *Doc.EntriesOffset;
if (Doc.EntriesCount)
TheHeader->EntriesCount = *Doc.EntriesCount;
Out.write(Buffer.begin(), Buffer.size());
return true;
}

View File

@ -38,6 +38,7 @@ void ScalarEnumerationTraits<object::OffloadKind>::enumeration(
ECase(OFK_OpenMP);
ECase(OFK_Cuda);
ECase(OFK_HIP);
ECase(OFK_SYCL);
ECase(OFK_LAST);
#undef ECase
IO.enumFallback<Hex16>(Value);
@ -50,8 +51,8 @@ void MappingTraits<OffloadYAML::Binary>::mapping(IO &IO,
IO.mapTag("!Offload", true);
IO.mapOptional("Version", O.Version);
IO.mapOptional("Size", O.Size);
IO.mapOptional("EntryOffset", O.EntryOffset);
IO.mapOptional("EntrySize", O.EntrySize);
IO.mapOptional("EntriesOffset", O.EntriesOffset);
IO.mapOptional("EntriesCount", O.EntriesCount);
IO.mapRequired("Members", O.Members);
IO.setContext(nullptr);
}

View File

@ -1,6 +1,6 @@
# RUN: yaml2obj %s | not obj2yaml 2>&1 | FileCheck %s
!Offload
EntrySize: 999999999
EntriesCount: 999999999
Members:
- ImageKind: IMG_Cubin
OffloadKind: OFK_OpenMP

View File

@ -1,6 +1,6 @@
# RUN: yaml2obj %s | not obj2yaml 2>&1 | FileCheck %s
!Offload
EntryOffset: 999999999
EntriesOffset: 999999999
Members:
- ImageKind: IMG_Cubin
OffloadKind: OFK_OpenMP

View File

@ -1,6 +1,6 @@
# RUN: yaml2obj %s | not obj2yaml 2>&1 | FileCheck %s
!Offload
Version: 2
Version: 3
Members:
- ImageKind: IMG_Cubin
OffloadKind: OFK_OpenMP

View File

@ -18,7 +18,14 @@ Members:
Value: "amdgcn-amd-amdhsa"
- Key: "arch"
Value: "gfx908"
Content: "cafefeed"
Content: "cafefeed"
- ImageKind: IMG_Object
OffloadKind: OFK_SYCL
# OIF_Metadata
Flags: 1
String:
- Key: "device"
Value: "gpu"
# CHECK: --- !Offload
# CHECK-NEXT: Members:
@ -40,4 +47,10 @@ Members:
# CHECK-NEXT: - Key: arch
# CHECK-NEXT: Value: gfx908
# CHECK-NEXT: Content: CAFEFEED
# CHECK-NEXT: - ImageKind: IMG_Object
# CHECK-NEXT: OffloadKind: OFK_SYCL
# CHECK-NEXT: Flags: 1
# CHECK-NEXT: String:
# CHECK-NEXT: - Key: device
# CHECK-NEXT: Value: gpu
# CHECK-NEXT: ...

View File

@ -16,49 +16,49 @@ using namespace llvm;
namespace {
void populateYAML(OffloadYAML::Binary &YAMLBinary, object::OffloadBinary &OB,
void populateYAML(OffloadYAML::Binary &YAMLBinary,
ArrayRef<std::unique_ptr<object::OffloadBinary>> OBinaries,
UniqueStringSaver Saver) {
YAMLBinary.Members.emplace_back();
auto &Member = YAMLBinary.Members.back();
Member.ImageKind = OB.getImageKind();
Member.OffloadKind = OB.getOffloadKind();
Member.Flags = OB.getFlags();
if (!OB.strings().empty()) {
Member.StringEntries = std::vector<OffloadYAML::Binary::StringEntry>();
for (const auto &Entry : OB.strings())
Member.StringEntries->emplace_back(OffloadYAML::Binary::StringEntry(
{Saver.save(Entry.first), Saver.save(Entry.second)}));
}
for (const auto &OBinaryPtr : OBinaries) {
object::OffloadBinary &OB = *OBinaryPtr;
if (!OB.getImage().empty())
Member.Content = arrayRefFromStringRef(OB.getImage());
YAMLBinary.Members.emplace_back();
auto &Member = YAMLBinary.Members.back();
Member.ImageKind = OB.getImageKind();
Member.OffloadKind = OB.getOffloadKind();
Member.Flags = OB.getFlags();
if (!OB.strings().empty()) {
Member.StringEntries = std::vector<OffloadYAML::Binary::StringEntry>();
for (const auto &StringEntry : OB.strings())
Member.StringEntries->emplace_back(OffloadYAML::Binary::StringEntry(
{Saver.save(StringEntry.first), Saver.save(StringEntry.second)}));
}
if (!OB.getImage().empty())
Member.Content = arrayRefFromStringRef(OB.getImage());
}
}
Expected<OffloadYAML::Binary *> dump(MemoryBufferRef Source,
UniqueStringSaver Saver) {
Expected<std::unique_ptr<object::OffloadBinary>> OB =
object::OffloadBinary::create(Source);
if (!OB)
return OB.takeError();
std::unique_ptr<OffloadYAML::Binary> YAMLBinary =
std::make_unique<OffloadYAML::Binary>();
YAMLBinary->Members = std::vector<OffloadYAML::Binary::Member>();
uint64_t Offset = 0;
while (Offset < (*OB)->getMemoryBufferRef().getBufferSize()) {
while (Offset < Source.getBufferSize()) {
MemoryBufferRef Buffer = MemoryBufferRef(
(*OB)->getData().drop_front(Offset), (*OB)->getFileName());
auto BinaryOrErr = object::OffloadBinary::create(Buffer);
if (!BinaryOrErr)
return BinaryOrErr.takeError();
Source.getBuffer().drop_front(Offset), Source.getBufferIdentifier());
auto BinariesOrErr = object::OffloadBinary::create(Buffer);
if (!BinariesOrErr)
return BinariesOrErr.takeError();
object::OffloadBinary &Binary = **BinaryOrErr;
SmallVector<std::unique_ptr<object::OffloadBinary>> &Binaries =
*BinariesOrErr;
populateYAML(*YAMLBinary, Binaries, Saver);
populateYAML(*YAMLBinary, Binary, Saver);
Offset += Binary.getSize();
Offset += Binaries[0]->getSize();
}
return YAMLBinary.release();

View File

@ -50,7 +50,9 @@ TEST(OffloadingTest, checkOffloadingBinary) {
FAIL();
// Make sure we get the same data out.
auto &Binary = **BinaryOrErr;
auto &Binaries = *BinaryOrErr;
ASSERT_EQ(Binaries.size(), 1u);
auto &Binary = *Binaries[0];
ASSERT_EQ(Data.TheImageKind, Binary.getImageKind());
ASSERT_EQ(Data.TheOffloadKind, Binary.getOffloadKind());
ASSERT_EQ(Data.Flags, Binary.getFlags());
@ -65,3 +67,209 @@ TEST(OffloadingTest, checkOffloadingBinary) {
EXPECT_TRUE(Binary.getSize() % OffloadBinary::getAlignment() == 0);
EXPECT_TRUE(Binary.getSize() == BinaryBuffer->getBuffer().size());
}
static std::unique_ptr<MemoryBuffer>
createMultiEntryBinary(size_t NumEntries,
SmallVectorImpl<std::string> &StringStorage) {
// Reserve space to prevent reallocation which would invalidate StringRefs.
// Each entry needs: "id", id_value, "arch", arch_value, image_content = 5
// strings.
StringStorage.reserve(NumEntries * 5);
SmallVector<OffloadBinary::OffloadingImage> Images;
for (size_t i = 0; i < NumEntries; ++i) {
OffloadBinary::OffloadingImage Data;
Data.TheImageKind = static_cast<ImageKind>(i % IMG_LAST);
Data.TheOffloadKind = static_cast<OffloadKind>(i % OFK_LAST);
MapVector<StringRef, StringRef> StringData;
StringStorage.push_back("id");
StringStorage.push_back(std::to_string(i));
StringData[StringStorage[StringStorage.size() - 2]] =
StringStorage[StringStorage.size() - 1];
StringStorage.push_back("arch");
StringStorage.push_back("gpu" + std::to_string(i));
StringData[StringStorage[StringStorage.size() - 2]] =
StringStorage[StringStorage.size() - 1];
Data.StringData = StringData;
// Make the last entry metadata-only (no image)
if (i == NumEntries - 1) {
Data.Flags = OIF_Metadata;
Data.Image = MemoryBuffer::getMemBuffer("", "", false);
} else {
Data.Flags = i * 100;
StringStorage.push_back("ImageData" + std::to_string(i));
Data.Image = MemoryBuffer::getMemBuffer(StringStorage.back(), "", false);
}
Images.push_back(std::move(Data));
}
return MemoryBuffer::getMemBufferCopy(OffloadBinary::write(Images));
}
// Test multi-entry binaries and extraction without index (get all entries).
TEST(OffloadingTest, checkMultiEntryBinaryExtraction) {
const size_t NumEntries = 5;
SmallVector<std::string> StringStorage;
auto BinaryBuffer = createMultiEntryBinary(NumEntries, StringStorage);
// Test extracting all entries (no index).
auto BinariesOrErr = OffloadBinary::create(*BinaryBuffer);
ASSERT_THAT_EXPECTED(BinariesOrErr, Succeeded());
auto &Binaries = *BinariesOrErr;
ASSERT_EQ(Binaries.size(), NumEntries)
<< "Expected all entries when no index provided";
// Verify each entry.
for (size_t i = 0; i < NumEntries; ++i) {
auto &Binary = *Binaries[i];
EXPECT_EQ(Binary.getImageKind(), static_cast<ImageKind>(i % IMG_LAST));
EXPECT_EQ(Binary.getOffloadKind(), static_cast<OffloadKind>(i % OFK_LAST));
EXPECT_EQ(Binary.getIndex(), i);
std::string ExpectedId = std::to_string(i);
std::string ExpectedArch = "gpu" + std::to_string(i);
EXPECT_EQ(Binary.getString("id"), ExpectedId);
EXPECT_EQ(Binary.getString("arch"), ExpectedArch);
// Last entry is metadata-only.
if (i == NumEntries - 1) {
EXPECT_EQ(Binary.getFlags(), OIF_Metadata);
EXPECT_TRUE(Binary.getImage().empty());
} else {
EXPECT_EQ(Binary.getFlags(), i * 100);
std::string ExpectedImage = "ImageData" + std::to_string(i);
EXPECT_EQ(Binary.getImage(), ExpectedImage);
}
}
// Ensure the size and alignment of the data is correct.
EXPECT_TRUE(Binaries[0]->getSize() % OffloadBinary::getAlignment() == 0);
EXPECT_TRUE(Binaries[0]->getSize() == BinaryBuffer->getBuffer().size());
}
// Test index-based extraction from multi-entry binary.
TEST(OffloadingTest, checkIndexBasedExtraction) {
const size_t NumEntries = 5;
SmallVector<std::string> StringStorage;
auto BinaryBuffer = createMultiEntryBinary(NumEntries, StringStorage);
// Test extracting specific indices.
for (uint64_t i = 0; i < NumEntries; ++i) {
auto BinariesOrErr = OffloadBinary::create(*BinaryBuffer, i);
ASSERT_THAT_EXPECTED(BinariesOrErr, Succeeded());
auto &Binaries = *BinariesOrErr;
ASSERT_EQ(Binaries.size(), 1u) << "Expected single entry when using index";
auto &Binary = *Binaries[0];
EXPECT_EQ(Binary.getImageKind(), static_cast<ImageKind>(i % IMG_LAST));
EXPECT_EQ(Binary.getOffloadKind(), static_cast<OffloadKind>(i % OFK_LAST));
EXPECT_EQ(Binary.getIndex(), i);
std::string ExpectedId = std::to_string(i);
std::string ExpectedArch = "gpu" + std::to_string(i);
EXPECT_EQ(Binary.getString("id"), ExpectedId);
EXPECT_EQ(Binary.getString("arch"), ExpectedArch);
// Last entry is metadata-only.
if (i == NumEntries - 1) {
EXPECT_EQ(Binary.getFlags(), OIF_Metadata);
EXPECT_TRUE(Binary.getImage().empty());
} else {
EXPECT_EQ(Binary.getFlags(), i * 100);
std::string ExpectedImage = "ImageData" + std::to_string(i);
EXPECT_EQ(Binary.getImage(), ExpectedImage);
}
}
// Test out-of-bounds index.
auto OutOfBoundsOrErr = OffloadBinary::create(*BinaryBuffer, NumEntries + 10);
EXPECT_THAT_EXPECTED(OutOfBoundsOrErr, Failed());
}
TEST(OffloadingTest, checkEdgeCases) {
// Test with empty string data.
{
OffloadBinary::OffloadingImage Data;
Data.TheImageKind = IMG_Object;
Data.TheOffloadKind = OFK_OpenMP;
Data.Flags = 0;
Data.StringData = MapVector<StringRef, StringRef>(); // Empty
std::string ImageContent = "TestImage";
Data.Image = MemoryBuffer::getMemBuffer(ImageContent, "", false);
auto BinaryBuffer =
MemoryBuffer::getMemBufferCopy(OffloadBinary::write(Data));
auto BinariesOrErr = OffloadBinary::create(*BinaryBuffer);
ASSERT_THAT_EXPECTED(BinariesOrErr, Succeeded());
auto &Binaries = *BinariesOrErr;
ASSERT_EQ(Binaries.size(), 1u);
EXPECT_TRUE(Binaries[0]->strings().empty());
EXPECT_EQ(Binaries[0]->getImage(), ImageContent);
}
// Test with empty image data.
{
std::string Key = "test";
std::string Value = "value";
OffloadBinary::OffloadingImage Data;
Data.TheImageKind = IMG_Object;
Data.TheOffloadKind = OFK_SYCL;
Data.Flags = 0;
MapVector<StringRef, StringRef> StringData;
StringData[Key] = Value;
Data.StringData = StringData;
Data.Image = MemoryBuffer::getMemBuffer("", "", false); // Empty image
auto BinaryBuffer =
MemoryBuffer::getMemBufferCopy(OffloadBinary::write(Data));
auto BinariesOrErr = OffloadBinary::create(*BinaryBuffer);
ASSERT_THAT_EXPECTED(BinariesOrErr, Succeeded());
auto &Binaries = *BinariesOrErr;
ASSERT_EQ(Binaries.size(), 1u);
EXPECT_TRUE(Binaries[0]->getImage().empty());
EXPECT_EQ(Binaries[0]->getString("test"), "value");
}
// Test with large string values.
{
std::string Key = "large_key";
std::string LargeValue(4096, 'X'); // Large value
std::string ImageContent = "Image";
OffloadBinary::OffloadingImage Data;
Data.TheImageKind = IMG_Bitcode;
Data.TheOffloadKind = OFK_OpenMP;
Data.Flags = 0;
MapVector<StringRef, StringRef> StringData;
StringData[Key] = LargeValue;
Data.StringData = StringData;
Data.Image = MemoryBuffer::getMemBuffer(ImageContent, "", false);
auto BinaryBuffer =
MemoryBuffer::getMemBufferCopy(OffloadBinary::write(Data));
auto BinariesOrErr = OffloadBinary::create(*BinaryBuffer);
ASSERT_THAT_EXPECTED(BinariesOrErr, Succeeded());
auto &Binaries = *BinariesOrErr;
ASSERT_EQ(Binaries.size(), 1u);
EXPECT_EQ(Binaries[0]->getString("large_key"), LargeValue);
EXPECT_EQ(Binaries[0]->getString("large_key").size(), 4096u);
}
}