3. PopEF library usage

The PopEF library provides both a C++ API and a Python API to write and read PopEF streams.

Note

The examples in this chapter have been written and tested in Python 3.6.9 and C++ version 17.

3.1. Writing a PopEF stream

PopEF streams are written by the Writer class, which writes to an underlying binary output stream. After you have created the Writer object, you can add one blob at a time to the stream. Only one blob writer can be active at once, and creating a new instance of Writer will deactivate the current instance. Interacting with a deactivated blob writer will throw an exception.

The following examples show how to write two blobs to a file and read them from those files.

Listing 3.1 Writing a PopEF stream (example in C++)
// Copyright (c) 2022 Graphcore Ltd. All rights reserved.

#include <exception>
#include <experimental/filesystem>
#include <fstream>
#include <iostream>

#include "popef/Reader.hpp"
#include "popef/Writer.hpp"

void removeFilesIfExists(const std::string &file_name) {
  if (std::experimental::filesystem::exists(file_name))
    std::remove(file_name.c_str());
}

int main() {
  const std::string file_name = "file.popef";

  try {
    /*** preparation of data for saving in individual blobs ***/
    static constexpr unsigned expected_num_of_executables = 1;
    static constexpr unsigned expected_num_of_opaque_blobs = 1;

    const std::string executable_name = "executable";
    const std::string opaque_name = "opaque";
    const std::string executable_content = "executableContent";
    const std::string opaque_content = "opaqueContent";

    /*** creating and saving blobs ***/
    // Note that the stream passed to the popef::Writer must be
    // opened in binary mode (PopEF expects that).
    std::ofstream stream(file_name, std::fstream::binary);
    popef::Writer writer(stream);
    // executable blob
    static constexpr bool compress = false;
    std::shared_ptr<popef::BlobWriter> executable_blob =
        writer.createExecutable(executable_name, compress);
    executable_blob->stream.write(executable_content.c_str(),
                                  executable_content.size());
    // opaque blob
    std::shared_ptr<popef::BlobWriter> opaque_blob =
        writer.createOpaqueBlob(opaque_name, executable_name);
    opaque_blob->stream.write(opaque_content.c_str(), opaque_content.size());
    writer.close();

    popef::Reader reader;
    reader.parseFile(file_name);

    if (reader.executables().size() != expected_num_of_executables ||
        reader.opaqueBlobs().size() != expected_num_of_opaque_blobs) {
      throw std::runtime_error(
          "The popef::Reader object contains the incorrect number of blobs.");
    }

    std::cout << "Executables number: " << reader.executables().size()
              << std::endl;
    std::cout << "Opaque blobs number: " << reader.opaqueBlobs().size()
              << std::endl;

    /*** checking the content correctness of the saved blobs ***/
    popef::OpaqueReader opaque_reader = reader.opaqueBlobs()[0];
    std::istreambuf_iterator<char> it(opaque_reader.data);
    std::istreambuf_iterator<char> end;
    const std::string opaque_content_read(it, end);

    if (opaque_name != opaque_reader.name ||
        executable_name != opaque_reader.executable ||
        opaque_content != opaque_content_read) {
      throw std::runtime_error("The opaque blob contains incorrect data.");
    }

    popef::ExecutableReader executable_reader = reader.executables()[0];
    it = executable_reader.executable;
    const std::string executable_content_read(it, end);

    if (executable_name != executable_reader.name ||
        executable_content != executable_content_read) {
      throw std::runtime_error("The executable blob contains incorrect data.");
    }

    std::cout << "Blobs content: [ Executable : " << executable_content_read
              << "; Opaque : " << opaque_content_read << "]" << std::endl;
  } catch (const std::exception &e) {
    std::cerr << "Exception caught: " << e.what() << std::endl;
    removeFilesIfExists(file_name);
    return EXIT_FAILURE;
  } catch (...) {
    std::cerr << "Exception caught: Something very bad has happened."
              << std::endl;
    removeFilesIfExists(file_name);
    return EXIT_FAILURE;
  }

  removeFilesIfExists(file_name);
  return EXIT_SUCCESS;
}

examples/exec_and_opaque_writer.cpp

