Skip to content

[mlir] BytecodeWriter: invoke reserveExtraSpace #126953

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 12, 2025
Merged

Conversation

nikalra
Copy link
Contributor

@nikalra nikalra commented Feb 12, 2025

Update BytecodeWriter to invoke reserveExtraSpace on the stream before writing to it. This will give clients implementing custom output streams the opportunity to allocate an appropriately sized buffer for the write.

@llvmbot
Copy link
Member

llvmbot commented Feb 12, 2025

@llvm/pr-subscribers-mlir

Author: Nikhil Kalra (nikalra)

Changes

For clients wanting to serialize bytecode to an in-memory buffer, there is currently no way to query BytecodeWriter for the required buffer size before the payload is written to the given stream. As a result, users of BytecodeWriter who need to serialize to an in-memory buffer must use raw_svector_ostream or equivalent, which results in repeated memory allocations and copies as the buffer is exhausted.

To solve this, we'll provide a new API for writing bytecode to a memory-mapped buffer that is appropriately sized for the given Operation being encoded. We do this by splitting bytecode encoding and writing into two separate routines so that it's possible to allocate a buffer for the encoded size prior to writing.

Future iterations of this routine may want to optimize encoding such that sections are also written to memory-mapped buffers so that the entire module isn't duplicated in memory prior to being written to the stream.


Full diff: https://github.com/llvm/llvm-project/pull/126953.diff

5 Files Affected:

  • (modified) llvm/include/llvm/Support/raw_ostream.h (+13)
  • (modified) llvm/lib/Support/raw_ostream.cpp (+13)
  • (modified) mlir/include/mlir/Bytecode/BytecodeWriter.h (+6)
  • (modified) mlir/lib/Bytecode/Writer/BytecodeWriter.cpp (+63-8)
  • (modified) mlir/unittests/Bytecode/BytecodeTest.cpp (+22)
diff --git a/llvm/include/llvm/Support/raw_ostream.h b/llvm/include/llvm/Support/raw_ostream.h
index d3b411590e7fd..90c0c013e38c8 100644
--- a/llvm/include/llvm/Support/raw_ostream.h
+++ b/llvm/include/llvm/Support/raw_ostream.h
@@ -13,6 +13,7 @@
 #ifndef LLVM_SUPPORT_RAW_OSTREAM_H
 #define LLVM_SUPPORT_RAW_OSTREAM_H
 
+#include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/DataTypes.h"
@@ -769,6 +770,18 @@ class buffer_unique_ostream : public raw_svector_ostream {
   ~buffer_unique_ostream() override { *OS << str(); }
 };
 
+// Creates an output stream with a fixed size buffer.
+class fixed_buffer_ostream : public raw_ostream {
+  MutableArrayRef<std::byte> Buffer;
+  size_t Pos = 0;
+
+  void write_impl(const char *Ptr, size_t Size) final;
+  uint64_t current_pos() const final { return Pos; }
+
+public:
+  fixed_buffer_ostream(MutableArrayRef<std::byte> Buffer);
+};
+
 // Helper struct to add indentation to raw_ostream. Instead of
 // OS.indent(6) << "more stuff";
 // you can use
diff --git a/llvm/lib/Support/raw_ostream.cpp b/llvm/lib/Support/raw_ostream.cpp
index e75ddc66b7d16..875c14782dd2e 100644
--- a/llvm/lib/Support/raw_ostream.cpp
+++ b/llvm/lib/Support/raw_ostream.cpp
@@ -1009,6 +1009,19 @@ void buffer_ostream::anchor() {}
 
 void buffer_unique_ostream::anchor() {}
 
