Files
llvm-project/llvm/unittests/Object/OffloadingTest.cpp
Yury Plyakhin 4d27530c69 [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>
2026-02-05 07:46:57 -08:00

276 lines
9.7 KiB
C++

#include "llvm/Object/OffloadBinary.h"
#include "llvm/Testing/Support/Error.h"
#include "gtest/gtest.h"
#include <random>
using namespace llvm;
using namespace llvm::object;
TEST(OffloadingTest, checkOffloadingBinary) {
// Create random data to fill the image.
std::mt19937 Rng(std::random_device{}());
std::uniform_int_distribution<uint64_t> SizeDist(0, 256);
std::uniform_int_distribution<uint16_t> KindDist(0);
std::uniform_int_distribution<uint16_t> BinaryDist(
std::numeric_limits<uint8_t>::min(), std::numeric_limits<uint8_t>::max());
std::uniform_int_distribution<int16_t> StringDist('!', '~');
std::vector<uint8_t> Image(SizeDist(Rng));
std::generate(Image.begin(), Image.end(), [&]() { return BinaryDist(Rng); });
std::vector<std::pair<std::string, std::string>> Strings(SizeDist(Rng));
for (auto &KeyAndValue : Strings) {
std::string Key(SizeDist(Rng), '\0');
std::string Value(SizeDist(Rng), '\0');
std::generate(Key.begin(), Key.end(), [&]() { return StringDist(Rng); });
std::generate(Value.begin(), Value.end(),
[&]() { return StringDist(Rng); });
KeyAndValue = std::make_pair(Key, Value);
}
// Create the image.
MapVector<StringRef, StringRef> StringData;
for (auto &KeyAndValue : Strings)
StringData[KeyAndValue.first] = KeyAndValue.second;
std::unique_ptr<MemoryBuffer> ImageData = MemoryBuffer::getMemBuffer(
{reinterpret_cast<char *>(Image.data()), Image.size()}, "", false);
OffloadBinary::OffloadingImage Data;
Data.TheImageKind = static_cast<ImageKind>(KindDist(Rng));
Data.TheOffloadKind = static_cast<OffloadKind>(KindDist(Rng));
Data.Flags = KindDist(Rng);
Data.StringData = StringData;
Data.Image = std::move(ImageData);
auto BinaryBuffer =
MemoryBuffer::getMemBufferCopy(OffloadBinary::write(Data));
auto BinaryOrErr = OffloadBinary::create(*BinaryBuffer);
if (!BinaryOrErr)
FAIL();
// Make sure we get the same data out.
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());
for (auto &KeyAndValue : Strings)
ASSERT_TRUE(StringData[KeyAndValue.first] ==
Binary.getString(KeyAndValue.first));
EXPECT_TRUE(Data.Image->getBuffer() == Binary.getImage());
// Ensure the size and alignment of the data is correct.
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);
}
}