Listing 3.2 Writing a PopEF stream (example in Python)
# Copyright (c) 2022 Graphcore Ltd. All rights reserved.

import os
import popef


def removeFileIfExists(fileName):
    if os.path.exists(fileName):
        os.remove(fileName)


fileName = "file.popef"

try:
    # preparation of data for saving in individual blobs #
    executablesNum = 1
    opaqueBlobsNum = 1

    executableName = "executable"
    opaqueName = "opaque"
    executableContent = ("executableContent").encode('ascii')
    opaqueContent = ("opaqueContent").encode('ascii')

    # creating and saving blobs #
    writer = popef.Writer(fileName)
    # executable blob
    compress = False
    executableBlob = writer.createExecutable(executableName, compress)
    executableBlob.write(executableContent, len(executableContent))
    # opaque blob
    opaqueBlob = writer.createOpaqueBlob(opaqueName, executableName)
    opaqueBlob.write(opaqueContent, len(opaqueContent))
    writer.close()

    reader = popef.Reader()
    reader.parseFile(fileName)

    assert len(reader.executables()) == executablesNum, """The popef.Reader
    object contains the incorrect number of executables."""
    assert len(reader.opaqueBlobs()) == opaqueBlobsNum, """The popef.Reader
    object contains the incorrect number of opaque blobs."""

    # checking the content correctness of the saved blobs #
    opaqueReader = reader.opaqueBlobs()[0]
    assert opaqueName == opaqueReader.name, """
    The opaque blob has an incorrect name."""
    assert executableName == opaqueReader.executable, """
    The executable for the opaque has an incorrect name."""
    assert opaqueContent == opaqueReader.data(), """
    The opaque contains incorrect data."""

    executableReader = reader.executables()[0]
    assert executableName == executableReader.name, """
    The executable has an incorrect name."""
    assert executableContent == executableReader.executable(), """
    The executable contains incorrect data."""
except BaseException as e:
    removeFileIfExists(fileName)
    raise e

removeFileIfExists(fileName)

examples/exec_and_opaque_writer.py

3.2. Multi-feed writers

Sometimes it’s useful to write to multiple feeds at the same time in an interleaved fashion. This cannot be accomplished using the Writer class, because of its exclusive nature and so we have the MultiFeedWriter class which does allow for multiple feeds to be written at the same time.

The examples below show how multiple feeds of different shapes and data types can be saved and read.

Listing 3.3 Multi-feed writers (example in C++)
// Copyright (c) 2022 Graphcore Ltd. All rights reserved.

#include <array>
#include <cstring>
#include <exception>
#include <experimental/filesystem>
#include <experimental/iterator>
#include <fstream>
#include <iostream>
#include <iterator>
#include <sstream>

#include "popef/Reader.hpp"
#include "popef/Writer.hpp"

void removeFilesIfExists(const std::string &file_name) {
  if (std::experimental::filesystem::exists(file_name))
    std::remove(file_name.c_str());
}

template <typename T>
std::string toString(const T *array, const size_t array_size) {
  std::stringstream ss;
  ss << "[";
  std::copy(array, array + array_size,
            std::experimental::make_ostream_joiner(ss, ", "));
  ss << "]";
  return ss.str();
}

template <typename T>
void validateFeedContent(const popef::FeedReader &feed_reader,
                         const std::vector<T> flatten_feed) {
  // reading feed content
  std::unique_ptr<std::istream> feed_stream =
      feed_reader.getStandaloneDataStream();
  std::istreambuf_iterator<char> it(*(feed_stream.get()));
  std::istreambuf_iterator<char> end;
  const std::vector<char> data_read(it, end);
  const T *feed_content_read = reinterpret_cast<const T *>(data_read.data());

  // checking data correcteness
  if (std::memcmp(feed_content_read, flatten_feed.data(),
                  flatten_feed.size() * sizeof(T)) != 0) {
    std::stringstream error_msg;
    error_msg << "Feed content does not match expected result." << std::endl;
    error_msg << "Expected content: "
              << toString(flatten_feed.data(), flatten_feed.size())
              << std::endl;
    error_msg << "Feed content: "
              << toString(feed_content_read, data_read.size() / sizeof(T))
              << std::endl;
    throw std::runtime_error(error_msg.str());
  }

  std::cout << "Feed \"" << feed_reader.info.name()
            << "\" contains correct data." << std::endl;
}

