Skip to content

Parallel Write performance improvements - #695

Open
MarkWolters wants to merge 8 commits into
mainfrom
io_improvements
Open

Parallel Write performance improvements#695
MarkWolters wants to merge 8 commits into
mainfrom
io_improvements

Conversation

@MarkWolters

@MarkWolters MarkWolters commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

This PR updates the graph parallel write process to take full advantage of the async write process. Whereas before although the write tasks were batched and handed off to achieve better performance they were not truly asynchronous. This update changes that to allow fully async graph index writes and increases the benefit of using parallel writes significantly. As an example prior to this update graph write time showed speedups of 4x - 8x using parallel writes

=== Testing Write Performance ===
Writing with NVQ + FUSED_ADC features...
Sequential write: 6074.18 ms
Parallel write:   1373.73 ms
Speedup:          4.42x

After this update the speedup is more along the lines of 24x - 32x

=== Testing Write Performance ===
Writing with NVQ + FUSED_ADC features...
Sequential write: 20147.29 ms
Parallel write:   627.52 ms
Speedup:          32.11x

This PR also introduces the ability to specify parallel writes as part of the test configuration through yaml files which is standard for BenchYAML and AutoBenchYAML test. The new parameter parallelGraphConstruction has been added to ConstructionParameters as a Boolean value. If set to Yes Grid will use parallel writes in graph construction. If set to No Grid will use the legacy serial writing pattern. If the parameter is omitted it will default to the legacy serial writes during graph construction.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Before you submit for review:

  • Does your PR follow guidelines from CONTRIBUTIONS.md?
  • Did you summarize what this PR does clearly and concisely?
  • Did you include performance data for changes which may be performance impacting?
  • Did you include useful docs for any user-facing changes or features?
  • Did you include useful javadocs for developer oriented changes, explaining new concepts or key changes?
  • Did you rebase your branch onto the latest main for regression testing and PR submission?
  • Did you trigger regression testing via Run Bench Main and review results?
  • Did you adhere to the code formatting guidelines (TBD)
  • Did you group your changes for easy review, providing meaningful descriptions for each commit?
  • Did you ensure that all files contain the correct copyright header?
  • Did you add documentation for this feature to the release notes directory?

If you did not complete any of these, then please explain below.

@MarkWolters
MarkWolters marked this pull request as ready for review July 20, 2026 18:55
Comment thread docs/release notes/4.1.0/695.performance.md Outdated

// One channel.write() for the entire task range — one syscall, one OS I/O request.
rangeBuffer.flip();
channel.write(rangeBuffer, baseOffset + (long) startOrdinal * recordSize).get();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is a comment in the main branch that says: This is critical for correctness as AsynchronousFileChannel.write() may not write all bytes in one call.. Doesn't this apply here as well? This .write() need not complete writing all the bytes? The previous code had a loop to account for this:

while (buffer.hasRemaining()) {
            int written = channel.write(buffer, currentPosition).get();
            .... 
        }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are correct that the comments originally stated that the blocking Future.get() calls were necessary this proved to be a misinterpretation of an error condition that occurred during testing and the comments were not correctly updated to reflect this. In the original implementation the asynchronicity of this class was essentially broken by blocking on the Future.get() unnecessarily because of an interpretation that the AsynchronousFileChannel.write() call was not writing all bytes causing data corruption. However the root cause of the issue was later determined to be a bug in the ByteBufferIndexWriter implementation which was resolved by PR 607 (#607). The original comments never got cleaned up, but should be removed with this PR.

@r-devulap r-devulap Aug 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looking up the JAVA doc of AsynchronousByteChannel (which is supposed to work the same way as AsynchronousFileChannel): https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/nio/channels/AsynchronousByteChannel.html:

This method initiates an asynchronous write operation to write a sequence of bytes to this channel from the given buffer. The handler parameter is a completion handler that is invoked when the write operation completes (or fails). The result passed to the completion handler is the number of bytes written.
The write operation may write up to r bytes to the channel, where r is the number of bytes remaining in the buffer, that is, src.remaining() at the time that the write is attempted.

Future.get() returns the number of bytes written which may or may not match src.remaining() because it may write up to r bytes. Don't we need to check that all the bytes were written before proceeding?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The situation where <r bytes are written would be extremely rare. AsynchronousFileChannel issues blocking pwrite() calls under the hood and the only time that should fail to write the full amount would be if the disk fills up partway through the write, if the filesystem quota limits are hit partway through or if the write exceeds the per-syscall write cap which is around 2G. Under normal circumstances that should never occur.

That said "extremely rare" is still a non-zero possibility so you have a point. If one of those circumstances were to occur it would basically be a silent failure which is no good. I'll push an update today to address this.

buf.putInt(0); // neighbor count
for (int n = 0; n < graph.getDegree(0); n++) buf.putInt(-1);
buf.flip();
pending.add(channel.write(buf, recordBase));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same thing here: do we need to account for partial writes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

see previous comment above

`OnDiskGraphIndexWriter`, without any code changes.

**Performance**
Example run writing with NVQ + FUSED_ADC features. Before this change, parallel writes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we include benchmark data in docs, we need to qualify the test scenario with system specs:
CPU/RAM/Storage type and layout

// CopyOnWriteArrayList handles concurrent additions safely
// Thread-local views for safe neighbor iteration.
// CopyOnWriteArrayList handles concurrent additions safely.
this.viewPerThread = ThreadLocal.withInitial(() -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Although it can be a twist, we should start trying to remove usage of ThreadLocal where possible. Of course, this pushes us closer to explicit task structuring, but there are caveats for virtual threads with ThreadLocal which re not ideal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(ah noticed the change was for comments only, sorry if superfluous)

@jshook jshook left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The filesystem used for testing here is not obvious. I would like to see the tests run on a small handful of fileystems at least, and this might require us to improve our GHA runner configs. It would be good to have some stabilizing performance data to compare with later, though.

// CopyOnWriteArrayList handles concurrent additions safely
// Thread-local views for safe neighbor iteration.
// CopyOnWriteArrayList handles concurrent additions safely.
this.viewPerThread = ThreadLocal.withInitial(() -> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(ah noticed the change was for comments only, sorry if superfluous)

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.

3 participants