feat(runtime): pinned-host-buffer fast path for create_from_slice uploads#1334
feat(runtime): pinned-host-buffer fast path for create_from_slice uploads#1334lilith wants to merge 1 commit into
Conversation
… uploads CUDA's cuMemcpyHtoDAsync from pageable host memory caps at ~5-6 GB/s because the driver internally stages through a hidden pinned bounce buffer. Allocating the host buffer with cuMemAllocHost_v2 lets the driver DMA directly, getting ~12-25 GB/s on PCIe 4.0. PR tracel-ai#1030 introduced the underlying `ComputeServer::staging` + `Bytes` swap machinery for the explicit `create` / `create_tensor` paths; this change extends the same fast path to the slice-input variants and adds explicit reserve-then-fill helpers for callers that want to skip the intermediate pageable `Vec<u8>`. What this adds: - `ComputeClient::reserve_staging(&[usize]) -> Vec<Bytes>` reserves pinned host buffers of the requested sizes. Falls back to plain heap allocations if the backend returns an error (e.g. pinned memory exhausted), so callers always receive buffers of the requested size. - `ComputeClient::create_from_slice_pinned(&[u8]) -> Handle` copies the input directly into a pinned staging buffer before uploading. Saves the caller `&[u8]` -> pageable `Vec<u8>` -> pinned `Bytes` host memcpy that `create_from_slice` performs today, halving host-side memory traffic for large uploads. - `ComputeClient::create_tensors_from_slices_pinned` is the batch variant of the above. - An `upload_bench` example under `examples/` measuring pageable vs pinned upload bandwidth across a few buffer sizes. What this changes implicitly (no API change): - `do_create_from_slices` (the implementation behind `create_from_slice` / `create_tensor_from_slice` / `create_tensors_from_slices`) now wraps slices in `Bytes` and calls `self.staging(..., true)` before submitting, so the default upload path also benefits from pinned DMA on CUDA without callers having to opt in. The slice-input variants still do one host memcpy (caller -> pinned), where the new `*_pinned` helpers do zero intermediate copies. - `do_create` no longer rewraps the already-staged `Bytes` via `Bytes::from_bytes_vec(data.to_vec())` before handing them to the server. That rewrap allocated a fresh pageable `Vec<u8>`, demoting the buffer back to `AllocationProperty::Native` and re-triggering the slow CUDA pageable bounce on the subsequent HtoD copy. Forwarding the `Bytes` as-is preserves the pinned property end-to-end. Backward compatibility: - No public signatures change. All new symbols (`reserve_staging`, `create_from_slice_pinned`, `create_tensors_from_slices_pinned`) are additions. - Backends without a pinned-memory concept ignore `staging` and behave exactly as today. - The new helpers fall back to plain `vec![0u8; n]` allocations when the backend returns an error from `staging`, so callers do not need to branch on backend support. Relationship to merged PR tracel-ai#1030 ("Feat/pinned mem"): PR tracel-ai#1030 added `ComputeServer::staging` and the `Bytes` allocation- property tracking that makes this possible. This change is purely a client-side extension that pulls the slice-input `create_*` paths through that same machinery and exposes two convenience helpers that let callers skip the intermediate pageable host buffer entirely.
…ad-code cleanup ## Cubecl patch — v0.10.0 stable Repoint the [patch.crates-io] block from lilith/cubecl commit `08d34ac0` (rebased on 0.10.0-pre.4) to `de2f9857` (rebased on the upstream **v0.10.0 release tag**, branch `pr/pinned-v0.10.0`). Functionally identical to the prior patch — same convenience wrappers (`create_from_slice_pinned`, `reserve_staging`) and same default- `create_from_slice` routing through pinned staging — just on the official released base instead of the pre-release. The actual upstream PR for the convenience wrappers is tracel-ai/cubecl#1334 (draft, against post-v0.10.0 main which is now 0.11.0-pre.x post-mega-refactor). PR #1030 had landed the base `staging()` API for v0.10.0 already. Drop these patches once the wrappers land upstream and we migrate to the cubecl release after v0.10.0. ## ssim2 dead-code cleanup Remove the unused `blur_pass_t_kernel` (~95 lines) in `crates/ssim2-gpu/src/kernels/blur.rs`. This was a fused column-walk + transpose experiment shipped under commit `990e120` (T_x.B v2) that turned out to regress: uncoalesced strided writes costed more bandwidth than the saved transpose. The actual win shipped under that commit was the LDS-tiled 32×32 transpose in `transpose.rs`; the fused kernel was left in source as dead code that LLVM dropped. Removing it now to keep the surface clean. ## Verification - All 320 GPU-crate tests pass with the v0.10.0 patch (lib+tests, doctest cubecl::wgpu failures excepted as pre-existing). - cvvdp-gpu 12 MP warm-ref bench: ~16.7 ms steady-state median (well within the ~22 ms historical band — GPU thermal state matters at this resolution). JOD 5.5640 bit-identical to prior runs.
nathanielsimard
left a comment
There was a problem hiding this comment.
Btw create from slices is mostly used for testing, the new reserve staging doesn't seem to be implemented at the right level. submit_blocking is expensive and shouldn't be used for data transfer, only for data fetching.
|
Independent +1 on the core change here. I hit the same thing from the other direction (a host-bound upload path) and arrived at the identical fix: in Worth emphasizing that the It's still on |
Summary
Extend the staging-buffer machinery added in #1030 (Feat/pinned mem) so the
slice-input
create_*paths also get the pinned-DMA fast path on CUDA, andadd two convenience helpers that let callers skip the intermediate pageable
host buffer entirely.
Motivation
On CUDA,
cuMemcpyHtoDAsyncfrom a pageable host buffer caps at ~5-6 GB/sbecause the driver internally stages through a hidden pinned bounce buffer.
Allocating the host buffer with
cuMemAllocHost_v2(which is whatComputeServer::stagingdoes) lets the driver DMA directly, getting~12-25 GB/s on PCIe 4.0 — i.e. a 2-4x bandwidth jump for free, just by
swapping the host-side allocation.
#1030 introduced the
ComputeServer::stagingAPI and theAllocationPropertytagging on
Bytes, and routedcreate/create_tensorthrough it. Thatleft the slice-input variants (
create_from_slice/create_tensor_from_slice/
create_tensors_from_slices) still building pageableVec<u8>buffers andnever calling
staging, so their HtoD copies kept hitting the slow path.Downstream image/codec pipelines that upload sRGB/u8 buffers typically reach
for the slice variants because the input is already a
&[u8]— losing thepinned fast path silently.
A microbench upload of a 48 MB buffer to a desktop CUDA device went from
~9 ms (pageable) to ~3 ms (pinned) on my workstation. The full
upload_benchexample is included.What this adds
ComputeClient::reserve_staging(&[usize]) -> Vec<Bytes>reserves pinnedhost buffers of the requested sizes. Falls back to plain heap allocations
if the backend's
stagingcall returns an error (e.g. pinned memoryexhausted), so callers always get buffers of the requested size.
ComputeClient::create_from_slice_pinned(&[u8]) -> Handlecopies theinput directly into a pinned staging buffer before uploading. Saves the
caller
&[u8]→ pageableVec<u8>→ pinnedByteshost memcpy thatcreate_from_sliceperforms today, halving host-side memory traffic forlarge uploads.
ComputeClient::create_tensors_from_slices_pinnedis the batch variantof the above.
examples/upload_benchcrate measuring pageable vs pinned uploadbandwidth across a few buffer sizes (4 MB / 48 MB / 192 MB).
What this changes implicitly (no public API change)
do_create_from_slices(the implementation behindcreate_from_slice/create_tensor_from_slice/create_tensors_from_slices) now wrapsslices in
Bytesand callsself.staging(..., true)before submitting,so the default upload path benefits from pinned DMA on CUDA without
callers having to opt in. The slice-input variants still do one host
memcpy (caller → pinned); the new
*_pinnedhelpers do zero intermediatecopies.
do_createno longer rewraps the already-stagedBytesviaBytes::from_bytes_vec(data.to_vec())before handing them to the server.That rewrap allocated a fresh pageable
Vec<u8>, demoting the bufferback to
AllocationProperty::Nativeand re-triggering the slow CUDApageable bounce on the subsequent HtoD copy. Forwarding the
Bytesas-is preserves the pinned property end-to-end.
Backward compatibility
staging) behave exactly as today.*_pinnedhelpers fall back to plainvec![0u8; n]allocationson
stagingerror, so callers do not need to branch on backend support.Test plan
do_create_from_sliceschange — wrapping eachinput slice in a
Bytesadds one allocation per upload on backendsthat decline to stage, but that allocation is the same one
do_create_from_sliceswas already doing on its existing slow path(via
Bytes::from_bytes_vec(data.to_vec())). Net per-upload alloccount is unchanged.
cargo test -p cubecl-runtime --libpasses locally (67/67).cargo clippy -p cubecl-runtime -p upload_bench --features cuda --no-depsclean.cargo fmt --checkclean.examples/upload_bench/layoutmatches existing example conventions (followed
examples/gelu/).