int main() {
  const std::string file_name = "feeds.popef";

  try {
    /*** preparation of data for saving in feeds ***/
    static constexpr unsigned expected_num_of_feeds = 2;
    static constexpr int64_t vector_size = 4;
    static constexpr int64_t matrix_size[2] = {3, 2};
    using vector4_double = std::array<double, vector_size>;
    using vector2_int = std::array<int, matrix_size[1]>;
    using mat3x2_int = std::array<vector2_int, matrix_size[0]>;

    const std::string feed_names[2] = {"feed1", "feed2"};
    const popef::DataType feed_data_types[2] = {popef::DataType::F64,
                                                popef::DataType::S32};

    // filling feeds
    std::vector<vector4_double> feed1 = {
        {0.5, 1.5, 2.5, 3.5}, {0.2, 1.2, 2.2, 3.2}, {0.0, 1.0, 2.0, 3.0}};

    std::vector<mat3x2_int> feed2 = {{{{0, 1}, {2, 3}, {4, 5}}},
                                     {{{6, 7}, {8, 9}, {0, 1}}}};

    // preparing data to check correctness
    std::vector<double> flatten_feed1;
    flatten_feed1.reserve(feed1.size() * vector_size);
    for (const vector4_double &vec : feed1) {
      flatten_feed1.insert(flatten_feed1.end(), vec.begin(), vec.end());
    }

    std::vector<int> flatten_feed2;
    flatten_feed1.reserve(feed2.size() * matrix_size[0] * matrix_size[1]);
    for (const mat3x2_int &mat : feed2) {
      for (const vector2_int &vec : mat) {
        flatten_feed2.insert(flatten_feed2.end(), vec.begin(), vec.end());
      }
    }

    // preparing feeds metadata
    popef::TensorInfo tensor_infos[2];
    tensor_infos[0].setShape({vector_size});
    tensor_infos[1].setShape({matrix_size[0], matrix_size[1]});
    tensor_infos[0].setDataType(feed_data_types[0]);
    tensor_infos[1].setDataType(feed_data_types[1]);

    const size_t vec_sizes_in_bytes[2] = {
        static_cast<size_t>(tensor_infos[0].sizeInBytes()),
        feed2[0][0].size() * sizeof(int)};

    popef::FeedDataInfo feed_infos[2];
    feed_infos[0].setName(feed_names[0]);
    feed_infos[1].setName(feed_names[1]);
    feed_infos[0].setNumTensors(feed1.size());
    feed_infos[1].setNumTensors(feed2.size());
    feed_infos[0].setTensorInfo(tensor_infos[0]);
    feed_infos[1].setTensorInfo(tensor_infos[1]);

    /*** creating and saving feeds ***/
    // Note that the stream passed to the popef::Writer must be
    // opened in binary mode (PopEF expects that).
    std::ofstream stream(file_name, std::fstream::binary);
    popef::Writer writer(stream);
    std::shared_ptr<popef::MultiFeedWriter> multi_feed_writer =
        writer.createMultiFeedData();
    multi_feed_writer->addFeed(feed_infos[0]);
    multi_feed_writer->addFeed(feed_infos[1]);
    for (size_t i = 0; i < feed2[0].size(); i++) {
      multi_feed_writer->write(feed_infos[1].name(),
                               reinterpret_cast<char *>(feed2[0][i].data()),
                               vec_sizes_in_bytes[1]);
    }
    multi_feed_writer->write(feed_infos[0].name(),
                             reinterpret_cast<char *>(feed1[0].data()),
                             vec_sizes_in_bytes[0]);
    multi_feed_writer->write(feed_infos[0].name(),
                             reinterpret_cast<char *>(feed1[1].data()),
                             vec_sizes_in_bytes[0]);
    for (size_t i = 0; i < feed2[1].size(); i++) {
      multi_feed_writer->write(feed_infos[1].name(),
                               reinterpret_cast<char *>(feed2[1][i].data()),
                               vec_sizes_in_bytes[1]);
    }
    multi_feed_writer->write(feed_infos[0].name(),
                             reinterpret_cast<char *>(feed1[2].data()),
                             vec_sizes_in_bytes[0]);
    writer.close();

    /*** reading saved feeds ***/
    popef::Reader reader;
    reader.parseFile(file_name);

    if (reader.feeds().size() != expected_num_of_feeds) {
      std::runtime_error(
          "The popef::Reader object contains the incorrect number of feeds.");
    }

    std::cout << "Feeds number: " << reader.feeds().size() << std::endl;

    /*** checking the content correctness of the saved feeds ***/
    for (size_t i = 0; i < reader.feeds().size(); i++) {
      if (reader.feeds()[i].info != feed_infos[i]) {
        std::stringstream error_msg;
        error_msg << "The feed : \"" << feed_infos[i].name()
                  << "\" contains incorrect metadata." << std::endl;
        std::runtime_error(error_msg.str());
      }
    }

    validateFeedContent(reader.feeds()[0], flatten_feed1);
    validateFeedContent(reader.feeds()[1], flatten_feed2);
  } catch (const std::exception &e) {
    std::cerr << "Exception caught: " << e.what() << std::endl;
    removeFilesIfExists(file_name);
    return EXIT_FAILURE;
  } catch (...) {
    std::cerr << "Exception caught: Something very bad has happened."
              << std::endl;
    removeFilesIfExists(file_name);
    return EXIT_FAILURE;
  }

  removeFilesIfExists(file_name);
  return EXIT_SUCCESS;
}

examples/multifeed_writer.cpp

Listing 3.4 Multi-feed writers (example in Python)
# Copyright (c) 2022 Graphcore Ltd. All rights reserved.

import os
import numpy as np
import popef


def removeFileIfExists(fileName):
    if os.path.exists(fileName):
        os.remove(fileName)


fileName = "feed.popef"

try:
    # preparation of data for saving in feeds #
    feedsNum = 2

    feedNames = ["feed1", "feed2"]
    feedDataTypes = [popef.F32, popef.S32]

    # filling feeds
    feeds = [[
        np.array([0.5, 1.5, 2.5, 3.5], np.float32),
        np.array([0.2, 1.2, 2.2, 3.2], np.float32),
        np.array([0.0, 1.0, 2.0, 3.0], np.float32)
    ],
             [
                 np.array([[0, 1], [2, 3], [4, 5]], np.int32),
                 np.array([[6, 7], [8, 9], [0, 1]], np.int32)
             ]]

    # preparing feeds metadata
    tensorInfos = [popef.TensorInfo(), popef.TensorInfo()]

    tensorInfos[0].setShape(feeds[0][0].shape)
    tensorInfos[0].setDataType(feedDataTypes[0])
    tensorInfos[1].setShape(feeds[1][0].shape)
    tensorInfos[1].setDataType(feedDataTypes[1])

    tensorSizesInBytes = [
        tensorInfos[0].sizeInBytes(), tensorInfos[1].sizeInBytes()
    ]

    feedDataInfos = [popef.FeedDataInfo(), popef.FeedDataInfo()]
    feedDataInfos[0].setName(feedNames[0])
    feedDataInfos[0].setTensorInfo(tensorInfos[0])
    feedDataInfos[0].setNumTensors(len(feeds[0]))

    feedDataInfos[1].setName(feedNames[1])
    feedDataInfos[1].setTensorInfo(tensorInfos[1])
    feedDataInfos[1].setNumTensors(len(feeds[1]))

    # creating and saving feeds #
    writer = popef.Writer(fileName)
    multiFeedWriter = writer.createMultiFeedData()
    multiFeedWriter.addFeed(feedDataInfos[0])
    multiFeedWriter.addFeed(feedDataInfos[1])

    multiFeedWriter.write(feedNames[1], feeds[1][0].tobytes(),
                          tensorSizesInBytes[1])
    multiFeedWriter.write(feedNames[0], feeds[0][0].tobytes(),
                          tensorSizesInBytes[0])
    multiFeedWriter.write(feedNames[0], feeds[0][1].tobytes(),
                          tensorSizesInBytes[0])
    multiFeedWriter.write(feedNames[1], feeds[1][1].tobytes(),
                          tensorSizesInBytes[1])
    multiFeedWriter.write(feedNames[0], feeds[0][2].tobytes(),
                          tensorSizesInBytes[0])
    writer.close()

    # reading feeds #
    reader = popef.Reader()
    reader.parseFile(fileName)

    assert len(reader.feeds()) == feedsNum, """
    The popef.Reader object contains the incorrect number of feeds."""

    # checking the content correctness of the saved feeds #
    for i in range(feedsNum):
        assert reader.feeds()[i].info.name() == feedNames[i], """
        The feed has an incorrect name."""
        assert reader.feeds()[i].info.numTensors() == len(feeds[i]), """
        The feed has incorrect number of tensors."""
        assert reader.feeds()[i].info.tensorInfo().dataType(
        ) == feedDataTypes[i], """
        The feed data type is set to the wrong value."""
        assert reader.feeds()[i].info.tensorInfo().shape() == list(
            feeds[i][0].shape), """
        The shape for the feed has incorrect values."""
        bufferContent = reader.feeds()[i].data()
        for j in range(len(feeds[i])):
            assert np.array_equal(bufferContent[j], feeds[i][j]), """
            The feed contains incorrect data."""
except BaseException as e:
    removeFileIfExists(fileName)
    raise e

removeFileIfExists(fileName)

examples/multifeed_writer.py

3.3. Reading PopEF streams

Reading a PopEF stream is done with the Reader class. Multiple streams can be read by the same Reader instance.

Reader will initially read the metadata from the stream into memory. No data blobs will be loaded into memory. These are stored as pointers into the stream, and can be accessed as std::istream objects from the individual blob readers.

Because only the metadata is read into the blob readers, it’s reasonably memory efficient to copy them. It’s best to do this rather than storing references into the vectors returned by the functions on Reader, because the vector might be resized leading to dangling references. It’s also impossible to read from a data blob istream object using a const reference, as reading from the stream modifies its state.

The following examples show how to write a few blobs to different files and read them from those files using a single Reader instance.

Listing 3.5 Reading PopEF streams (example in C++)
// Copyright (c) 2022 Graphcore Ltd. All rights reserved.

#include <exception>
#include <experimental/filesystem>
#include <iostream>
#include <map>

#include "popef/Reader.hpp"
#include "popef/Writer.hpp"

using BlobsInFiles = std::map<std::string, std::vector<popef::BlobType>>;

void removeFilesIfExist(const BlobsInFiles &blobs_in_files) {
  for (const auto &item : blobs_in_files) {
    const std::string &file_name = item.first;
    if (std::experimental::filesystem::exists(file_name))
      std::remove(file_name.c_str());
  }
}

int main() {
  /*** arrangement blobs in files ***/
  BlobsInFiles blobs_in_files;
  blobs_in_files["file1.popef"] = {popef::BlobType::POPLAR_EXECUTABLE,
                                   popef::BlobType::OPAQUE};
  blobs_in_files["file2.popef"] = {popef::BlobType::OPAQUE,
                                   popef::BlobType::DATA_TENSOR};
  blobs_in_files["file3.popef"] = {popef::BlobType::DATA_TENSOR,
                                   popef::BlobType::POPLAR_EXECUTABLE};

  try {
    /*** preparation of data for saving in individual blobs ***/
    static constexpr unsigned expected_num_of_executables = 2;
    static constexpr unsigned expected_num_of_opaque_blobs = 2;
    static constexpr unsigned expected_num_of_tensor_blobs = 2;

    // executable blob parameters
    static constexpr bool compress = false;
    const std::string executable_name = "executable";
    const std::string executable_content = "executableContent";
    // opaque blob parameters
    const std::string opaque_name = "opaque";
    const std::string opaque_content = "opaqueContent";
    // tensor data blob parameters
    const std::string tensor_name = "tensor";
    const float tensor_data = 0.5;
    const popef::DataType tensor_data_type = popef::DataType::F32;
    popef::TensorInfo tensor_info;
    tensor_info.setShape({}); // scalar values have empty shape
    tensor_info.setDataType(tensor_data_type);
    popef::TensorDataInfo tensor_data_info;
    tensor_data_info.setName(tensor_name);
    tensor_data_info.setTensorInfo(tensor_info);

    /*** saving blobs to the files ***/
    for (const auto &[file_name, blob_types] : blobs_in_files) {
      popef::FileWriter writer(file_name);
      for (const auto &blob_type : blob_types) {
        switch (blob_type) {
        case popef::BlobType::POPLAR_EXECUTABLE: {
          std::shared_ptr<popef::BlobWriter> executable_blob =
              writer.createExecutable(executable_name, compress);
          executable_blob->stream.write(executable_content.c_str(),
                                        executable_content.size());
          break;
        }
        case popef::BlobType::OPAQUE: {
          std::shared_ptr<popef::BlobWriter> opaque_blob =
              writer.createOpaqueBlob(opaque_name, executable_name);
          opaque_blob->stream.write(opaque_content.c_str(),
                                    opaque_content.size());
          break;
        }
        case popef::BlobType::DATA_TENSOR: {
          std::shared_ptr<popef::BlobWriter> tensor_blob =
              writer.createTensorData(tensor_data_info);
          tensor_blob->stream.write(
              reinterpret_cast<const char *>(&tensor_data), sizeof(float));
          break;
        }
        default:
          throw std::runtime_error("An unexpected blob type was encountered");
        }
      }
      writer.close();
    }

    /*** reading all files ***/
    popef::Reader reader;
    for (const auto &item : blobs_in_files) {
      const std::string &file_name = item.first;
      reader.parseFile(file_name);
    }

    if (reader.executables().size() != expected_num_of_executables ||
        reader.opaqueBlobs().size() != expected_num_of_opaque_blobs ||
        reader.tensors().size() != expected_num_of_tensor_blobs) {
      std::runtime_error(
          "The popef::Reader object contains the incorrect number of blobs.");
    }

    /*** checking the content correctness of the saved blobs ***/
    for (const popef::OpaqueReader &opaque : reader.opaqueBlobs()) {
      std::unique_ptr<std::istream> stream = opaque.getStandaloneDataStream();
      std::istreambuf_iterator<char> it(*(stream.get()));
      std::istreambuf_iterator<char> end;
      const std::string opaque_content_read(it, end);

      if (opaque_name != opaque.name || executable_name != opaque.executable ||
          opaque_content != opaque_content_read) {
        std::runtime_error("The opaque blob contains incorrect data.");
      }
    }
    std::cout << "The opaque blobs contain correct data." << std::endl;

    for (const popef::ExecutableReader &executable : reader.executables()) {
      std::unique_ptr<std::istream> stream =
          executable.getStandaloneExecutableStream();
      std::istreambuf_iterator<char> it(*(stream.get()));
      std::istreambuf_iterator<char> end;
      const std::string executable_content_read(it, end);

      if (executable_name != executable.name ||
          executable_content != executable_content_read) {
        throw std::runtime_error(
            "The executable blob contains incorrect data.");
      }
    }
    std::cout << "The executable blobs contain correct data." << std::endl;

    for (const popef::TensorReader &tensor : reader.tensors()) {
      std::unique_ptr<std::istream> stream = tensor.getStandaloneDataStream();
      std::istreambuf_iterator<char> it(*(stream.get()));
      std::istreambuf_iterator<char> end;
      const std::vector<char> data_read(it, end);
      const float tensor_data_read =
          *(reinterpret_cast<const float *>(data_read.data()));

      if (tensor_data_info != tensor.info || tensor_data != tensor_data_read) {
        throw std::runtime_error(
            "The tensor data blob contains incorrect data.");
      }
    }

    std::cout << "The tensor data blobs contain correct data." << std::endl;
  } catch (const std::exception &e) {
    std::cerr << "Exception caught: " << e.what() << std::endl;
    removeFilesIfExist(blobs_in_files);
    return EXIT_FAILURE;
  } catch (...) {
    std::cerr << "Exception caught: Something very bad has happened."
              << std::endl;
    removeFilesIfExist(blobs_in_files);
    return EXIT_FAILURE;
  }

  removeFilesIfExist(blobs_in_files);
  return EXIT_SUCCESS;
}

