Repository navigation
[vulkan] Stage device-to-host copies in host-cached memory - #9675
mcourteaux wants to merge 2 commits into
Conversation
The staging buffer of copy_to_host and of device-to-host buffer_copy preferred device-local host-visible memory: on GPUs with a resizable BAR that is a window into VRAM, which the CPU reads uncached over the bus. Reading it back measured 22 MB/s on an RTX 4070, against 11.7 GB/s from host-cached memory. Prefer HOST_CACHED (and coherent) memory instead; devices without it still fall back to any host-visible memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FRemi1N2Cn82dNxq2aZkF
derek-gerstmann
left a comment
There was a problem hiding this comment.
Thanks for digging into this! The existing pool selection was designed to select the largest available memory pool to avoid out-of-memory issues on constrained devices. However, your commit that adjusts the memory property flags to prefer the fast path for readbacks with an appropriate fallback is a big win overall. Approved with a few changes.
| << "caching=" << halide_memory_caching_name(properties.caching) << "\n"; | ||
| return invalid_memory_type; | ||
| }; | ||
| return required_flags; |
There was a problem hiding this comment.
In src/runtime/vulkan_memory.h:877 — Fallback memory selection can pick non-coherent HOST_VISIBLE memory when HOST_CACHED | HOST_COHERENT is unavailable.
- Mechanism:
VulkanMemoryAllocator::allocate_block(src/runtime/vulkan_memory.h:714-785) runs two passes:- Pass 1 (preferred): requires
required_flags | preferred_flags. For(DeviceToHost, CachedCoherent), this isHOST_VISIBLE_BIT | HOST_CACHED_BIT | HOST_COHERENT_BIT. - Pass 2 (fallback): requires only
required_flags, whichrequired_flags_for_memory_propertiessets toHOST_VISIBLE_BITalone (src/runtime/vulkan_memory.h:851).
- Pass 1 (preferred): requires
- Why it matters: The Vulkan specification (§11.2) guarantees at least one
HOST_VISIBLE | HOST_COHERENTmemory type on every device, but does not guarantee aHOST_VISIBLE | HOST_CACHED | HOST_COHERENTtype. On devices where cached host memory is not coherent (e.g., mobile/ARM SoCs exposing separateHOST_VISIBLE | HOST_CACHEDandHOST_VISIBLE | HOST_COHERENTtypes), Pass 1 finds no match and falls back to Pass 2 (HOST_VISIBLEonly). IfHOST_VISIBLE | HOST_CACHED(non-coherent) appears beforeHOST_VISIBLE | HOST_COHERENT, Pass 2 selects non-coherent memory even though Halide never callsvkInvalidateMappedMemoryRanges(orvkFlushMappedMemoryRanges), risking stale CPU cache reads. - Suggested Fix: In
required_flags_for_memory_properties, requireVK_MEMORY_PROPERTY_HOST_COHERENT_BITwhenever a host-visible allocation requestsCachedCoherentorUncachedCoherent:switch (properties.caching) { case MemoryCaching::CachedCoherent: case MemoryCaching::UncachedCoherent: if (required_flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) { required_flags |= VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; } break; case MemoryCaching::Cached: case MemoryCaching::Uncached: case MemoryCaching::DefaultCaching: break; case MemoryCaching::InvalidCaching: default: error(user_context) << "VulkanMemoryAllocator: Unable to convert type! Invalid memory caching request!\n\t" << "caching=" << halide_memory_caching_name(properties.caching) << "\n"; return invalid_memory_type; };
There was a problem hiding this comment.
Good catch: the runtime never calls vkFlushMappedMemoryRanges / vkInvalidateMappedMemoryRanges, so it relies on coherent memory. Done in a6d16a3 as you suggested: required_flags_for_memory_properties now requires HOST_COHERENT for host-visible memory when the caching is CachedCoherent or UncachedCoherent, so the fallback pass can no longer pick non-coherent memory. The spec guarantees a HOST_VISIBLE | HOST_COHERENT type, so the requirement can always be met.
| // memory (e.g. a resizable BAR window into VRAM) is uncached for the CPU, | ||
| // and reads from it crawl across the bus (~20 MB/s instead of ~10 GB/s). | ||
| preferred_flags |= VK_MEMORY_PROPERTY_HOST_CACHED_BIT; | ||
| break; |
There was a problem hiding this comment.
src/runtime/vulkan_memory.h:877 — preferred_flags_for_memory_properties sets VK_MEMORY_PROPERTY_HOST_CACHED_BIT unconditionally under case MemoryVisibility::DeviceToHost:, overriding explicit Uncached / UncachedCoherent requests.
- Mechanism:
switch (properties.caching)at lines 887–900 already addsVK_MEMORY_PROPERTY_HOST_CACHED_BITforMemoryCaching::CachedCoherentandMemoryCaching::Cached. SettingVK_MEMORY_PROPERTY_HOST_CACHED_BITunconditionally inswitch (properties.visibility)undercase MemoryVisibility::DeviceToHost:is redundant whencachingisCachedCoherent(which bothvulkan.cppcall sites now pass), and causes(DeviceToHost, UncachedCoherent)or(DeviceToHost, Uncached)to still preferHOST_CACHED_BIT. - Suggested Fix: Either only default
DeviceToHosttoHOST_CACHED_BITwhenproperties.caching == MemoryCaching::DefaultCaching, or letDeviceToHostfall through likeHostToDevicesoproperties.cachingalone controls cache flags:case MemoryVisibility::DeviceToHost: if (properties.caching == MemoryCaching::DefaultCaching) { preferred_flags |= VK_MEMORY_PROPERTY_HOST_CACHED_BIT; } break;
There was a problem hiding this comment.
Agreed, done in a6d16a3: DeviceToHost now only prefers HOST_CACHED when the caching is DefaultCaching; the two staging requests pass CachedCoherent, so the caching switch decides by itself. Re-measured on the RTX 4070 with both changes: a 48 MB copy_to_host takes ~8 ms (~6.3 GB/s), against 6.8 s (7 MB/s) on main. The 70 Vulkan gpu/device/async/copy correctness tests pass (61 pass, 9 skip for other backends).
… readbacks only by default Review feedback: - required_flags_for_memory_properties now requires HOST_COHERENT for host-visible memory when the caching is CachedCoherent or UncachedCoherent. The runtime never flushes or invalidates mapped ranges, and the fallback pass only looks at the required flags, so on devices whose cached host memory isn't coherent it could pick non-coherent memory. Every device has a HOST_VISIBLE | HOST_COHERENT type, so the requirement can always be met. - DeviceToHost only prefers HOST_CACHED when the caching is DefaultCaching, so an explicit caching request decides by itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011FRemi1N2Cn82dNxq2aZkF
Copies from device to host on Vulkan were extremely slow on GPUs with host-visible VRAM: about 22 MB/s on an RTX 4070, against several GB/s on CUDA.
Cause
The staging buffer of
copy_to_hostand of device-to-hostbuffer_copyis requested withMemoryVisibility::DeviceToHostandMemoryCaching::UncachedCoherent. ForDeviceToHost,preferred_flags_for_memory_propertiesaddsVK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, and nothing asks forHOST_CACHED. On devices that expose host-visible VRAM (the PCIe BAR window, or all of VRAM with resizable BAR) that preference selects VRAM for the staging buffer. The GPU's copy into it is fast, but the CPU then reads it through an uncached mapping across the bus, one non-posted read after another.Memory types on the RTX 4070 used for testing (resizable BAR off, 256 MiB BAR):
HOST_VISIBLE | HOST_COHERENT | HOST_CACHEDDEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENTThe old request picked type 4. When that heap was full, the fallback took the first host-visible type (uncached system RAM), which is also slow to read.
Fix
For memory the host reads, prefer
HOST_CACHED(and coherent) memory:DeviceToHostnow prefersHOST_CACHEDinstead ofDEVICE_LOCAL;MemoryCaching::CachedCoherent.Devices without host-cached memory still fall back to any host-visible memory, as before. Uploads are unchanged.
Results (RTX 4070, Linux)
copy_to_host: from ~2 s down to ~7.5 ms (6.4 GB/s).Tests
Copy-related correctness tests on Vulkan (
HL_JIT_TARGET=host-vulkan-vk_v13-vk_int8-vk_int16-vk_int64):async_copy_chain,async_device_copy,device_buffer_copies_with_profile,device_buffer_copy,device_copy_at_inner_loop,device_crop,device_slice,extern_stage_on_device,gpu_give_input_buffers_device_allocations,gpu_jit_explicit_copy_to_device,gpu_mixed_dimensionality,gpu_non_contiguous_copy,host_inside_gpu_loop,leak_device_memory: all pass (gpu_async_copyskips, CUDA-only).🤖 Generated with Claude Code
https://claude.ai/code/session_011FRemi1N2Cn82dNxq2aZkF