+void fixed_buffer_ostream::write_impl(const char *Ptr, size_t Size) {
+  if (Pos + Size <= Buffer.size()) {
+    memcpy((void *)(Buffer.data() + Pos), Ptr, Size);
+    Pos += Size;
+  } else {
+    report_fatal_error(
+        "Attempted to write past the end of the fixed size buffer.");
+  }
+}
+
+fixed_buffer_ostream::fixed_buffer_ostream(MutableArrayRef<std::byte> Buffer)
+    : raw_ostream(true), Buffer{Buffer} {}
+
 Error llvm::writeToOutput(StringRef OutputFileName,
                           std::function<Error(raw_ostream &)> Write) {
   if (OutputFileName == "-")
diff --git a/mlir/include/mlir/Bytecode/BytecodeWriter.h b/mlir/include/mlir/Bytecode/BytecodeWriter.h
index c6cff0bc81314..4945adc3e9304 100644
--- a/mlir/include/mlir/Bytecode/BytecodeWriter.h
+++ b/mlir/include/mlir/Bytecode/BytecodeWriter.h
@@ -192,6 +192,12 @@ class BytecodeWriterConfig {
 LogicalResult writeBytecodeToFile(Operation *op, raw_ostream &os,
                                   const BytecodeWriterConfig &config = {});
 
+/// Writes the bytecode for the given operation to a memory-mapped buffer.
+/// It only ever fails if setDesiredByteCodeVersion can't be honored.
+/// Returns nullptr on failure.
+std::shared_ptr<ArrayRef<std::byte>>
+writeBytecode(Operation *op, const BytecodeWriterConfig &config = {});
+
 } // namespace mlir
 
 #endif // MLIR_BYTECODE_BYTECODEWRITER_H
diff --git a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
index 2b4697434717d..c2a33e897ec07 100644
--- a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
+++ b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
@@ -20,8 +20,11 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/Endian.h"
+#include "llvm/Support/Memory.h"
 #include "llvm/Support/raw_ostream.h"
+#include <cstddef>
 #include <optional>
+#include <system_error>
 
 #define DEBUG_TYPE "mlir-bytecode-writer"
 
@@ -652,7 +655,7 @@ class BytecodeWriter {
         propertiesSection(numberingState, stringSection, config.getImpl()) {}
 
   /// Write the bytecode for the given root operation.
-  LogicalResult write(Operation *rootOp, raw_ostream &os);
+  LogicalResult writeInto(Operation *rootOp, EncodingEmitter &emitter);
 
 private:
   //===--------------------------------------------------------------------===//
@@ -718,9 +721,8 @@ class BytecodeWriter {
 };
 } // namespace
 
-LogicalResult BytecodeWriter::write(Operation *rootOp, raw_ostream &os) {
-  EncodingEmitter emitter;
-
+LogicalResult BytecodeWriter::writeInto(Operation *rootOp,
+                                        EncodingEmitter &emitter) {
   // Emit the bytecode file header. This is how we identify the output as a
   // bytecode file.
   emitter.emitString("ML\xefR", "bytecode header");
@@ -761,9 +763,6 @@ LogicalResult BytecodeWriter::write(Operation *rootOp, raw_ostream &os) {
     return rootOp->emitError(
         "unexpected properties emitted incompatible with bytecode <5");
 
-  // Write the generated bytecode to the provided output stream.
-  emitter.writeTo(os);
-
   return success();
 }
 
@@ -1348,5 +1347,61 @@ void BytecodeWriter::writePropertiesSection(EncodingEmitter &emitter) {
 LogicalResult mlir::writeBytecodeToFile(Operation *op, raw_ostream &os,
                                         const BytecodeWriterConfig &config) {
   BytecodeWriter writer(op, config);
-  return writer.write(op, os);
+  EncodingEmitter emitter;
+
+  if (succeeded(writer.writeInto(op, emitter))) {
+    emitter.writeTo(os);
+    return success();
+  }
+
+  return failure();
+}
+
+namespace {
+struct MemoryMappedBlock {
+  static std::shared_ptr<MemoryMappedBlock>
+  createMemoryMappedBlock(size_t numBytes) {
+    auto instance = std::make_shared<MemoryMappedBlock>();
+
+    std::error_code ec;
+    instance->mmapBlock =
+        llvm::sys::OwningMemoryBlock{llvm::sys::Memory::allocateMappedMemory(
+            numBytes, nullptr, llvm::sys::Memory::MF_WRITE, ec)};
+    if (ec)
+      return nullptr;
+
+    instance->writableView = MutableArrayRef<std::byte>(
+        (std::byte *)instance->mmapBlock.base(), numBytes);
+
+    return instance;
+  }
+
+  llvm::sys::OwningMemoryBlock mmapBlock;
+  MutableArrayRef<std::byte> writableView;
+};
+} // namespace
+
+std::shared_ptr<ArrayRef<std::byte>>
+mlir::writeBytecode(Operation *op, const BytecodeWriterConfig &config) {
+  BytecodeWriter writer(op, config);
+  EncodingEmitter emitter;
+  if (succeeded(writer.writeInto(op, emitter))) {
+    // Allocate a new memory block for the emitter to write into.
+    auto block = MemoryMappedBlock::createMemoryMappedBlock(emitter.size());
+    if (!block)
+      return nullptr;
+
+    // Wrap the block in an output stream.
+    llvm::fixed_buffer_ostream stream(block->writableView);
+    emitter.writeTo(stream);
+
+    // Write protect the block.
+    if (llvm::sys::Memory::protectMappedMemory(
+            block->mmapBlock.getMemoryBlock(), llvm::sys::Memory::MF_READ))
+      return nullptr;
+
+    return std::shared_ptr<ArrayRef<std::byte>>(block, &block->writableView);
+  }
+
+  return nullptr;
 }
diff --git a/mlir/unittests/Bytecode/BytecodeTest.cpp b/mlir/unittests/Bytecode/BytecodeTest.cpp
index cb915a092a0be..a3c069fbcab58 100644
--- a/mlir/unittests/Bytecode/BytecodeTest.cpp
+++ b/mlir/unittests/Bytecode/BytecodeTest.cpp
@@ -16,9 +16,11 @@
 
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Endian.h"
+#include "llvm/Support/LogicalResult.h"
 #include "llvm/Support/MemoryBufferRef.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include <cstring>
 
 using namespace llvm;
 using namespace mlir;
@@ -88,6 +90,26 @@ TEST(Bytecode, MultiModuleWithResource) {
   checkResourceAttribute(*roundTripModule);
 }
 
+TEST(Bytecode, WriteEquivalence) {
+  MLIRContext context;
+  Builder builder(&context);
+  ParserConfig parseConfig(&context);
+  OwningOpRef<Operation *> module =
+      parseSourceString<Operation *>(irWithResources, parseConfig);
+  ASSERT_TRUE(module);
+
+  // Write the module to bytecode
+  std::string buffer;
+  llvm::raw_string_ostream ostream(buffer);
+  ASSERT_TRUE(succeeded(writeBytecodeToFile(module.get(), ostream)));
+
+  // Write the module to bytecode using the mmap API.
+  auto writeBuffer = writeBytecode(module.get());
+  ASSERT_TRUE(writeBuffer);
+  ASSERT_EQ(writeBuffer->size(), buffer.size());
+  ASSERT_EQ(memcmp(buffer.data(), writeBuffer->data(), writeBuffer->size()), 0);
+}
+
 namespace {
 /// A custom operation for the purpose of showcasing how discardable attributes
 /// are handled in absence of properties.

@llvmbot
Copy link
Member

llvmbot commented Feb 12, 2025

@llvm/pr-subscribers-llvm-support

Author: Nikhil Kalra (nikalra)

Changes

For clients wanting to serialize bytecode to an in-memory buffer, there is currently no way to query BytecodeWriter for the required buffer size before the payload is written to the given stream. As a result, users of BytecodeWriter who need to serialize to an in-memory buffer must use raw_svector_ostream or equivalent, which results in repeated memory allocations and copies as the buffer is exhausted.

To solve this, we'll provide a new API for writing bytecode to a memory-mapped buffer that is appropriately sized for the given Operation being encoded. We do this by splitting bytecode encoding and writing into two separate routines so that it's possible to allocate a buffer for the encoded size prior to writing.

Future iterations of this routine may want to optimize encoding such that sections are also written to memory-mapped buffers so that the entire module isn't duplicated in memory prior to being written to the stream.


Full diff: https://github.com/llvm/llvm-project/pull/126953.diff

5 Files Affected:

  • (modified) llvm/include/llvm/Support/raw_ostream.h (+13)
  • (modified) llvm/lib/Support/raw_ostream.cpp (+13)
  • (modified) mlir/include/mlir/Bytecode/BytecodeWriter.h (+6)
  • (modified) mlir/lib/Bytecode/Writer/BytecodeWriter.cpp (+63-8)
  • (modified) mlir/unittests/Bytecode/BytecodeTest.cpp (+22)
diff --git a/llvm/include/llvm/Support/raw_ostream.h b/llvm/include/llvm/Support/raw_ostream.h
index d3b411590e7fd..90c0c013e38c8 100644
--- a/llvm/include/llvm/Support/raw_ostream.h
+++ b/llvm/include/llvm/Support/raw_ostream.h
@@ -13,6 +13,7 @@
 #ifndef LLVM_SUPPORT_RAW_OSTREAM_H
 #define LLVM_SUPPORT_RAW_OSTREAM_H
 
+#include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/DataTypes.h"
@@ -769,6 +770,18 @@ class buffer_unique_ostream : public raw_svector_ostream {
   ~buffer_unique_ostream() override { *OS << str(); }
 };
 
+// Creates an output stream with a fixed size buffer.
+class fixed_buffer_ostream : public raw_ostream {
+  MutableArrayRef<std::byte> Buffer;
+  size_t Pos = 0;
+
+  void write_impl(const char *Ptr, size_t Size) final;
+  uint64_t current_pos() const final { return Pos; }
+
+public:
+  fixed_buffer_ostream(MutableArrayRef<std::byte> Buffer);
+};
+
 // Helper struct to add indentation to raw_ostream. Instead of
 // OS.indent(6) << "more stuff";
 // you can use
diff --git a/llvm/lib/Support/raw_ostream.cpp b/llvm/lib/Support/raw_ostream.cpp
index e75ddc66b7d16..875c14782dd2e 100644
--- a/llvm/lib/Support/raw_ostream.cpp
+++ b/llvm/lib/Support/raw_ostream.cpp
@@ -1009,6 +1009,19 @@ void buffer_ostream::anchor() {}
 
 void buffer_unique_ostream::anchor() {}
 
+void fixed_buffer_ostream::write_impl(const char *Ptr, size_t Size) {
+  if (Pos + Size <= Buffer.size()) {
+    memcpy((void *)(Buffer.data() + Pos), Ptr, Size);
+    Pos += Size;
+  } else {
+    report_fatal_error(
+        "Attempted to write past the end of the fixed size buffer.");
+  }
+}
+
+fixed_buffer_ostream::fixed_buffer_ostream(MutableArrayRef<std::byte> Buffer)
+    : raw_ostream(true), Buffer{Buffer} {}
+
 Error llvm::writeToOutput(StringRef OutputFileName,
                           std::function<Error(raw_ostream &)> Write) {
   if (OutputFileName == "-")
diff --git a/mlir/include/mlir/Bytecode/BytecodeWriter.h b/mlir/include/mlir/Bytecode/BytecodeWriter.h
index c6cff0bc81314..4945adc3e9304 100644
--- a/mlir/include/mlir/Bytecode/BytecodeWriter.h
+++ b/mlir/include/mlir/Bytecode/BytecodeWriter.h
@@ -192,6 +192,12 @@ class BytecodeWriterConfig {
 LogicalResult writeBytecodeToFile(Operation *op, raw_ostream &os,
                                   const BytecodeWriterConfig &config = {});
 
+/// Writes the bytecode for the given operation to a memory-mapped buffer.
+/// It only ever fails if setDesiredByteCodeVersion can't be honored.
+/// Returns nullptr on failure.
+std::shared_ptr<ArrayRef<std::byte>>
+writeBytecode(Operation *op, const BytecodeWriterConfig &config = {});
+
 } // namespace mlir
 
 #endif // MLIR_BYTECODE_BYTECODEWRITER_H
diff --git a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
index 2b4697434717d..c2a33e897ec07 100644
--- a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
+++ b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
@@ -20,8 +20,11 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/Endian.h"
+#include "llvm/Support/Memory.h"
 #include "llvm/Support/raw_ostream.h"
+#include <cstddef>
 #include <optional>
+#include <system_error>
 
 #define DEBUG_TYPE "mlir-bytecode-writer"
 
@@ -652,7 +655,7 @@ class BytecodeWriter {
         propertiesSection(numberingState, stringSection, config.getImpl()) {}
 
   /// Write the bytecode for the given root operation.
-  LogicalResult write(Operation *rootOp, raw_ostream &os);
+  LogicalResult writeInto(Operation *rootOp, EncodingEmitter &emitter);
 
 private:
   //===--------------------------------------------------------------------===//
@@ -718,9 +721,8 @@ class BytecodeWriter {
 };
 } // namespace
 
-LogicalResult BytecodeWriter::write(Operation *rootOp, raw_ostream &os) {
-  EncodingEmitter emitter;
-
+LogicalResult BytecodeWriter::writeInto(Operation *rootOp,
+                                        EncodingEmitter &emitter) {
   // Emit the bytecode file header. This is how we identify the output as a
   // bytecode file.
   emitter.emitString("ML\xefR", "bytecode header");
@@ -761,9 +763,6 @@ LogicalResult BytecodeWriter::write(Operation *rootOp, raw_ostream &os) {
     return rootOp->emitError(
         "unexpected properties emitted incompatible with bytecode <5");
 
-  // Write the generated bytecode to the provided output stream.
-  emitter.writeTo(os);
-
   return success();
 }
 
@@ -1348,5 +1347,61 @@ void BytecodeWriter::writePropertiesSection(EncodingEmitter &emitter) {
 LogicalResult mlir::writeBytecodeToFile(Operation *op, raw_ostream &os,
                                         const BytecodeWriterConfig &config) {
   BytecodeWriter writer(op, config);
-  return writer.write(op, os);
+  EncodingEmitter emitter;
+
+  if (succeeded(writer.writeInto(op, emitter))) {
+    emitter.writeTo(os);
+    return success();
+  }
+
+  return failure();
+}
+
+namespace {
+struct MemoryMappedBlock {
+  static std::shared_ptr<MemoryMappedBlock>
+  createMemoryMappedBlock(size_t numBytes) {
+    auto instance = std::make_shared<MemoryMappedBlock>();
+
+    std::error_code ec;
+    instance->mmapBlock =
+        llvm::sys::OwningMemoryBlock{llvm::sys::Memory::allocateMappedMemory(
+            numBytes, nullptr, llvm::sys::Memory::MF_WRITE, ec)};
+    if (ec)
+      return nullptr;
+
+    instance->writableView = MutableArrayRef<std::byte>(
+        (std::byte *)instance->mmapBlock.base(), numBytes);
+
+    return instance;
+  }
+
+  llvm::sys::OwningMemoryBlock mmapBlock;
+  MutableArrayRef<std::byte> writableView;
+};
+} // namespace
+
+std::shared_ptr<ArrayRef<std::byte>>
+mlir::writeBytecode(Operation *op, const BytecodeWriterConfig &config) {
+  BytecodeWriter writer(op, config);
+  EncodingEmitter emitter;
+  if (succeeded(writer.writeInto(op, emitter))) {
+    // Allocate a new memory block for the emitter to write into.
+    auto block = MemoryMappedBlock::createMemoryMappedBlock(emitter.size());
+    if (!block)
+      return nullptr;
+
+    // Wrap the block in an output stream.
+    llvm::fixed_buffer_ostream stream(block->writableView);
+    emitter.writeTo(stream);
+
+    // Write protect the block.
+    if (llvm::sys::Memory::protectMappedMemory(
+            block->mmapBlock.getMemoryBlock(), llvm::sys::Memory::MF_READ))
+      return nullptr;
+
+    return std::shared_ptr<ArrayRef<std::byte>>(block, &block->writableView);
+  }
+
+  return nullptr;
 }
diff --git a/mlir/unittests/Bytecode/BytecodeTest.cpp b/mlir/unittests/Bytecode/BytecodeTest.cpp
index cb915a092a0be..a3c069fbcab58 100644
--- a/mlir/unittests/Bytecode/BytecodeTest.cpp
+++ b/mlir/unittests/Bytecode/BytecodeTest.cpp
@@ -16,9 +16,11 @@
 
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Endian.h"
+#include "llvm/Support/LogicalResult.h"
 #include "llvm/Support/MemoryBufferRef.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include <cstring>
 
 using namespace llvm;
 using namespace mlir;
@@ -88,6 +90,26 @@ TEST(Bytecode, MultiModuleWithResource) {
   checkResourceAttribute(*roundTripModule);
 }
 
+TEST(Bytecode, WriteEquivalence) {
+  MLIRContext context;
+  Builder builder(&context);
+  ParserConfig parseConfig(&context);
+  OwningOpRef<Operation *> module =
+      parseSourceString<Operation *>(irWithResources, parseConfig);
+  ASSERT_TRUE(module);
+
+  // Write the module to bytecode
+  std::string buffer;
+  llvm::raw_string_ostream ostream(buffer);
+  ASSERT_TRUE(succeeded(writeBytecodeToFile(module.get(), ostream)));
+
+  // Write the module to bytecode using the mmap API.
+  auto writeBuffer = writeBytecode(module.get());
+  ASSERT_TRUE(writeBuffer);
+  ASSERT_EQ(writeBuffer->size(), buffer.size());
+  ASSERT_EQ(memcmp(buffer.data(), writeBuffer->data(), writeBuffer->size()), 0);
+}
+
 namespace {
 /// A custom operation for the purpose of showcasing how discardable attributes
 /// are handled in absence of properties.

@llvmbot
Copy link
Member

llvmbot commented Feb 12, 2025

@llvm/pr-subscribers-mlir-core

Author: Nikhil Kalra (nikalra)

Changes

For clients wanting to serialize bytecode to an in-memory buffer, there is currently no way to query BytecodeWriter for the required buffer size before the payload is written to the given stream. As a result, users of BytecodeWriter who need to serialize to an in-memory buffer must use raw_svector_ostream or equivalent, which results in repeated memory allocations and copies as the buffer is exhausted.

To solve this, we'll provide a new API for writing bytecode to a memory-mapped buffer that is appropriately sized for the given Operation being encoded. We do this by splitting bytecode encoding and writing into two separate routines so that it's possible to allocate a buffer for the encoded size prior to writing.

Future iterations of this routine may want to optimize encoding such that sections are also written to memory-mapped buffers so that the entire module isn't duplicated in memory prior to being written to the stream.


Full diff: https://github.com/llvm/llvm-project/pull/126953.diff

5 Files Affected:

  • (modified) llvm/include/llvm/Support/raw_ostream.h (+13)
  • (modified) llvm/lib/Support/raw_ostream.cpp (+13)
  • (modified) mlir/include/mlir/Bytecode/BytecodeWriter.h (+6)
  • (modified) mlir/lib/Bytecode/Writer/BytecodeWriter.cpp (+63-8)
  • (modified) mlir/unittests/Bytecode/BytecodeTest.cpp (+22)
diff --git a/llvm/include/llvm/Support/raw_ostream.h b/llvm/include/llvm/Support/raw_ostream.h
index d3b411590e7fd..90c0c013e38c8 100644
--- a/llvm/include/llvm/Support/raw_ostream.h
+++ b/llvm/include/llvm/Support/raw_ostream.h
@@ -13,6 +13,7 @@
 #ifndef LLVM_SUPPORT_RAW_OSTREAM_H
 #define LLVM_SUPPORT_RAW_OSTREAM_H
 
+#include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/DataTypes.h"
@@ -769,6 +770,18 @@ class buffer_unique_ostream : public raw_svector_ostream {
   ~buffer_unique_ostream() override { *OS << str(); }
 };
 
+// Creates an output stream with a fixed size buffer.
+class fixed_buffer_ostream : public raw_ostream {
+  MutableArrayRef<std::byte> Buffer;
+  size_t Pos = 0;
+
+  void write_impl(const char *Ptr, size_t Size) final;
+  uint64_t current_pos() const final { return Pos; }
+
+public:
+  fixed_buffer_ostream(MutableArrayRef<std::byte> Buffer);
+};
+
 // Helper struct to add indentation to raw_ostream. Instead of
 // OS.indent(6) << "more stuff";
 // you can use
diff --git a/llvm/lib/Support/raw_ostream.cpp b/llvm/lib/Support/raw_ostream.cpp
index e75ddc66b7d16..875c14782dd2e 100644
--- a/llvm/lib/Support/raw_ostream.cpp
+++ b/llvm/lib/Support/raw_ostream.cpp
@@ -1009,6 +1009,19 @@ void buffer_ostream::anchor() {}
 
 void buffer_unique_ostream::anchor() {}
 
+void fixed_buffer_ostream::write_impl(const char *Ptr, size_t Size) {
+  if (Pos + Size <= Buffer.size()) {
+    memcpy((void *)(Buffer.data() + Pos), Ptr, Size);
+    Pos += Size;
+  } else {
+    report_fatal_error(
+        "Attempted to write past the end of the fixed size buffer.");
+  }
+}
+
+fixed_buffer_ostream::fixed_buffer_ostream(MutableArrayRef<std::byte> Buffer)
+    : raw_ostream(true), Buffer{Buffer} {}
+
 Error llvm::writeToOutput(StringRef OutputFileName,
                           std::function<Error(raw_ostream &)> Write) {
   if (OutputFileName == "-")
diff --git a/mlir/include/mlir/Bytecode/BytecodeWriter.h b/mlir/include/mlir/Bytecode/BytecodeWriter.h
index c6cff0bc81314..4945adc3e9304 100644
--- a/mlir/include/mlir/Bytecode/BytecodeWriter.h
+++ b/mlir/include/mlir/Bytecode/BytecodeWriter.h
@@ -192,6 +192,12 @@ class BytecodeWriterConfig {
 LogicalResult writeBytecodeToFile(Operation *op, raw_ostream &os,
                                   const BytecodeWriterConfig &config = {});
 
+/// Writes the bytecode for the given operation to a memory-mapped buffer.
+/// It only ever fails if setDesiredByteCodeVersion can't be honored.
+/// Returns nullptr on failure.
+std::shared_ptr<ArrayRef<std::byte>>
+writeBytecode(Operation *op, const BytecodeWriterConfig &config = {});
+
 } // namespace mlir
 
 #endif // MLIR_BYTECODE_BYTECODEWRITER_H
diff --git a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
index 2b4697434717d..c2a33e897ec07 100644
--- a/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
+++ b/mlir/lib/Bytecode/Writer/BytecodeWriter.cpp
@@ -20,8 +20,11 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/Endian.h"
+#include "llvm/Support/Memory.h"
 #include "llvm/Support/raw_ostream.h"
+#include <cstddef>
 #include <optional>
+#include <system_error>
 
 #define DEBUG_TYPE "mlir-bytecode-writer"
 
@@ -652,7 +655,7 @@ class BytecodeWriter {
         propertiesSection(numberingState, stringSection, config.getImpl()) {}
 
   /// Write the bytecode for the given root operation.
-  LogicalResult write(Operation *rootOp, raw_ostream &os);
+  LogicalResult writeInto(Operation *rootOp, EncodingEmitter &emitter);
 
 private:
   //===--------------------------------------------------------------------===//
@@ -718,9 +721,8 @@ class BytecodeWriter {
 };
 } // namespace
 
-LogicalResult BytecodeWriter::write(Operation *rootOp, raw_ostream &os) {
-  EncodingEmitter emitter;
-
+LogicalResult BytecodeWriter::writeInto(Operation *rootOp,
+                                        EncodingEmitter &emitter) {
   // Emit the bytecode file header. This is how we identify the output as a
   // bytecode file.
   emitter.emitString("ML\xefR", "bytecode header");
@@ -761,9 +763,6 @@ LogicalResult BytecodeWriter::write(Operation *rootOp, raw_ostream &os) {
     return rootOp->emitError(
         "unexpected properties emitted incompatible with bytecode <5");
 
-  // Write the generated bytecode to the provided output stream.
-  emitter.writeTo(os);
-
   return success();
 }
 
@@ -1348,5 +1347,61 @@ void BytecodeWriter::writePropertiesSection(EncodingEmitter &emitter) {
 LogicalResult mlir::writeBytecodeToFile(Operation *op, raw_ostream &os,
                                         const BytecodeWriterConfig &config) {
   BytecodeWriter writer(op, config);
-  return writer.write(op, os);
+  EncodingEmitter emitter;
+
+  if (succeeded(writer.writeInto(op, emitter))) {
+    emitter.writeTo(os);
+    return success();
+  }
+
+  return failure();
+}
+
+namespace {
+struct MemoryMappedBlock {
+  static std::shared_ptr<MemoryMappedBlock>
+  createMemoryMappedBlock(size_t numBytes) {
+    auto instance = std::make_shared<MemoryMappedBlock>();
+
+    std::error_code ec;
+    instance->mmapBlock =
+        llvm::sys::OwningMemoryBlock{llvm::sys::Memory::allocateMappedMemory(
+            numBytes, nullptr, llvm::sys::Memory::MF_WRITE, ec)};
+    if (ec)
+      return nullptr;
+
+    instance->writableView = MutableArrayRef<std::byte>(
+        (std::byte *)instance->mmapBlock.base(), numBytes);
+
+    return instance;
+  }
+
+  llvm::sys::OwningMemoryBlock mmapBlock;
+  MutableArrayRef<std::byte> writableView;
+};
+} // namespace
+
+std::shared_ptr<ArrayRef<std::byte>>
+mlir::writeBytecode(Operation *op, const BytecodeWriterConfig &config) {
+  BytecodeWriter writer(op, config);
+  EncodingEmitter emitter;
+  if (succeeded(writer.writeInto(op, emitter))) {
+    // Allocate a new memory block for the emitter to write into.
+    auto block = MemoryMappedBlock::createMemoryMappedBlock(emitter.size());
+    if (!block)
+      return nullptr;
+
+    // Wrap the block in an output stream.
+    llvm::fixed_buffer_ostream stream(block->writableView);
+    emitter.writeTo(stream);
+
+    // Write protect the block.
+    if (llvm::sys::Memory::protectMappedMemory(
+            block->mmapBlock.getMemoryBlock(), llvm::sys::Memory::MF_READ))
+      return nullptr;
+
+    return std::shared_ptr<ArrayRef<std::byte>>(block, &block->writableView);
+  }
+
+  return nullptr;
 }
diff --git a/mlir/unittests/Bytecode/BytecodeTest.cpp b/mlir/unittests/Bytecode/BytecodeTest.cpp
index cb915a092a0be..a3c069fbcab58 100644
--- a/mlir/unittests/Bytecode/BytecodeTest.cpp
+++ b/mlir/unittests/Bytecode/BytecodeTest.cpp
@@ -16,9 +16,11 @@
 
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Endian.h"
+#include "llvm/Support/LogicalResult.h"
 #include "llvm/Support/MemoryBufferRef.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include <cstring>
 
 using namespace llvm;
 using namespace mlir;
@@ -88,6 +90,26 @@ TEST(Bytecode, MultiModuleWithResource) {
   checkResourceAttribute(*roundTripModule);
 }
 
+TEST(Bytecode, WriteEquivalence) {
+  MLIRContext context;
+  Builder builder(&context);
+  ParserConfig parseConfig(&context);
+  OwningOpRef<Operation *> module =
+      parseSourceString<Operation *>(irWithResources, parseConfig);
+  ASSERT_TRUE(module);
+
+  // Write the module to bytecode
+  std::string buffer;
+  llvm::raw_string_ostream ostream(buffer);
+  ASSERT_TRUE(succeeded(writeBytecodeToFile(module.get(), ostream)));
+
+  // Write the module to bytecode using the mmap API.
+  auto writeBuffer = writeBytecode(module.get());
+  ASSERT_TRUE(writeBuffer);
+  ASSERT_EQ(writeBuffer->size(), buffer.size());
+  ASSERT_EQ(memcmp(buffer.data(), writeBuffer->data(), writeBuffer->size()), 0);
+}
+
 namespace {
 /// A custom operation for the purpose of showcasing how discardable attributes
 /// are handled in absence of properties.

Copy link
Contributor

@River707 River707 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not yet clear to me the utility of this API. I'd prefer to keep the interface constrained to just going through raw_ostream, instead of adding additional API (especially for this one which has somewhat complex constraints/contract with the user).

It seems like the only thing really missing from the actual bytecode logic is to call reserveExtraSpace on the provided raw_ostream before the writeTo call? That would allow for custom ostreams to do whatever logic they need to enable avoiding multiple allocations when writing to a buffer.

@nikalra
Copy link
Contributor Author

nikalra commented Feb 12, 2025

It's not yet clear to me the utility of this API. I'd prefer to keep the interface constrained to just going through raw_ostream, instead of adding additional API (especially for this one which has somewhat complex constraints/contract with the user).

It seems like the only thing really missing from the actual bytecode logic is to call reserveExtraSpace on the provided raw_ostream before the writeTo call? That would allow for custom ostreams to do whatever logic they need to enable avoiding multiple allocations when writing to a buffer.

Ah, thank you! I was hoping there was an easier way to do this. I'll update the PR to do that.

@nikalra nikalra changed the title [mlir] API to serialize bytecode to mmap'd buffer [mlir] BytecodeWriter: invoke reserveExtraSpace Feb 12, 2025
@nikalra nikalra requested a review from River707 February 12, 2025 20:54
Copy link
Contributor

@River707 River707 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@nikalra nikalra merged commit f3a2990 into llvm:main Feb 12, 2025
8 checks passed
@nikalra nikalra deleted the bytecode-size branch February 12, 2025 22:17
flovent pushed a commit to flovent/llvm-project that referenced this pull request Feb 13, 2025
Update `BytecodeWriter` to invoke `reserveExtraSpace` on the stream
before writing to it. This will give clients implementing custom output
streams the opportunity to allocate an appropriately sized buffer for
the write.
joaosaffran pushed a commit to joaosaffran/llvm-project that referenced this pull request Feb 14, 2025
Update `BytecodeWriter` to invoke `reserveExtraSpace` on the stream
before writing to it. This will give clients implementing custom output
streams the opportunity to allocate an appropriately sized buffer for
the write.
sivan-shani pushed a commit to sivan-shani/llvm-project that referenced this pull request Feb 24, 2025
Update `BytecodeWriter` to invoke `reserveExtraSpace` on the stream
before writing to it. This will give clients implementing custom output
streams the opportunity to allocate an appropriately sized buffer for
the write.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
llvm:support mlir:core MLIR Core Infrastructure mlir
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants