Skip to content

How to access RNTuple in a BulkIO style#7112

Closed
jpivarski wants to merge 2 commits intoroot-project:v6-22-00-patchesfrom
jpivarski:jpivarski/vCHEP-2021-studies
Closed

How to access RNTuple in a BulkIO style#7112
jpivarski wants to merge 2 commits intoroot-project:v6-22-00-patchesfrom
jpivarski:jpivarski/vCHEP-2021-studies

Conversation

@jpivarski
Copy link
Copy Markdown
Contributor

This PR is not intended to be merged into ROOT! That's why it's a draft!

The purpose of this PR is to show which private members I had to make public to access RNTuple in a BulkIO style.

Two of these changes were just to parameterize the cluster and page sizes:

  • fClusterSizeEntries was made public so that I could set it and make it apples-to-apples with the other formats.
  • kDefaultElementsPerPage = 2097152 is large, but 8× less than the maximum size that can be compressed. The maximum is 0xffffff because the header provides 3 bytes to specify the uncompressed size, so that uncompressed size can't exceed that. The number I chose here is 2**21, which is 8× below that limit, to allow for 8-byte integers and floating point numbers. What's probably missing here is the logic for splitting the data to be compressed into a series of blocks with this maximum size. (TTree and normal serialized objects do that.)

The rest of the changes are just turning private/protected members into public ones so that they can be read directly in a BulkIO style. Here's how that's done: suppose you're filling a buffer named array using a view of type V returned by GetViewCollection or GetView<T>. We know the length of elements to read, so the function is

template <typename V, typename T>
void read_from_rntuple_view(T* buffer, V& view, int64_t& offset, int64_t length) {
  int64_t current = 0;
  while (current < length) {
    T* data = (T*)view.fField.Map(offset + current);
    int32_t num = view.fField.fPrincipalColumn->fCurrentPage.GetNElements();
    int32_t skipped = (offset + current) - view.fField.fPrincipalColumn->fCurrentPage.GetGlobalRangeFirst();
    int32_t remaining = num - skipped;
    if (current + remaining > length) {
      remaining = length - current;
    }
    if (remaining > 0) {
      std::memcpy(&buffer[current], data, remaining*sizeof(T));
    }
    current += remaining;
  }
  offset += current;
}

Here's a sample usage:

auto ntuple = RNTupleReader::Open(std::move(model), "rntuple", filename);
auto view3 = ntuple->GetViewCollection("field");
auto view2 = view3.GetViewCollection("std::vector<std::vector<float>>");
auto view1 = view2.GetViewCollection("std::vector<float>");
auto view0 = view1.GetView<float>("float");

int64_t offset3 = 0;
int64_t offset2 = 0;
int64_t offset1 = 0;
int64_t offset0 = 0;

for (int64_t entry = 0;  entry < num_entries_in_file;  entry += num_entries_in_cluster) {
  int64_t length = num_entries_in_cluster;
  if (entry + length > num_entries_in_file) {
    length = num_entries_in_file - entry;   // trim for last cluster
  }

  int32_t* buffer_offset3 = allocate_output_offsets<int32_t>(length + 1);
  buffer_offsets3[0] = 0; // Awkward Array and Arrow offsets are a "fencepost" around the items they contain.
                          // For RNTuple input, which has no "dead space" before the first entry,
                          // they always start at zero. As a "fencepost," buffer_offsets.size() == length + 1.
  read_from_rntuple_view(&buffer_offsets3[1], view3, offset3, length);

  length = buffer_offsets3[length];   // now "length" is the length of the second level
  int32_t* buffer_offsets2 = allocate_output_offsets<int32_t>(length + 1);
  buffer_offsets2[0] = 0;
  read_from_rntuple_view(&buffer_offsets2[1], view2, offset2, length);

  length = buffer_offsets2[length];
  int32_t* buffer_offsets1 = allocate_output_offsets<int32_t>(length + 1);
  buffer_offsets1[0] = 0;
  read_from_rntuple_view(&buffer_offsets1[1], view1, offset1, length);

  length = buffer_offsets1[length];
  float* buffer_content = allocate_output_content<float>(length);   // content is different from offsets
  read_from_rntuple_view(buffer_content, view0, offset0, length);

  return somehow_pack_together(buffer_offset3, buffer_offset2, buffer_offset1, buffer_content);
}

@phsft-bot
Copy link
Copy Markdown

Can one of the admins verify this patch?

@jpivarski
Copy link
Copy Markdown
Contributor Author

@jblomer @pcanal

@jblomer
Copy link
Copy Markdown
Contributor

jblomer commented Apr 23, 2021

@jpivarski Many thanks for sharing, that's very helpful! The cluster and page sizes should be addressed in #7853
For bulk access with views, I was considering an interface like this

const T *RNTupleView<T>::MapV(IndexType index, unsigned int &nItems);

The nItems parameter would return the number of elements one can use in the T* output buffer. So T* would point into the page buffer and return the number of elements until the end of the page.

Copy link
Copy Markdown
Contributor

@jblomer jblomer left a comment

Choose a reason for hiding this comment

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

(should have left the regular comment as a review comment)

@jpivarski
Copy link
Copy Markdown
Contributor Author

That would be a good interface. There would always be a practical limit on the number of items that one could view, but suppose we just don't want to specify it (and risk running out of memory in extreme cases). Can nItems = -1 or nItems < 0 mean "send me everything"? The function would still change nItems in place to tell the caller how many items are actually in the const T* that it returns.

Also, who owns the const T* memory? Would deleting the RNTupleView free that const T*?

Might the const T* a concatenation of multiple pages, or is this still granular at the page level? If it can be a concatenation of multiple pages, then it would be a different buffer from the one the RNTupleView manages internally. If it could be either a view or a copy, then the RNTupleView must own it because this object would have the most information to keep track of whether its internal buffer-freeing frees the const T*.

@jpivarski
Copy link
Copy Markdown
Contributor Author

@oshadura should be on this thread, too.

@jblomer
Copy link
Copy Markdown
Contributor

jblomer commented Apr 24, 2021

I think we would not need nItems as an input parameter. The returned pointer should always be treated as owned by ROOT. If the view operates on a mappable type, it would point into the backing page. Otherwise, the view has a helper object (field) to read the value, in which the pointer would point. In that case, nItems would always be 1. The pointer remains valid as long as no other function on the view is called.

@guitargeek
Copy link
Copy Markdown
Contributor

guitargeek commented Nov 5, 2023

It seems like this discussion has concluded. Feel free to open a new PR to ROOT master or open a GitHub issue to continue the discussion if necessary (or let us know if this PR really needs to be re-opened).

@guitargeek guitargeek closed this Nov 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants