Conversation
The frame path issued one blocking cudaMemcpy per compressed block, which the block-count sweep in docs/BENCHMARKS.md priced at roughly 34 microseconds a block. Assembly now issues those copies on one non-default stream with a single terminal synchronization, and first merges consecutive blocks that are contiguous both in device memory and in the output into one copy. A run of full blocks is contiguous on the device because every data block but the last decodes to exactly the frame's block max, which is also the slot stride. Silesia's 64 KB rung goes 549.923/519.201/545.261 ms to 374.037/375.946/381.386 ms over three interleaved passes, -29.9 %; the 256 KB rung -11.1 % and the 1 MB rung -3.1 %, with no overlap between the two sides at any rung. The two shapes the issue proposed were measured first and both rejected: async copies straight into the caller's pageable buffer are flat, because a device-to-pageable cudaMemcpyAsync is synchronous with respect to the host by specification, and a 32 MiB pinned bounce buffer regresses 25 % at the 256 KB rung by adding a host memcpy of the whole output. All three shapes are recorded with their methodology. The drain runs on every exit path from the loop, including the error returns, so an in-flight copy can never outlive the device buffer it reads. Two fixtures hold the adjacency test honest, each proven by deleting the half it covers and watching only that fixture go red: a stored block between two compressed ones, where the output side disagrees while the device side holds, and a short compressed block followed by a full one, where the device side disagrees while the output side holds. Neither shape was in the suite before - the first is not built by any existing case, and no compressor emits the second - so either half of the test could have shipped missing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #133.
What changed
The frame assembly loop issued one blocking
cudaMemcpyper compressed block.It now issues the copies on one non-default stream with a single terminal
cudaStreamSynchronize, and before issuing anything it merges consecutiveblocks that are contiguous on both sides into one copy.
The merge is what pays, and what lets it is a property of the frame format
rather than of this corpus. Device slots are strided by the frame's block max,
and every data block but the last decodes to exactly that, so a run of full
blocks is one contiguous device range. Compressed blocks land at consecutive
output offsets wherever no uncompressed block separates them. Both sides are
tested per block, so a frame that breaks either just gets more copies and never
wrong bytes.
Which shape, and why not the other two
The issue proposed two shapes and asked for whichever the bench rewards. Both
were built and measured, and both were rejected.
out, one terminal syncThe first is flat for the reason the issue's own design note predicts:
outisthe caller's buffer and is pageable, and a device-to-pageable
cudaMemcpyAsyncis synchronous with respect to the host by specification, sothe stall it was meant to remove is still paid. The second removes the stall for
real and loses anyway, because it adds one host memcpy of the whole output and
past the 64 KB rung there are too few submissions left for the saved latency to
cover it.
So the terminal synchronization is not what paid. That is the first row, and it
is flat. The win is the merge.
Numbers
Three interleaved passes, after / before / after / before / after / before in one
session, both binaries built from the same tree, recorded 2026-08-10 inside the
digest-pinned
nvidia/cuda:12.6.2-devel-ubuntu24.04container(
sha256:738fba0fbdb225b7a2931c58a5c8f03a84d3cd2f6a84975826a157339ef750b8) onthe RTX 3080 (sm_86, driver 560.94, CUDA 12.6, nvcc V12.6.77), 3 warmup + 30
measured runs per rung, output byte-verified against the original once per rung
before timing.
The two sides do not overlap at any rung. The gap widens with the block count,
which is what removing a per-block cost looks like; the 1 MB rung has 203 blocks
and correspondingly little to win.
A first attempt at this A/B was discarded rather than reported. Two of its twelve
rung measurements came out near three times their neighbours, which is host
contention rather than a property of either binary. The table above is a second
session with nothing else running against the device.
Proof that the guards bite
The merge test has two halves, and each has a fixture that goes red when that
half is deleted and stays green when it is present. Neither shape was in the
suite before, and the first measurement of that is the reason both exist: with
the output half deleted, the whole suite stayed green.
tests/frame_twin.cu2d, a stored block between two compressed ones. Onlycompressed blocks occupy device slots, so blocks 0 and 2 are adjacent in device
memory and 64 KiB apart in the output. With the output-side test deleted:
The
41is block 2's text arriving where block 1's stored bytes belong.tests/frame_twin.cu2e, a short compressed block followed by a full one. Nocompressor emits a short block anywhere but last, so this one is spliced from
two frames carrying the same descriptor; the frame format permits it and liblz4
accepts it, which is asserted in the fixture before the placement is checked.
With the device-side test deleted:
Each deletion reddens only its own fixture. Both fixtures assert the frame shape
they claim to build, so they cannot quietly stop covering it if liblz4's
behaviour moves.
Fail-closed and lifetime
The capacity check is untouched and still runs before the first byte is written,
so a too-small
dststill yieldsCUDEC_ERR_OUTPUT_TOO_SMALLwith no partialoutput. Every CUDA failure still maps to
CUDEC_ERR_CUDAthroughFRAME_CUDA,and the terminal synchronization's status is checked rather than dropped.
The drain is an RAII guard, so it runs on every exit path from the loop
including the error returns, and it is declared after the device buffer it
protects so it is destroyed first. An in-flight copy can never outlive the
device memory it reads. Neither offset sum in the merge can overflow: the
device side is bounded by
n * block_max, whose overflow is already refusedabove, and the output side by
total, which the capacity walk computed withoutone.
Gate
Full suite inside the digest-pinned container on the RTX 3080:
frame_twinandframe_host_negativeare both in that set. Prettier reportsdocs/BENCHMARKS.mdclean.The device-side sanitizer net did NOT run. Compute Sanitizer cannot attach to
this device through the WSL2 paravirtualized path, which is #258, and the remedy
the tool names is a machine-wide elevation that is not taken here. Nothing in
this PR should be read as having passed that gate.
This change had no second reader. The evidence above stands in place of one.