From b122fba5452b6cf4d0c2d4ec0f6d694e13f4e4be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Ingve=20Schj=C3=B8lberg?= Date: Fri, 17 Jul 2026 14:34:39 +0200 Subject: [PATCH 1/5] [vulkan] Make allocator conform method idempotent conform() used request->alignment to decide whether to raise the region's nearest_multiple, but it also writes the conformed alignment back into the request. Therefore, conforming an already-conformed request can produce a larger size because the second pass sees a larger request->alignment and decides to raise the nearest_multiple. The conform method of the vulkan memory allocator is called by the runtime block allocator on multiple levels, using requests that were already conformed, so different layers see mismatching sizes. The mismatch causes issues observable on drivers reporting 64-byte buffer alignment (Mesa lavapipe/RADV): a 96-byte buffer gets a 64-byte region, and the transfer overruns it. The Khronos validation layer flags it as VUID-vkCmdCopyBuffer-size-00115/00116 ("size (96) is greater than the buffer size (64)"). These errors are present before this commit and gone after it. This commit changes conform() to use the conformed alignment instead of request->alignment, to decide whether to raise nearest_multiple. Co-authored-by: Claude Opus 4.8 --- src/runtime/vulkan_memory.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/runtime/vulkan_memory.h b/src/runtime/vulkan_memory.h index b21a2e476d86..5a4862441101 100644 --- a/src/runtime/vulkan_memory.h +++ b/src/runtime/vulkan_memory.h @@ -1006,12 +1006,18 @@ int VulkanMemoryAllocator::conform(void *user_context, MemoryRequest *request) { } } - // Ensure the request ends on an aligned address - if (request->alignment > config.nearest_multiple) { - request->properties.nearest_multiple = request->alignment; + size_t actual_alignment = conform_alignment(request->alignment, memory_requirements.alignment); + + // Ensure the request ends on an aligned address. + // NOTE: use the conformed alignment and not the input alignment. Otherwise, conform is not idempotent: + // A first pass that uses the original nearest_multiple writes a new alignment such that a subsequent pass + // sees a larger alignment and writes a larger nearest_multiple, which results in a larger size calculated + // in the second pass. The block allocator requires that conform is idempotent because it calls conform + // defensively several times at different layers. + if (actual_alignment > config.nearest_multiple) { + request->properties.nearest_multiple = actual_alignment; } - size_t actual_alignment = conform_alignment(request->alignment, memory_requirements.alignment); size_t actual_offset = aligned_offset(request->offset, actual_alignment); size_t actual_size = conform_size(actual_offset, memory_requirements.size, actual_alignment, request->properties.nearest_multiple); From b5be63595ff5a5500b72b1359b0c5846647a512a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Ingve=20Schj=C3=B8lberg?= Date: Mon, 20 Jul 2026 09:49:15 +0200 Subject: [PATCH 2/5] [vulkan] Ensure block requests are sized big enough to fit their regions Previously it was possible that a region's conformed size exceeded the conformed size of its block. This could happen because a block's size was conformed by conform_block_request (rounding to the config nearest_multiple, then overwriting with VkMemoryRequirements.size), while regions carved from the block were conformed by a separate path that also rounds up to the device buffer alignment. The two paths disagree: for a buffer whose size is a multiple of the config nearest_multiple but not of the device alignment, the block was sized smaller than the region request it had to hold. On drivers reporting 64-byte buffer alignment (Mesa lavapipe/RADV), a 9338976-byte buffer (a multiple of 32 but not 64) produced a 9338976-byte block, while the region inside it conformed to 9339008. RegionAllocator::reserve then failed its headroom check, collect() retried, a second block failed identically, and allocation failed with "Failed to allocate device memory!". This commit fixes the issue by factoring the request-conforming arithmetic into a shared helper and calling it from both the block and region conform paths, so a block is always sized to hold a conformed region request of the same size. Fixes #8868 Co-authored-by: Claude Opus 4.8 --- src/runtime/vulkan_memory.h | 70 ++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/src/runtime/vulkan_memory.h b/src/runtime/vulkan_memory.h index 5a4862441101..b9f84baf6b73 100644 --- a/src/runtime/vulkan_memory.h +++ b/src/runtime/vulkan_memory.h @@ -136,6 +136,8 @@ class VulkanMemoryAllocator { int lookup_requirements(void *user_context, size_t size, uint32_t usage_flags, VkMemoryRequirements *memory_requirements); + int conform_request(MemoryRequest *request, VkMemoryRequirements *memory_requirements); + size_t block_byte_count = 0; size_t block_count = 0; size_t region_byte_count = 0; @@ -576,6 +578,39 @@ int VulkanMemoryAllocator::lookup_requirements(void *user_context, size_t size, vkDestroyBuffer(this->device, buffer, this->alloc_callbacks); return halide_error_code_success; } +int VulkanMemoryAllocator::conform_request(MemoryRequest *request, VkMemoryRequirements *memory_requirements) { + size_t actual_alignment = conform_alignment(request->alignment, memory_requirements->alignment); + + // Ensure the request ends on an aligned address. + // NOTE: use the conformed alignment and not the input alignment. Otherwise, conform is not idempotent: + // A first pass that uses the original nearest_multiple writes a new alignment such that a subsequent pass + // sees a larger alignment and writes a larger nearest_multiple, which results in a larger size calculated + // in the second pass. The block allocator requires that conform is idempotent because it calls conform + // defensively several times at different layers. + if (actual_alignment > config.nearest_multiple) { + request->properties.nearest_multiple = actual_alignment; + } + + size_t actual_offset = aligned_offset(request->offset, actual_alignment); + size_t actual_size = conform_size(actual_offset, memory_requirements->size, actual_alignment, request->properties.nearest_multiple); + +#if defined(HL_VK_DEBUG_MEM) + if ((request->size != actual_size) || (request->alignment != actual_alignment) || (request->offset != actual_offset)) { + debug(nullptr) << "VulkanMemoryAllocator: Adjusting request to match requirements (\n" + << " size = " << (uint64_t)request->size << " => " << (uint64_t)actual_size << ",\n" + << " alignment = " << (uint64_t)request->alignment << " => " << (uint64_t)actual_alignment << ",\n" + << " offset = " << (uint64_t)request->offset << " => " << (uint64_t)actual_offset << ",\n" + << " required.size = " << (uint64_t)memory_requirements->size << ",\n" + << " required.alignment = " << (uint64_t)memory_requirements->alignment << "\n)\n"; + } +#endif + request->size = actual_size; + request->alignment = actual_alignment; + request->offset = actual_offset; + + return halide_error_code_success; +} + int VulkanMemoryAllocator::conform_block_request(void *instance_ptr, MemoryRequest *request) { @@ -614,9 +649,9 @@ int VulkanMemoryAllocator::conform_block_request(void *instance_ptr, MemoryReque << "dedicated=" << (request->dedicated ? "true" : "false") << ")\n"; #endif - request->size = memory_requirements.size; request->properties.alignment = memory_requirements.alignment; - return halide_error_code_success; + + return instance->conform_request(request, &memory_requirements); } int VulkanMemoryAllocator::allocate_block(void *instance_ptr, MemoryBlock *block) { @@ -1006,36 +1041,7 @@ int VulkanMemoryAllocator::conform(void *user_context, MemoryRequest *request) { } } - size_t actual_alignment = conform_alignment(request->alignment, memory_requirements.alignment); - - // Ensure the request ends on an aligned address. - // NOTE: use the conformed alignment and not the input alignment. Otherwise, conform is not idempotent: - // A first pass that uses the original nearest_multiple writes a new alignment such that a subsequent pass - // sees a larger alignment and writes a larger nearest_multiple, which results in a larger size calculated - // in the second pass. The block allocator requires that conform is idempotent because it calls conform - // defensively several times at different layers. - if (actual_alignment > config.nearest_multiple) { - request->properties.nearest_multiple = actual_alignment; - } - - size_t actual_offset = aligned_offset(request->offset, actual_alignment); - size_t actual_size = conform_size(actual_offset, memory_requirements.size, actual_alignment, request->properties.nearest_multiple); - -#if defined(HL_VK_DEBUG_MEM) - if ((request->size != actual_size) || (request->alignment != actual_alignment) || (request->offset != actual_offset)) { - debug(nullptr) << "VulkanMemoryAllocator: Adjusting request to match requirements (\n" - << " size = " << (uint64_t)request->size << " => " << (uint64_t)actual_size << ",\n" - << " alignment = " << (uint64_t)request->alignment << " => " << (uint64_t)actual_alignment << ",\n" - << " offset = " << (uint64_t)request->offset << " => " << (uint64_t)actual_offset << ",\n" - << " required.size = " << (uint64_t)memory_requirements.size << ",\n" - << " required.alignment = " << (uint64_t)memory_requirements.alignment << "\n)\n"; - } -#endif - request->size = actual_size; - request->alignment = actual_alignment; - request->offset = actual_offset; - - return halide_error_code_success; + return conform_request(request, &memory_requirements); } int VulkanMemoryAllocator::conform_region_request(void *instance_ptr, MemoryRequest *request) { From 36cdf2ae2e96d9bad8b33817704fcf8974655424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Ingve=20Schj=C3=B8lberg?= Date: Tue, 21 Jul 2026 11:18:26 +0200 Subject: [PATCH 3/5] [vulkan] Move conform implementation to memory_resources.h and add tests This commit moves the conform implementation from the vulkan_memory.h header into the memory_resources.h header so that it can be tested by the runtime/internal block+region allocator test. A test case is added to check that conform is idempotent over a number of different inputs, as well as testing the specific values from drivers reporting 64-bit alignment that revealed the previous version of conform to not be idempotent. Co-authored-by: Claude Opus 4.8 --- src/runtime/internal/memory_resources.h | 24 +++++++++ src/runtime/vulkan_memory.h | 70 ++++++++++++------------- test/runtime/block_allocator.cpp | 64 ++++++++++++++++++++++ 3 files changed, 121 insertions(+), 37 deletions(-) diff --git a/src/runtime/internal/memory_resources.h b/src/runtime/internal/memory_resources.h index ea5f5420263b..339e167c852b 100644 --- a/src/runtime/internal/memory_resources.h +++ b/src/runtime/internal/memory_resources.h @@ -161,6 +161,30 @@ ALWAYS_INLINE size_t conform_size(size_t offset, size_t size, size_t alignment, } } +// Updates request with conformed alignment, offset and size, computed from the given required size and alignment constraints, +// rounding up to the nearest multiple if specified. The request is updated in-place. +// -- Required alignment must be power of two! +ALWAYS_INLINE void conform_memory_request(MemoryRequest *request, size_t required_size, size_t required_alignment, size_t nearest_multiple) { + size_t actual_alignment = conform_alignment(request->alignment, required_alignment); + + // Ensure the request ends on an aligned address. + // NOTE: use the conformed alignment and not the input alignment. Otherwise, conform is not idempotent: + // A first pass that uses the original nearest_multiple writes a new alignment such that a subsequent pass + // sees a larger alignment and writes a larger nearest_multiple, which results in a larger size calculated + // in the second pass. The block allocator requires that conform is idempotent because it calls conform + // defensively several times at different layers. + if (actual_alignment > nearest_multiple) { + request->properties.nearest_multiple = actual_alignment; + } + + size_t actual_offset = aligned_offset(request->offset, actual_alignment); + size_t actual_size = conform_size(actual_offset, required_size, actual_alignment, request->properties.nearest_multiple); + + request->size = actual_size; + request->alignment = actual_alignment; + request->offset = actual_offset; +} + // Clamps the given value to be within the [min_value, max_value] range ALWAYS_INLINE size_t clamped_size(size_t value, size_t min_value, size_t max_value) { size_t result = (value < min_value) ? min_value : value; diff --git a/src/runtime/vulkan_memory.h b/src/runtime/vulkan_memory.h index b9f84baf6b73..b2f7f7b38602 100644 --- a/src/runtime/vulkan_memory.h +++ b/src/runtime/vulkan_memory.h @@ -136,8 +136,6 @@ class VulkanMemoryAllocator { int lookup_requirements(void *user_context, size_t size, uint32_t usage_flags, VkMemoryRequirements *memory_requirements); - int conform_request(MemoryRequest *request, VkMemoryRequirements *memory_requirements); - size_t block_byte_count = 0; size_t block_count = 0; size_t region_byte_count = 0; @@ -578,39 +576,6 @@ int VulkanMemoryAllocator::lookup_requirements(void *user_context, size_t size, vkDestroyBuffer(this->device, buffer, this->alloc_callbacks); return halide_error_code_success; } -int VulkanMemoryAllocator::conform_request(MemoryRequest *request, VkMemoryRequirements *memory_requirements) { - size_t actual_alignment = conform_alignment(request->alignment, memory_requirements->alignment); - - // Ensure the request ends on an aligned address. - // NOTE: use the conformed alignment and not the input alignment. Otherwise, conform is not idempotent: - // A first pass that uses the original nearest_multiple writes a new alignment such that a subsequent pass - // sees a larger alignment and writes a larger nearest_multiple, which results in a larger size calculated - // in the second pass. The block allocator requires that conform is idempotent because it calls conform - // defensively several times at different layers. - if (actual_alignment > config.nearest_multiple) { - request->properties.nearest_multiple = actual_alignment; - } - - size_t actual_offset = aligned_offset(request->offset, actual_alignment); - size_t actual_size = conform_size(actual_offset, memory_requirements->size, actual_alignment, request->properties.nearest_multiple); - -#if defined(HL_VK_DEBUG_MEM) - if ((request->size != actual_size) || (request->alignment != actual_alignment) || (request->offset != actual_offset)) { - debug(nullptr) << "VulkanMemoryAllocator: Adjusting request to match requirements (\n" - << " size = " << (uint64_t)request->size << " => " << (uint64_t)actual_size << ",\n" - << " alignment = " << (uint64_t)request->alignment << " => " << (uint64_t)actual_alignment << ",\n" - << " offset = " << (uint64_t)request->offset << " => " << (uint64_t)actual_offset << ",\n" - << " required.size = " << (uint64_t)memory_requirements->size << ",\n" - << " required.alignment = " << (uint64_t)memory_requirements->alignment << "\n)\n"; - } -#endif - request->size = actual_size; - request->alignment = actual_alignment; - request->offset = actual_offset; - - return halide_error_code_success; -} - int VulkanMemoryAllocator::conform_block_request(void *instance_ptr, MemoryRequest *request) { @@ -647,11 +612,25 @@ int VulkanMemoryAllocator::conform_block_request(void *instance_ptr, MemoryReque << "uniform_buffer_offset_alignment=" << (uint32_t)instance->physical_device_limits.minUniformBufferOffsetAlignment << ", " << "storage_buffer_offset_alignment=" << (uint32_t)instance->physical_device_limits.minStorageBufferOffsetAlignment << ", " << "dedicated=" << (request->dedicated ? "true" : "false") << ")\n"; + + const MemoryRequest original_request = *request; #endif request->properties.alignment = memory_requirements.alignment; + conform_memory_request(request, memory_requirements.size, memory_requirements.alignment, instance->config.nearest_multiple); + +#if defined(HL_VK_DEBUG_MEM) + if ((request->size != original_request.size) || (request->alignment != original_request.alignment) || (request->offset != original_request.offset)) { + debug(nullptr) << "VulkanMemoryAllocator: Adjusting request to match requirements (\n" + << " size = " << (uint64_t)original_request.size << " => " << (uint64_t)request->size << ",\n" + << " alignment = " << (uint64_t)original_request.alignment << " => " << (uint64_t)request->alignment << ",\n" + << " offset = " << (uint64_t)original_request.offset << " => " << (uint64_t)request->offset << ",\n" + << " required.size = " << (uint64_t)memory_requirements.size << ",\n" + << " required.alignment = " << (uint64_t)memory_requirements.alignment << "\n)\n"; + } +#endif - return instance->conform_request(request, &memory_requirements); + return halide_error_code_success; } int VulkanMemoryAllocator::allocate_block(void *instance_ptr, MemoryBlock *block) { @@ -1041,7 +1020,24 @@ int VulkanMemoryAllocator::conform(void *user_context, MemoryRequest *request) { } } - return conform_request(request, &memory_requirements); +#if defined(HL_VK_DEBUG_MEM) + const MemoryRequest original_request = *request; +#endif + + conform_memory_request(request, memory_requirements.size, memory_requirements.alignment, config.nearest_multiple); + +#if defined(HL_VK_DEBUG_MEM) + if ((request->size != original_request.size) || (request->alignment != original_request.alignment) || (request->offset != original_request.offset)) { + debug(nullptr) << "VulkanMemoryAllocator: Adjusting request to match requirements (\n" + << " size = " << (uint64_t)original_request.size << " => " << (uint64_t)request->size << ",\n" + << " alignment = " << (uint64_t)original_request.alignment << " => " << (uint64_t)request->alignment << ",\n" + << " offset = " << (uint64_t)original_request.offset << " => " << (uint64_t)request->offset << ",\n" + << " required.size = " << (uint64_t)memory_requirements.size << ",\n" + << " required.alignment = " << (uint64_t)memory_requirements.alignment << "\n)\n"; + } +#endif + + return halide_error_code_success; } int VulkanMemoryAllocator::conform_region_request(void *instance_ptr, MemoryRequest *request) { diff --git a/test/runtime/block_allocator.cpp b/test/runtime/block_allocator.cpp index 7bc3d5ebd92f..77347356d3c4 100644 --- a/test/runtime/block_allocator.cpp +++ b/test/runtime/block_allocator.cpp @@ -460,6 +460,70 @@ int main(int argc, char **argv) { } } + // test conform_memory_request + { + // conform_memory_request must be idempotent: the block allocator conforms + // requests defensively at several layers, feeding an already-conformed + // request back in. Restrict the sweep to request alignments that do not + // exceed the required alignment (both powers of two) so the conformed + // alignment stays a power of two and aligned_offset does not abort. + for (size_t required_alignment = 1; required_alignment <= 128; required_alignment *= 2) { + for (size_t nearest_multiple = 0; nearest_multiple <= 64; nearest_multiple = nearest_multiple ? nearest_multiple * 2 : 1) { + for (size_t request_alignment = 0; request_alignment <= required_alignment; request_alignment = request_alignment ? request_alignment * 2 : 1) { + for (size_t required_size = 1; required_size <= 100; ++required_size) { + for (size_t offset = 0; offset <= 64; offset += 16) { + MemoryRequest request = {0}; + request.offset = offset; + request.alignment = request_alignment; + + conform_memory_request(&request, required_size, required_alignment, nearest_multiple); + MemoryRequest conformed = request; + + conform_memory_request(&request, required_size, required_alignment, nearest_multiple); + + halide_abort_if_false(user_context, request.size == conformed.size); + halide_abort_if_false(user_context, request.alignment == conformed.alignment); + halide_abort_if_false(user_context, request.offset == conformed.offset); + halide_abort_if_false(user_context, request.properties.nearest_multiple == conformed.properties.nearest_multiple); + + halide_abort_if_false(user_context, conformed.size >= required_size); + halide_abort_if_false(user_context, conformed.size >= conformed.alignment); + halide_abort_if_false(user_context, conformed.alignment == conform_alignment(request_alignment, required_alignment)); + halide_abort_if_false(user_context, conformed.offset == aligned_offset(offset, conformed.alignment)); + halide_abort_if_false(user_context, (conformed.offset % conformed.alignment) == 0); + if (conformed.properties.nearest_multiple > 0) { + halide_abort_if_false(user_context, (conformed.size % conformed.properties.nearest_multiple) == 0); + } + } + } + } + } + } + + // Regression scenarios from the vulkan allocator fixes (Mesa lavapipe/RADV + // reporting 64-byte buffer alignment): a buffer whose size is a multiple of + // the config nearest_multiple but not of the device alignment must round up + // to the device alignment and stay stable across repeated conforms. + size_t regression_required_size[2] = {96, 9338976}; + size_t regression_required_alignment[2] = {64, 64}; + size_t regression_nearest_multiple[2] = {32, 32}; + for (int i = 0; i < 2; ++i) { + MemoryRequest request = {0}; + + conform_memory_request(&request, regression_required_size[i], regression_required_alignment[i], regression_nearest_multiple[i]); + MemoryRequest conformed = request; + + conform_memory_request(&request, regression_required_size[i], regression_required_alignment[i], regression_nearest_multiple[i]); + + halide_abort_if_false(user_context, conformed.size >= regression_required_size[i]); + halide_abort_if_false(user_context, (conformed.size % regression_required_alignment[i]) == 0); + halide_abort_if_false(user_context, request.size == conformed.size); + halide_abort_if_false(user_context, request.alignment == conformed.alignment); + halide_abort_if_false(user_context, request.offset == conformed.offset); + halide_abort_if_false(user_context, request.properties.nearest_multiple == conformed.properties.nearest_multiple); + } + } + BlockAllocator::destroy(user_context, instance); HALIDE_CHECK(user_context, get_allocated_system_memory() == 0); } From ba7925450c146f7a43ea2fd735ce2aee2beef8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Ingve=20Schj=C3=B8lberg?= Date: Fri, 17 Jul 2026 16:06:41 +0200 Subject: [PATCH 4/5] [vulkan] Fix error messages in allocate_block The error messages in allocate_block say "unable to deallocate". Likely copy-pasted from deallcate_block. --- src/runtime/vulkan_memory.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/vulkan_memory.h b/src/runtime/vulkan_memory.h index b2f7f7b38602..1d299e748cbd 100644 --- a/src/runtime/vulkan_memory.h +++ b/src/runtime/vulkan_memory.h @@ -641,12 +641,12 @@ int VulkanMemoryAllocator::allocate_block(void *instance_ptr, MemoryBlock *block void *user_context = instance->owner_context; if ((instance->device == nullptr) || (instance->physical_device == nullptr)) { - error(user_context) << "VulkanBlockAllocator: Unable to deallocate block! Invalid device handle!\n"; + error(user_context) << "VulkanBlockAllocator: Unable to allocate block! Invalid device handle!\n"; return halide_error_code_internal_error; } if (block == nullptr) { - error(user_context) << "VulkanBlockAllocator: Unable to deallocate block! Invalid pointer!\n"; + error(user_context) << "VulkanBlockAllocator: Unable to allocate block! Invalid pointer!\n"; return halide_error_code_internal_error; } From 07c6b6cb7e2194d039c34eb1444af485a3be8844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Ingve=20Schj=C3=B8lberg?= Date: Fri, 17 Jul 2026 16:40:24 +0200 Subject: [PATCH 5/5] [vulkan] Fix compiling region allocator with DEBUG_RUNTIME_INTERNAL The debug print of find_block_region has block->memory.allocation.size. But this does not compile. block->memory has a size member itself, so use that instead. --- src/runtime/internal/region_allocator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/internal/region_allocator.h b/src/runtime/internal/region_allocator.h index 8c04116a3a65..d1351fccdae5 100644 --- a/src/runtime/internal/region_allocator.h +++ b/src/runtime/internal/region_allocator.h @@ -329,7 +329,7 @@ BlockRegion *RegionAllocator::find_block_region(void *user_context, const Memory debug(user_context) << "RegionAllocator: found suitable region ( " << "user_context=" << (void *)(user_context) << " " << "block_resource=" << (void *)block << " " - << "block_size=" << (uint32_t)block->memory.allocation.size << " " + << "block_size=" << (uint32_t)block->memory.size << " " << "block_reserved=" << (uint32_t)block->reserved << " " << "requested_size=" << (uint32_t)request.size << " " << "requested_is_dedicated=" << (request.dedicated ? "true" : "false") << " "