Skip to content

[QDP] StreamingProducer: use VecDeque for O(1) buffer advance - #1462

Open
0lai0 wants to merge 4 commits into
apache:mainfrom
0lai0:refatcor-trackE3
Open

[QDP] StreamingProducer: use VecDeque for O(1) buffer advance#1462
0lai0 wants to merge 4 commits into
apache:mainfrom
0lai0:refatcor-trackE3

Conversation

@0lai0

@0lai0 0lai0 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Related Issues

Closes #1436

Changes

  • Bug fix
  • New feature
  • Refactoring
  • Documentation
  • Test
  • CI/CD pipeline
  • Other

Why

StreamingProducer tracked consumed elements with a buffer_cursor and, once the cursor passed the halfway mark (BUFFER_COMPACT_DENOM), reclaimed the prefix with Vec::drain(..cursor) — an O(n) memmove of the retained tail, on the streaming hot path.

A VecDeque advances its head instead, so discarding a consumed prefix never shifts the data that is still live: O(1) amortized buffer advance, no periodic compaction pass, and no change in output.

How

  • buffer: Vec<T> + buffer_cursor: usizebuffer: VecDeque<T>; removed the now-unused BUFFER_COMPACT_DENOM compaction heuristic
  • produce() copies the batch out of as_slices() (stitching both halves when a batch straddles the ring's wrap boundary), then drain(..take)s the consumed prefix. Copying via the slices keeps the batch copy on extend_from_slice's bulk path — Drain is not TrustedLen, so extend(drain) would copy element by element. Recycled batch buffers are still reused, so the batch copy itself stays allocation-free
  • Refills use extend(&scratch[..written]) rather than .iter().copied(): VecDeque specializes Extend<&T> for T: Copy into a bulk copy_slice
  • The build path pre-reserves the steady state (batch_size * sample_size + initial_cap, the peak live length) so the ring never reallocates mid-run. VecDeque::from(Vec) reuses the existing allocation, so this only tops the capacity up.

Checklist

  • Added or updated unit tests for all changes
  • Added or updated documentation for all changes

@0lai0
0lai0 requested a review from 400Ping as a code owner August 5, 2026 06:14

@viiccwen viiccwen 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.

Thx for the correctness and capacity tests. Since the main change is hot-path performance, could you also provide a small reproducible before/after benchmark?

It would be helpful to compare the current Vec + cursor implementation against this VecDeque implementation.

@rich7420

rich7420 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

LGTM.

@0lai0

0lai0 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @viiccwen and @rich7420
Added beanch mark make -C qdp bench_streaming_buffer
(before = frozen pre-change producer; after = shipped StreamingProducer)

Buffer-only (refill chunk 65536), Xeon w3-2435, --release:

batch:chunk before after speedup
1:16 782 ns 504 ns 1.55x
1:4 3370 ns 2480 ns 1.36x
1:1 9668 ns 9716 ns 1.00x
4:1 54176 ns 54469 ns 0.99x

E2E via Parquet: ~1.01–1.02x (decode dominates). Win only when batch ≪ chunk; at 1:1+ both do the same work.
Happy to reword the PR around #1436's O(1) advance / constant-capacity criteria rather than throughput

@viiccwen viiccwen 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.

left comments.

Comment thread qdp/Makefile
Comment thread qdp/docs/benchmarks/streaming-buffer-vecdeque.md Outdated

@viiccwen viiccwen 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.

left comments.

// leaves a remainder behind, so the head walks around the ring instead of resetting.
const READ_CHUNK: usize = STRIDE + SAMPLE_LEN;
// Each sample written by write_f32_parquet_n, repeated across the batch.
const SAMPLE: [f32; SAMPLE_LEN] = [0.25, 0.5, 0.75, 1.0];

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.

Every sample in this fixture has the same values, the test cannot detect samples being reordered, duplicated, or skipped across the wrap boundary. Reversing the front and back slices still passes this test.

Comment on lines +1855 to +1864
let mut producer = StreamingProducer::<f32> {
reader,
buffer: VecDeque::with_capacity(STRIDE + READ_CHUNK),
read_chunk_scratch: vec![0.0_f32; READ_CHUNK],
sample_size: SAMPLE_LEN,
batch_size: BATCH_SIZE,
num_qubits: 2,
batches_yielded: 0,
batch_limit: usize::MAX,
};

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.

IMO, This test constructs the deque with the expected steady-state capacity directly, so it does not verify the reserve logic in build_streaming_producer, right? The production reservation could be removed or broken while this test still passes.

Could the test create the producer through the prod initialization path and then verify that capacity remains stable for 100 batches?

// this only tops it up by `required` — making front-advance realloc-free from batch 0.
let required = config.batch_size * sample_size;
let mut buffer = VecDeque::from(buffer);
buffer.reserve((required + initial_cap).saturating_sub(buffer.len()));

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.

This eagerly reserves space for a full batch plus one refill chunk, even when the file only contains a partial batch. For large samples, this can substantially increase memory usage; for example, 20-qubit amplitude data with batch_size=64 and f64 grows from roughly 8 MiB to 520 MiB.

Could we avoid reserving the theoretical peak before EOF is known, and use checked arithmetic for the capacity calculation?

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.

[Refactor] StreamingProducer: use VecDeque for O(1) buffer advance

3 participants