examples/multifile_reader.cpp

Listing 3.6 Reading PopEF streams (example in Python)
# Copyright (c) 2022 Graphcore Ltd. All rights reserved.

import os
import numpy as np
import typing
import popef


def removeFilesIfExist(blobsInFiles: typing.Dict[str, popef.BlobType]):
    for fileName in blobsInFiles:
        if os.path.exists(fileName):
            os.remove(fileName)


# arrangement blobs in files #
blobsInFiles = {
    "file1.popef": [popef.POPLAR_EXECUTABLE, popef.OPAQUE],
    "file2.popef": [popef.OPAQUE, popef.DATA_TENSOR],
    "file3.popef": [popef.DATA_TENSOR, popef.POPLAR_EXECUTABLE]
}

try:
    # preparation of data for saving in individual blobs #
    executablesNum = 2
    opaqueBlobsNum = 2
    tensorBlobsNum = 2

    # executable blob parameters
    executableName = "executable"
    executableContent = ("executableContent").encode('ascii')
    compress = False
    # opaque blob parameters
    opaqueName = "opaque"
    opaqueContent = ("opaqueContent").encode('ascii')
    # tensor data blob parameters
    tensorName = "tensor"
    floatArray = np.array(0.5, np.float32)
    tensorDataType = popef.F32
    tensorInfo = popef.TensorInfo()
    tensorInfo.setShape(floatArray.shape)
    tensorInfo.setDataType(tensorDataType)
    tensorDataInfo = popef.TensorDataInfo()
    tensorDataInfo.setName(tensorName)
    tensorDataInfo.setTensorInfo(tensorInfo)

    # saving blobs to the files #
    for fileName, blobTypes in blobsInFiles.items():
        writer = writer = popef.Writer(fileName)
        for blobType in blobTypes:
            if blobType == popef.POPLAR_EXECUTABLE:
                executableBlob = writer.createExecutable(
                    executableName, compress)
                executableBlob.write(executableContent, len(executableContent))
            elif blobType == popef.OPAQUE:
                opaqueBlob = writer.createOpaqueBlob(opaqueName,
                                                     executableName)
                opaqueBlob.write(opaqueContent, len(opaqueContent))
            elif blobType == popef.DATA_TENSOR:
                tensorBlob = writer.createTensorData(tensorDataInfo)
                tensorBlob.write(floatArray.tobytes(),
                                 tensorInfo.sizeInBytes())
            else:
                raise AssertionError("An unexpected blob type was encountered")
        writer.close()

    # reading all files #
    reader = popef.Reader()
    for fileName in blobsInFiles:
        reader.parseFile(fileName)

    assert len(reader.executables()) == executablesNum, """
    The popef.Reader object contains the incorrect number of executables."""
    assert len(reader.opaqueBlobs()) == opaqueBlobsNum, """
    The popef.Reader object contains the incorrect number of opaque blobs."""
    assert len(reader.tensors()) == tensorBlobsNum, """
    The popef.Reader object contains the incorrect number of tensors."""

    # checking the content correctness of the saved blobs #
    for opaque in reader.opaqueBlobs():
        assert opaqueName == opaque.name, """
        The opaque blob has an incorrect name."""
        assert executableName == opaque.executable, """
        The executable for the opaque has an incorrect name."""
        assert opaqueContent == opaque.data(), """
        The opaque contains incorrect data."""

    for executable in reader.executables():
        assert executableName == executable.name, """
        The executable has an incorrect name."""
        assert executableContent == executable.executable(), """
        The executable contains incorrect data."""

    for tensor in reader.tensors():
        assert tensorName == tensor.info.name(), """
        The tensor has an incorrect name."""
        assert tensorDataType == tensor.info.tensorInfo().dataType(), """
        The tensor data type is set to the wrong value."""
        assert list(floatArray.shape) == tensor.info.tensorInfo().shape(), """
        The shape for the tensor has incorrect values."""
        assert np.array_equal(floatArray, tensor.data()), """
        The tensor contains incorrect data."""
except BaseException as e:
    removeFilesIfExist(blobsInFiles)
    raise e

removeFilesIfExist(blobsInFiles)

examples/multifile_reader.py