| // Copyright 2023 The IREE Authors |
| // |
| // Licensed under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| |
| #include "iree/io/parameter_index_provider.h" |
| |
| #include "iree/hal/utils/file_cache.h" |
| |
| // Limit concurrent operations to avoid blowing the stack. This is arbitrary and |
| // if we wanted to support more we could switch to using heap allocations or |
| // a growable stack scratchpad. |
| #define IREE_IO_PARAMETER_OP_BATCH_MAX_CONCURRENCY 8 |
| |
| typedef struct iree_io_parameter_index_provider_t { |
| iree_io_parameter_provider_t base; |
| iree_allocator_t host_allocator; |
| iree_host_size_t max_concurrent_operations; |
| iree_string_view_t scope; |
| iree_io_parameter_index_t* index; |
| iree_hal_file_cache_t* file_cache; |
| } iree_io_parameter_index_provider_t; |
| |
| static const iree_io_parameter_provider_vtable_t |
| iree_io_parameter_index_provider_vtable; |
| |
| static iree_io_parameter_index_provider_t* |
| iree_io_parameter_index_provider_cast( |
| iree_io_parameter_provider_t* IREE_RESTRICT base_provider) { |
| return (iree_io_parameter_index_provider_t*)base_provider; |
| } |
| |
| IREE_API_EXPORT iree_status_t iree_io_parameter_index_provider_create( |
| iree_string_view_t scope, iree_io_parameter_index_t* index, |
| iree_host_size_t max_concurrent_operations, iree_allocator_t host_allocator, |
| iree_io_parameter_provider_t** out_provider) { |
| IREE_ASSERT_ARGUMENT(index); |
| IREE_ASSERT_ARGUMENT(out_provider); |
| *out_provider = NULL; |
| IREE_TRACE_ZONE_BEGIN(z0); |
| IREE_TRACE_ZONE_APPEND_TEXT(z0, scope.data, scope.size); |
| |
| max_concurrent_operations = |
| iree_max(1, iree_min(max_concurrent_operations, |
| IREE_IO_PARAMETER_OP_BATCH_MAX_CONCURRENCY)); |
| |
| iree_io_parameter_index_provider_t* provider = NULL; |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_allocator_malloc_with_trailing(host_allocator, sizeof(*provider), |
| scope.size, (void**)&provider)); |
| iree_atomic_ref_count_init(&provider->base.ref_count); |
| provider->base.vtable = &iree_io_parameter_index_provider_vtable; |
| provider->host_allocator = host_allocator; |
| provider->max_concurrent_operations = max_concurrent_operations; |
| |
| provider->scope = iree_make_string_view( |
| (const char*)provider + sizeof(*provider), scope.size); |
| if (scope.size > 0) { |
| memcpy((void*)provider->scope.data, scope.data, scope.size); |
| } |
| |
| provider->index = index; |
| iree_io_parameter_index_retain(index); |
| |
| iree_status_t status = |
| iree_hal_file_cache_create(host_allocator, &provider->file_cache); |
| |
| if (iree_status_is_ok(status)) { |
| *out_provider = (iree_io_parameter_provider_t*)provider; |
| } else { |
| iree_io_parameter_provider_release((iree_io_parameter_provider_t*)provider); |
| } |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| static void iree_io_parameter_index_provider_destroy( |
| iree_io_parameter_provider_t* IREE_RESTRICT base_provider) { |
| iree_io_parameter_index_provider_t* provider = |
| iree_io_parameter_index_provider_cast(base_provider); |
| iree_allocator_t host_allocator = provider->host_allocator; |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| iree_hal_file_cache_release(provider->file_cache); |
| iree_io_parameter_index_release(provider->index); |
| |
| iree_allocator_free(host_allocator, provider); |
| |
| IREE_TRACE_ZONE_END(z0); |
| } |
| |
| static iree_status_t iree_io_parameter_index_provider_notify( |
| iree_io_parameter_provider_t* base_provider, |
| iree_io_parameter_provider_signal_t signal) { |
| iree_io_parameter_index_provider_t* provider = |
| iree_io_parameter_index_provider_cast(base_provider); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| switch (signal) { |
| case IREE_IO_PARAMETER_PROVIDER_SIGNAL_SUSPEND: |
| case IREE_IO_PARAMETER_PROVIDER_SIGNAL_LOW_MEMORY: |
| iree_hal_file_cache_trim(provider->file_cache); |
| break; |
| default: |
| break; |
| } |
| |
| IREE_TRACE_ZONE_END(z0); |
| return iree_ok_status(); |
| } |
| |
| static bool iree_io_parameter_index_provider_query_support( |
| iree_io_parameter_provider_t* base_provider, iree_string_view_t scope) { |
| iree_io_parameter_index_provider_t* provider = |
| iree_io_parameter_index_provider_cast(base_provider); |
| return iree_string_view_equal(scope, provider->scope); |
| } |
| |
| // Resolves a parameter with |key| for use on the given |device|. |
| // Returns the entry containing the parameter metadata and a retained |
| // HAL file that stores it (must be released by the caller). |
| // If the parameter is synthetic and not backed by a file then the returned |
| // file will be NULL. |
| static iree_status_t iree_io_parameter_index_provider_resolve( |
| iree_io_parameter_index_provider_t* provider, iree_hal_device_t* device, |
| iree_hal_queue_affinity_t queue_affinity, iree_string_view_t scope, |
| iree_string_view_t key, iree_hal_memory_access_t access, |
| const iree_io_parameter_index_entry_t** out_entry, |
| iree_hal_file_t** out_file) { |
| IREE_ASSERT_ARGUMENT(out_entry); |
| IREE_ASSERT_ARGUMENT(out_file); |
| *out_entry = NULL; |
| *out_file = NULL; |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| // Lookup the parameter in the index. |
| const iree_io_parameter_index_entry_t* entry = NULL; |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_io_parameter_index_lookup(provider->index, key, &entry)); |
| |
| // Get (or import) the HAL file backing the entry. |
| // NOTE: file is retained! |
| iree_hal_file_t* file = NULL; |
| if (entry->type == IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_FILE) { |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, |
| iree_hal_file_cache_lookup(provider->file_cache, device, queue_affinity, |
| access, entry->storage.file.handle, |
| IREE_HAL_EXTERNAL_BUFFER_FLAG_NONE, &file)); |
| } |
| |
| *out_entry = entry; |
| *out_file = file; |
| IREE_TRACE_ZONE_END(z0); |
| return iree_ok_status(); |
| } |
| |
| // Validates that the range specified by [offset, offset+length) is in bounds. |
| static iree_status_t iree_io_validate_parameter_range( |
| iree_hal_memory_access_t required_access, |
| const iree_io_parameter_index_entry_t* entry, uint64_t offset, |
| uint64_t length) { |
| iree_hal_memory_access_t allowed_access = IREE_HAL_MEMORY_ACCESS_NONE; |
| switch (entry->type) { |
| case IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_SPLAT: |
| // Synthetic entries are read-only. |
| allowed_access = IREE_HAL_MEMORY_ACCESS_READ; |
| break; |
| case IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_FILE: |
| // Access to the entry depends on the access of the file backing it. |
| if (iree_all_bits_set( |
| iree_io_file_handle_access(entry->storage.file.handle), |
| IREE_IO_FILE_ACCESS_READ)) { |
| allowed_access |= IREE_HAL_MEMORY_ACCESS_READ; |
| } |
| if (iree_all_bits_set( |
| iree_io_file_handle_access(entry->storage.file.handle), |
| IREE_IO_FILE_ACCESS_WRITE)) { |
| allowed_access |= |
| IREE_HAL_MEMORY_ACCESS_WRITE | IREE_HAL_MEMORY_ACCESS_DISCARD; |
| } |
| break; |
| default: |
| // Unknown entries are inaccessible. |
| allowed_access = IREE_HAL_MEMORY_ACCESS_NONE; |
| break; |
| } |
| if (!iree_all_bits_set(allowed_access, required_access)) { |
| #if IREE_STATUS_MODE |
| iree_bitfield_string_temp_t temp0, temp1; |
| iree_string_view_t allowed_memory_access_str = |
| iree_hal_memory_access_format(allowed_access, &temp0); |
| iree_string_view_t required_memory_access_str = |
| iree_hal_memory_access_format(required_access, &temp1); |
| return iree_make_status( |
| IREE_STATUS_PERMISSION_DENIED, |
| "parameter `%.*s` storage does not support the requested access " |
| "type; parameter allows %.*s, operation requires %.*s", |
| (int)entry->key.size, entry->key.data, |
| (int)allowed_memory_access_str.size, allowed_memory_access_str.data, |
| (int)required_memory_access_str.size, required_memory_access_str.data); |
| #else |
| return iree_status_from_code(IREE_STATUS_PERMISSION_DENIED); |
| #endif // IREE_STATUS_MODE |
| } |
| |
| if (offset + length > entry->length) { |
| return iree_make_status( |
| IREE_STATUS_OUT_OF_RANGE, |
| "parameter `%.*s` range out of bounds (offset=%" PRIu64 |
| ", length=%" PRIu64 ", size=%" PRIu64 ")", |
| (int)entry->key.size, entry->key.data, offset, length, entry->length); |
| } |
| |
| return iree_ok_status(); |
| } |
| |
| // Stateful batch management of multiple parameter operations. |
| // |
| // The batch distributes operations over multiple timelines based on how much |
| // I/O is done on each. It's possible for pathologically bad latency if there |
| // are large and small operations interleaved as all large operations may end up |
| // serialized on one timeline and all small ones on the other. |
| // We distribute by tracking the total bytes outstanding on each timeline and |
| // always placing the next operation on the one with the fewest. This assumes |
| // that all I/O happens at roughly the same speed but if parameters come from |
| // different files on different devices that may not be the case. It's better |
| // than doing nothing, though. |
| // |
| // Though we mostly focus on file I/O we also support DMA operations such as |
| // splats (synthetic parameters) and copies (device->device transfers) by way |
| // of a command buffer we build and submit when needed. Today there is just a |
| // single command buffer we submit with zero barriers which should be equivalent |
| // to spreading it out over multiple timelines. |
| // |
| // NOTE: we expect count == 0 to have been handled by callers to avoid the |
| // overhead of the batch setup and submission but it's valid to have a zero |
| // count. |
| typedef struct iree_io_parameter_op_batch_t { |
| // Parameter provider sourcing the parameter metadata. |
| iree_io_parameter_index_provider_t* provider; // unretained |
| // Device hosting the batch operation. |
| iree_hal_device_t* device; // unretained |
| // Queue affinity indicating where batch operations can run. |
| iree_hal_queue_affinity_t queue_affinity; |
| |
| // Semaphores that must be waited on prior to any operations begin. |
| iree_hal_semaphore_list_t wait_semaphore_list; |
| // Semaphores that must be signaled after all operations complete. |
| iree_hal_semaphore_list_t signal_semaphore_list; |
| |
| // Number of concurrent timelines available for processing the batch. |
| // Expects 0 < concurrency <= IREE_IO_PARAMETER_OP_BATCH_MAX_CONCURRENCY. |
| // Not all timelines may be used and timeline_live_count should be checked to |
| // see which are active. |
| iree_host_size_t concurrency; |
| // Number of timelines which have had operations scheduled against them. |
| // This allows us to filter out timelines that are idle upon completion |
| // and avoid unneeded waits. 0 if no timeline was used (count = 0, etc). |
| iree_host_size_t timeline_live_count; |
| // Sum of byte read/writes outstanding per timeline as a proxy for the amount |
| // of work performed in a particular timeline. |
| uint64_t |
| timeline_bytes_outstanding[IREE_IO_PARAMETER_OP_BATCH_MAX_CONCURRENCY]; |
| // Semaphore per timeline. |
| iree_hal_semaphore_t* |
| timeline_semaphores[IREE_IO_PARAMETER_OP_BATCH_MAX_CONCURRENCY]; |
| // Current payload per timeline; when the semaphore reaches this value the |
| // timeline has quiesced. |
| uint64_t timeline_values[IREE_IO_PARAMETER_OP_BATCH_MAX_CONCURRENCY]; |
| |
| // On-demand allocated command buffer used for transfer operations (usually |
| // splats, but could be copies to/from device buffers). |
| iree_hal_command_buffer_t* transfer_command_buffer; |
| // Sum of byte read/writes in the transfer command buffer as a proxy for |
| // the amount of work performed within the command buffer. We expect transfer |
| // operations to be cheaper than file I/O operations but are not trying to be |
| // precise here. |
| uint64_t transfer_bytes_outstanding; |
| } iree_io_parameter_op_batch_t; |
| |
| // Begins a parameter operation batch against the given |provider|. |
| // Operations will be scheduled on |device| with |queue_affinity|. All batch |
| // operations will wait until |wait_semaphore_list| has been reached and after |
| // all batch operations complete |signal_semaphore_list| will be signaled. |
| // Upon return callers are required to call iree_io_parameter_op_batch_end |
| // regardless of whether the caller encounters an error. |
| static void iree_io_parameter_op_batch_begin( |
| iree_io_parameter_index_provider_t* provider, iree_hal_device_t* device, |
| iree_hal_queue_affinity_t queue_affinity, |
| const iree_hal_semaphore_list_t wait_semaphore_list, |
| const iree_hal_semaphore_list_t signal_semaphore_list, |
| iree_io_parameter_op_batch_t* IREE_RESTRICT out_batch) { |
| IREE_ASSERT_ARGUMENT(provider); |
| IREE_ASSERT_ARGUMENT(device); |
| IREE_ASSERT_ARGUMENT(out_batch); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| memset(out_batch, 0, sizeof(*out_batch)); |
| |
| out_batch->provider = provider; |
| out_batch->device = device; |
| out_batch->queue_affinity = queue_affinity; |
| |
| out_batch->wait_semaphore_list = wait_semaphore_list; |
| out_batch->signal_semaphore_list = signal_semaphore_list; |
| |
| // We could limit the concurrency from the max based on the batch size but |
| // since the compiler batches everything and most models go over the default |
| // max concurrency this is fine for now. |
| out_batch->concurrency = |
| iree_max(1, iree_min(provider->max_concurrent_operations, |
| IREE_IO_PARAMETER_OP_BATCH_MAX_CONCURRENCY)); |
| |
| IREE_TRACE_ZONE_END(z0); |
| } |
| |
| // Resolves the parameter entry from |enumerator| at index |i|. |
| // |access| indicates the required access permissions to the parameter storage. |
| // Returns the entry, the span indicating source/target ranges, and optionally |
| // a file (NULL if a splat). |out_file| is retained and must be released by the |
| // caller if set. |
| static iree_status_t iree_io_parameter_op_batch_resolve_entry( |
| const iree_io_parameter_op_batch_t* batch, iree_string_view_t scope, |
| iree_io_parameter_enumerator_t enumerator, iree_host_size_t i, |
| iree_hal_memory_access_t access, |
| const iree_io_parameter_index_entry_t** IREE_RESTRICT out_entry, |
| iree_io_parameter_span_t* IREE_RESTRICT out_span, |
| iree_hal_file_t** IREE_RESTRICT out_file) { |
| IREE_ASSERT_ARGUMENT(out_entry); |
| IREE_ASSERT_ARGUMENT(out_span); |
| IREE_ASSERT_ARGUMENT(out_file); |
| *out_entry = NULL; |
| memset(out_span, 0, sizeof(*out_span)); |
| *out_file = NULL; |
| |
| // Fetch the next parameter to copy and its buffer range. |
| iree_string_view_t key = iree_string_view_empty(); |
| iree_io_parameter_span_t span = {0}; |
| IREE_RETURN_IF_ERROR(enumerator.fn(enumerator.user_data, i, &key, &span)); |
| |
| // Lookup the parameter metadata and get its backing file. |
| const iree_io_parameter_index_entry_t* entry = NULL; |
| iree_hal_file_t* file = NULL; // retained, NULL if splat |
| IREE_RETURN_IF_ERROR(iree_io_parameter_index_provider_resolve( |
| batch->provider, batch->device, batch->queue_affinity, scope, key, access, |
| &entry, &file)); |
| |
| // Validate the parameter range is in-bounds. |
| iree_status_t status = iree_io_validate_parameter_range( |
| access, entry, span.parameter_offset, span.length); |
| |
| if (iree_status_is_ok(status)) { |
| *out_entry = entry; |
| *out_span = span; |
| *out_file = file; |
| } else { |
| iree_hal_file_release(file); |
| } |
| return status; |
| } |
| |
| typedef struct { |
| iree_hal_semaphore_list_t wait_semaphore_list; |
| iree_hal_semaphore_list_t signal_semaphore_list; |
| uint64_t scratch_values[2]; // wait/signal payload values |
| } iree_io_parameter_op_step_t; |
| |
| // Selects a timeline with the fewest bytes outstanding and accounts for the new |
| // |op_byte_length| bytes on that timeline. Returns semaphore lists the caller |
| // must wait on before performing their operation and signal after their |
| // operation completes. |
| static iree_status_t iree_io_parameter_op_batch_advance_timeline( |
| iree_io_parameter_op_batch_t* batch, uint64_t op_byte_length, |
| iree_io_parameter_op_step_t* IREE_RESTRICT out_step) { |
| IREE_ASSERT_ARGUMENT(batch); |
| IREE_ASSERT_ARGUMENT(out_step); |
| memset(out_step, 0, sizeof(*out_step)); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, op_byte_length); |
| |
| // Find the timeline with the fewest outstanding bytes. |
| // Linear scan as the number of timelines is expected to be small. |
| uint64_t smallest_value = batch->timeline_bytes_outstanding[0]; |
| iree_host_size_t smallest_index = 0; |
| for (iree_host_size_t i = 1; i < batch->concurrency; ++i) { |
| if (batch->timeline_bytes_outstanding[i] < smallest_value) { |
| smallest_value = batch->timeline_bytes_outstanding[i]; |
| smallest_index = i; |
| } |
| } |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, (int64_t)smallest_index); |
| const iree_host_size_t timeline_index = smallest_index; |
| |
| // Acquire the timeline semaphore used for this operation. |
| // We create the semaphores on-demand so that in cases where we don't perform |
| // any operations (loads that perform synchronous imports) or only a small |
| // amount (one transfer command buffer or just a handful of operations) we |
| // don't create so much garbage. The intent is that HAL devices pool |
| // semaphores but not all do - if we made that an expected requirement we |
| // could simplify this. |
| iree_hal_semaphore_t* timeline_semaphore = |
| batch->timeline_semaphores[timeline_index]; |
| const bool is_first_timeline_use = timeline_semaphore == NULL; |
| if (!timeline_semaphore) { |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, |
| iree_hal_semaphore_create(batch->device, IREE_HAL_QUEUE_AFFINITY_ANY, |
| batch->timeline_values[timeline_index], |
| IREE_HAL_SEMAPHORE_FLAG_DEFAULT, |
| &batch->timeline_semaphores[timeline_index])); |
| timeline_semaphore = batch->timeline_semaphores[timeline_index]; |
| } |
| const uint64_t previous_timeline_value = |
| batch->timeline_values[timeline_index]; |
| const uint64_t next_timeline_value = ++batch->timeline_values[timeline_index]; |
| |
| // Account for the bytes processed by the operation, which is a good enough |
| // metric for distribution (assuming all operations take about the same amount |
| // of memory or I/O bandwidth). |
| batch->timeline_bytes_outstanding[timeline_index] += op_byte_length; |
| |
| // Select the wait semaphore list; the first wave of operations all wait on |
| // the original wait semaphore list provided by the initiator. |
| if (is_first_timeline_use) { |
| // First use of this timeline; wait on incoming list and begin the timeline. |
| IREE_ASSERT_EQ(timeline_index, batch->timeline_live_count); |
| ++batch->timeline_live_count; |
| out_step->wait_semaphore_list = batch->wait_semaphore_list; |
| } else { |
| // Continuation of the selected timeline. |
| out_step->scratch_values[0] = previous_timeline_value; |
| out_step->wait_semaphore_list.count = 1; |
| out_step->wait_semaphore_list.semaphores = |
| &batch->timeline_semaphores[timeline_index]; |
| out_step->wait_semaphore_list.payload_values = &out_step->scratch_values[0]; |
| } |
| |
| // Signal the continuation of the timeline. |
| out_step->scratch_values[1] = next_timeline_value; |
| out_step->signal_semaphore_list.count = 1; |
| out_step->signal_semaphore_list.semaphores = |
| &batch->timeline_semaphores[timeline_index]; |
| out_step->signal_semaphore_list.payload_values = &out_step->scratch_values[1]; |
| |
| IREE_TRACE_ZONE_END(z0); |
| return iree_ok_status(); |
| } |
| |
| // Enqueues a queue-ordered allocation. |
| // A timeline is selected based on utilization and the following operation is |
| // guaranteed to select the same timeline to ensure the allocation and |
| // operation are serialized and the wait has a higher chance of being elided. |
| static iree_status_t iree_io_parameter_op_batch_enqueue_alloca( |
| iree_io_parameter_op_batch_t* batch, iree_hal_pool_t* pool, |
| iree_hal_buffer_params_t params, iree_device_size_t allocation_size, |
| iree_hal_buffer_t** IREE_RESTRICT out_buffer) { |
| IREE_ASSERT_ARGUMENT(batch); |
| IREE_ASSERT_ARGUMENT(out_buffer); |
| *out_buffer = NULL; |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| // By passing 0 for the operation size we ensure that subsequent operations |
| // select the same timeline because the timeline with the fewest outstanding |
| // bytes will be returned after this. This intuitively seems good as we keep |
| // the allocation and operation using it serialized within a single timeline |
| // and allow devices that can elide back-to-back barriers to do so, but we may |
| // find that we also want to distribute allocations. |
| iree_io_parameter_op_step_t step; |
| IREE_RETURN_AND_END_ZONE_IF_ERROR(z0, |
| iree_io_parameter_op_batch_advance_timeline( |
| batch, /*op_byte_length=*/0, &step)); |
| |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_hal_device_queue_alloca( |
| batch->device, batch->queue_affinity, step.wait_semaphore_list, |
| step.signal_semaphore_list, pool, params, allocation_size, |
| IREE_HAL_ALLOCA_FLAG_NONE, out_buffer)); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return iree_ok_status(); |
| } |
| |
| // Enqueues a splat operation in the batch into the |buffer| range. |
| // Splats get routed to a transfer command buffer that we'll submit at the end |
| // of the batch. This avoids the need for us to check all of the operations |
| // ahead of time at the cost of potentially acquiring more semaphores than we |
| // need in cases where everything is a splat. Splats are pretty much only useful |
| // for testing/development, though, so it's ok to not be super efficient here. |
| static iree_status_t iree_io_parameter_op_batch_enqueue_splat( |
| iree_io_parameter_op_batch_t* batch, iree_hal_buffer_t* buffer, |
| iree_device_size_t buffer_offset, iree_device_size_t length, |
| const void* pattern, iree_host_size_t pattern_length) { |
| IREE_ASSERT_ARGUMENT(batch); |
| IREE_ASSERT_ARGUMENT(buffer); |
| IREE_ASSERT_ARGUMENT(pattern); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| // Create the transfer command buffer on first use. |
| if (!batch->transfer_command_buffer) { |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_hal_command_buffer_create( |
| batch->device, IREE_HAL_COMMAND_BUFFER_MODE_ONE_SHOT, |
| IREE_HAL_COMMAND_CATEGORY_TRANSFER, batch->queue_affinity, 0, |
| &batch->transfer_command_buffer)); |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_hal_command_buffer_begin(batch->transfer_command_buffer)); |
| } |
| |
| // Add the splat fill to the command buffer. |
| // Parameter ranges cannot overlap so there's no barrier required. |
| batch->transfer_bytes_outstanding += length; |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_hal_command_buffer_fill_buffer( |
| batch->transfer_command_buffer, |
| iree_hal_make_buffer_ref(buffer, buffer_offset, length), pattern, |
| pattern_length, IREE_HAL_FILL_FLAG_NONE)); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return iree_ok_status(); |
| } |
| |
| // Enqueues a file read operation in the batch. |
| static iree_status_t iree_io_parameter_op_batch_enqueue_file_read( |
| iree_io_parameter_op_batch_t* batch, iree_hal_file_t* source_file, |
| uint64_t source_file_offset, iree_hal_buffer_t* target_buffer, |
| iree_device_size_t target_buffer_offset, iree_device_size_t length, |
| iree_hal_read_flags_t flags) { |
| IREE_ASSERT_ARGUMENT(batch); |
| IREE_ASSERT_ARGUMENT(source_file); |
| IREE_ASSERT_ARGUMENT(target_buffer); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| iree_io_parameter_op_step_t step; |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_io_parameter_op_batch_advance_timeline(batch, length, &step)); |
| |
| iree_status_t status = iree_hal_device_queue_read( |
| batch->device, batch->queue_affinity, step.wait_semaphore_list, |
| step.signal_semaphore_list, source_file, source_file_offset, |
| target_buffer, target_buffer_offset, length, flags); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| // Enqueues a file write operation in the batch. |
| static iree_status_t iree_io_parameter_op_batch_enqueue_file_write( |
| iree_io_parameter_op_batch_t* batch, iree_hal_buffer_t* source_buffer, |
| iree_device_size_t source_buffer_offset, iree_hal_file_t* target_file, |
| uint64_t target_file_offset, iree_device_size_t length, |
| iree_hal_write_flags_t flags) { |
| IREE_ASSERT_ARGUMENT(batch); |
| IREE_ASSERT_ARGUMENT(source_buffer); |
| IREE_ASSERT_ARGUMENT(target_file); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| iree_io_parameter_op_step_t step; |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( |
| z0, iree_io_parameter_op_batch_advance_timeline(batch, length, &step)); |
| |
| iree_status_t status = iree_hal_device_queue_write( |
| batch->device, batch->queue_affinity, step.wait_semaphore_list, |
| step.signal_semaphore_list, source_buffer, source_buffer_offset, |
| target_file, target_file_offset, length, flags); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| // Flushes any outstanding work in the |batch| and signals the user timeline. |
| // Must only be called once at the end of the batch. |
| static iree_status_t iree_io_parameter_op_batch_flush( |
| iree_io_parameter_op_batch_t* batch) { |
| IREE_ASSERT_ARGUMENT(batch); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| |
| // If any transfers were performed we'll need to submit the command buffer we |
| // built during recording. Order doesn't matter so we can issue it alongside |
| // all of the other work by just appending it to an arbitrary timeline. We try |
| // to still balance things by selecting a timeline with the fewest operation |
| // bytes outstanding even if the cost of a byte differs between file I/O and |
| // pure DMA operations. |
| iree_status_t status = iree_ok_status(); |
| if (batch->transfer_command_buffer) { |
| IREE_TRACE_ZONE_BEGIN_NAMED(z_transfer, |
| "iree_io_parameter_op_batch_flush_transfer"); |
| status = iree_hal_command_buffer_end(batch->transfer_command_buffer); |
| iree_io_parameter_op_step_t step; |
| if (iree_status_is_ok(status)) { |
| status = iree_io_parameter_op_batch_advance_timeline( |
| batch, batch->transfer_bytes_outstanding, &step); |
| } |
| if (iree_status_is_ok(status)) { |
| status = iree_hal_device_queue_execute( |
| batch->device, batch->queue_affinity, step.wait_semaphore_list, |
| step.signal_semaphore_list, batch->transfer_command_buffer, |
| iree_hal_buffer_binding_table_empty(), IREE_HAL_EXECUTE_FLAG_NONE); |
| } |
| IREE_TRACE_ZONE_END(z_transfer); |
| } |
| |
| // Join all concurrent timelines and continue the user-provided timeline. |
| if (iree_status_is_ok(status)) { |
| // If no queue operations were performed (all load imports, 0 entries, etc) |
| // we need to issue a barrier to link the wait->signal semaphore lists. |
| if (batch->timeline_live_count == 0) { |
| IREE_TRACE_ZONE_APPEND_TEXT(z0, "pass-through wait-signal"); |
| status = iree_hal_device_queue_barrier( |
| batch->device, batch->queue_affinity, batch->wait_semaphore_list, |
| batch->signal_semaphore_list, IREE_HAL_EXECUTE_FLAG_NONE); |
| } else { |
| IREE_TRACE_ZONE_APPEND_TEXT(z0, "timeline set wait chain"); |
| // Note that we allocate timelines on-demand up to timeline_live_count so |
| // we can just pass the [0, timeline_live_count) range here. |
| iree_hal_semaphore_list_t join_semaphore_list = { |
| .count = batch->timeline_live_count, |
| .semaphores = batch->timeline_semaphores, |
| .payload_values = batch->timeline_values, |
| }; |
| status = iree_hal_device_queue_barrier( |
| batch->device, batch->queue_affinity, join_semaphore_list, |
| batch->signal_semaphore_list, IREE_HAL_EXECUTE_FLAG_NONE); |
| } |
| } |
| |
| // Report the total number of bytes transferred by the batch. |
| IREE_TRACE({ |
| uint64_t total_bytes = batch->transfer_bytes_outstanding; |
| for (iree_host_size_t i = 0; i < batch->concurrency; ++i) { |
| total_bytes += batch->timeline_bytes_outstanding[i]; |
| } |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, total_bytes); |
| }); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| // Ends a parameter operation batch. |
| // The provided |status| must be set to any failure that may have occurred |
| // between when the batch began and this method was called. The status will be |
| // passed on to the initiating caller by way of the signal semaphores being |
| // immediately failed. Returns the status provided to allow for propagating |
| // failures. |
| static iree_status_t iree_io_parameter_op_batch_end( |
| iree_io_parameter_op_batch_t* batch, iree_status_t status) { |
| IREE_ASSERT_ARGUMENT(batch); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| IREE_TRACE_ZONE_APPEND_TEXT( |
| z0, iree_status_code_string(iree_status_code(status))); |
| |
| // If the batch recording succeeded we flush now to finish any pending |
| // operations and signal the semaphores. |
| if (iree_status_is_ok(status)) { |
| status = iree_io_parameter_op_batch_flush(batch); |
| } |
| |
| // If the batch recording failed (or our flush did) we need to propagate that |
| // to the downstream user semaphores. |
| if (!iree_status_is_ok(status)) { |
| iree_hal_semaphore_list_fail(batch->signal_semaphore_list, |
| iree_status_clone(status)); |
| } |
| |
| // Resources are safe to release even if there are pending device operations |
| // as the device guarantees the resources remain live. |
| for (iree_host_size_t i = 0; i < batch->concurrency; ++i) { |
| iree_hal_semaphore_release(batch->timeline_semaphores[i]); |
| } |
| iree_hal_command_buffer_release(batch->transfer_command_buffer); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| static void iree_io_file_handle_buffer_release(void* user_data, |
| iree_hal_buffer_t* buffer) { |
| iree_io_file_handle_release((iree_io_file_handle_t*)user_data); |
| } |
| |
| static iree_status_t iree_io_parameter_index_provider_load( |
| iree_io_parameter_provider_t* base_provider, iree_hal_device_t* device, |
| iree_hal_queue_affinity_t queue_affinity, |
| const iree_hal_semaphore_list_t wait_semaphore_list, |
| const iree_hal_semaphore_list_t signal_semaphore_list, |
| iree_string_view_t source_scope, iree_hal_buffer_params_t target_params, |
| iree_host_size_t count, iree_io_parameter_enumerator_t enumerator, |
| iree_io_parameter_emitter_t emitter) { |
| iree_io_parameter_index_provider_t* provider = |
| iree_io_parameter_index_provider_cast(base_provider); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, count); |
| |
| // Initialize the batch state. |
| iree_io_parameter_op_batch_t batch; |
| iree_io_parameter_op_batch_begin(provider, device, queue_affinity, |
| wait_semaphore_list, signal_semaphore_list, |
| &batch); |
| |
| // Process each entry by enqueuing the appropriate operation. |
| iree_status_t status = iree_ok_status(); |
| for (iree_host_size_t i = 0; i < count; ++i) { |
| IREE_TRACE_ZONE_BEGIN_NAMED(z_entry, |
| "iree_io_parameter_index_provider_load_entry"); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z_entry, i); |
| |
| // Fetch the next parameter to process. |
| const iree_io_parameter_index_entry_t* source_entry = NULL; |
| iree_io_parameter_span_t span; |
| iree_hal_file_t* source_file = NULL; // retained, NULL if splat |
| status = iree_io_parameter_op_batch_resolve_entry( |
| &batch, source_scope, enumerator, i, IREE_HAL_MEMORY_ACCESS_READ, |
| &source_entry, &span, &source_file); |
| if (iree_status_is_ok(status)) { |
| IREE_TRACE_ZONE_APPEND_TEXT(z_entry, source_entry->key.data, |
| source_entry->key.size); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z_entry, span.length); |
| } |
| |
| // TODO(benvanik): refactor iree_io_parameter_index_provider_resolve so that |
| // it doesn't resolve the HAL file. Today if we hit the perfect case where |
| // all loads are able to be imported directly then we don't end up using the |
| // file we get back and instead have just wasted time/resources managing it. |
| // On CPU it's relatively cheap (a few mallocs) but on GPU it may require |
| // extremely expensive driver handling. Startup paths with parameters aren't |
| // usually critical, though, so it's (probably) fine today as-is. |
| |
| // Try first to reuse the file backing store directly as a buffer. This only |
| // works with specific file types and with specific target usage. The most |
| // common cases for this are when using parameters as staging sources (so |
| // host memory is ok) or on unified memory systems (where host memory is |
| // device memory) and the file was originally mapped. We could extend the |
| // conditions in which we use this with some better file handle helpers that |
| // allow us to map files that we already have open via other mechanisms |
| // (FILE, fd, etc). |
| iree_hal_buffer_t* target_buffer = NULL; |
| if (iree_status_is_ok(status) && |
| source_entry->type == IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_FILE && |
| iree_io_file_handle_type(source_entry->storage.file.handle) == |
| IREE_IO_FILE_HANDLE_TYPE_HOST_ALLOCATION) { |
| iree_byte_span_t host_allocation = |
| iree_io_file_handle_primitive(source_entry->storage.file.handle) |
| .value.host_allocation; |
| iree_hal_external_buffer_t external_buffer = { |
| .type = IREE_HAL_EXTERNAL_BUFFER_TYPE_HOST_ALLOCATION, |
| .flags = IREE_HAL_EXTERNAL_BUFFER_FLAG_NONE, |
| .size = host_allocation.data_length, |
| .handle = |
| { |
| .host_allocation = |
| { |
| .ptr = host_allocation.data + |
| source_entry->storage.file.offset, |
| }, |
| }, |
| }; |
| iree_hal_buffer_release_callback_t release_callback = { |
| .fn = iree_io_file_handle_buffer_release, |
| .user_data = source_entry->storage.file.handle, |
| }; |
| iree_io_file_handle_retain(source_entry->storage.file.handle); |
| iree_status_t import_status = iree_hal_allocator_import_buffer( |
| iree_hal_device_allocator(device), target_params, &external_buffer, |
| release_callback, &target_buffer); |
| if (iree_status_is_ok(import_status)) { |
| // Import succeeded - issue a barrier to preserve the async timeline. |
| IREE_TRACE_ZONE_APPEND_TEXT(z_entry, "import succeeded"); |
| } else { |
| // Failed to import - that's ok as we'll just do the full allocate + |
| // read. |
| IREE_TRACE_ZONE_APPEND_TEXT(z_entry, "import failed"); |
| import_status = iree_status_ignore(import_status); |
| iree_io_file_handle_release(source_entry->storage.file.handle); |
| } |
| } |
| |
| // When the import path above fails we fall back to alloca + fill/read. |
| if (iree_status_is_ok(status) && !target_buffer) { |
| // Enqueue an allocation of the target buffer on a timeline. |
| // The next operation we enqueue will go on the same timeline. |
| status = iree_io_parameter_op_batch_enqueue_alloca( |
| &batch, /*pool=*/NULL, target_params, span.length, &target_buffer); |
| |
| // Enqueue the operation on the same timeline as the allocation. |
| if (iree_status_is_ok(status)) { |
| switch (source_entry->type) { |
| case IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_SPLAT: { |
| IREE_ASSERT(!source_file); |
| status = iree_io_parameter_op_batch_enqueue_splat( |
| &batch, target_buffer, span.buffer_offset, span.length, |
| source_entry->storage.splat.pattern, |
| source_entry->storage.splat.pattern_length); |
| break; |
| } |
| case IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_FILE: { |
| IREE_ASSERT(source_file); |
| status = iree_io_parameter_op_batch_enqueue_file_read( |
| &batch, source_file, |
| source_entry->storage.file.offset + span.parameter_offset, |
| target_buffer, span.buffer_offset, span.length, 0); |
| break; |
| } |
| default: { |
| status = iree_make_status( |
| IREE_STATUS_FAILED_PRECONDITION, |
| "load not supported with parameters of type %d", |
| (int)source_entry->type); |
| break; |
| } |
| } |
| } |
| } |
| |
| iree_hal_file_release(source_file); |
| |
| // Emit the target buffer so the caller can handle it. The callee must |
| // retain it if they want to keep it live. We're allowed to emit out of |
| // order but are currently always 1:1 with enumeration (which may be useful |
| // in the future if we decide to make enumeration non-indexing). |
| if (iree_status_is_ok(status)) { |
| status = emitter.fn(emitter.user_data, i, target_buffer); |
| } |
| iree_hal_buffer_release(target_buffer); |
| |
| IREE_TRACE_ZONE_END(z_entry); |
| if (!iree_status_is_ok(status)) break; |
| } |
| |
| // Flush any outstanding batch operations and end the batch. |
| status = iree_io_parameter_op_batch_end(&batch, status); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| static iree_status_t iree_io_parameter_index_provider_gather( |
| iree_io_parameter_provider_t* base_provider, iree_hal_device_t* device, |
| iree_hal_queue_affinity_t queue_affinity, |
| const iree_hal_semaphore_list_t wait_semaphore_list, |
| const iree_hal_semaphore_list_t signal_semaphore_list, |
| iree_string_view_t source_scope, iree_hal_buffer_t* target_buffer, |
| iree_host_size_t count, iree_io_parameter_enumerator_t enumerator) { |
| iree_io_parameter_index_provider_t* provider = |
| iree_io_parameter_index_provider_cast(base_provider); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, count); |
| |
| // Initialize the batch state. |
| iree_io_parameter_op_batch_t batch; |
| iree_io_parameter_op_batch_begin(provider, device, queue_affinity, |
| wait_semaphore_list, signal_semaphore_list, |
| &batch); |
| |
| // Process each entry by enqueuing the appropriate operation. |
| iree_status_t status = iree_ok_status(); |
| for (iree_host_size_t i = 0; i < count; ++i) { |
| IREE_TRACE_ZONE_BEGIN_NAMED( |
| z_entry, "iree_io_parameter_index_provider_gather_entry"); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z_entry, i); |
| |
| // Fetch the next parameter to process. |
| const iree_io_parameter_index_entry_t* source_entry = NULL; |
| iree_io_parameter_span_t span; |
| iree_hal_file_t* source_file = NULL; // retained, NULL if splat |
| status = iree_io_parameter_op_batch_resolve_entry( |
| &batch, source_scope, enumerator, i, IREE_HAL_MEMORY_ACCESS_READ, |
| &source_entry, &span, &source_file); |
| if (iree_status_is_ok(status)) { |
| IREE_TRACE_ZONE_APPEND_TEXT(z_entry, source_entry->key.data, |
| source_entry->key.size); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z_entry, span.length); |
| } |
| |
| // Enqueue the transfer/file operation. |
| if (iree_status_is_ok(status)) { |
| switch (source_entry->type) { |
| case IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_SPLAT: { |
| IREE_ASSERT(!source_file); |
| status = iree_io_parameter_op_batch_enqueue_splat( |
| &batch, target_buffer, span.buffer_offset, span.length, |
| source_entry->storage.splat.pattern, |
| source_entry->storage.splat.pattern_length); |
| break; |
| } |
| case IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_FILE: { |
| IREE_ASSERT(source_file); |
| status = iree_io_parameter_op_batch_enqueue_file_read( |
| &batch, source_file, |
| source_entry->storage.file.offset + span.parameter_offset, |
| target_buffer, span.buffer_offset, span.length, 0); |
| break; |
| } |
| default: { |
| status = iree_make_status( |
| IREE_STATUS_FAILED_PRECONDITION, |
| "gather not supported with parameters of type %d", |
| (int)source_entry->type); |
| break; |
| } |
| } |
| } |
| |
| iree_hal_file_release(source_file); |
| |
| IREE_TRACE_ZONE_END(z_entry); |
| if (!iree_status_is_ok(status)) break; |
| } |
| |
| // Flush any outstanding batch operations and end the batch. |
| status = iree_io_parameter_op_batch_end(&batch, status); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| static iree_status_t iree_io_parameter_index_provider_scatter( |
| iree_io_parameter_provider_t* base_provider, iree_hal_device_t* device, |
| iree_hal_queue_affinity_t queue_affinity, |
| const iree_hal_semaphore_list_t wait_semaphore_list, |
| const iree_hal_semaphore_list_t signal_semaphore_list, |
| iree_hal_buffer_t* source_buffer, iree_string_view_t target_scope, |
| iree_host_size_t count, iree_io_parameter_enumerator_t enumerator) { |
| iree_io_parameter_index_provider_t* provider = |
| iree_io_parameter_index_provider_cast(base_provider); |
| IREE_TRACE_ZONE_BEGIN(z0); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, count); |
| |
| // Initialize the batch state. |
| iree_io_parameter_op_batch_t batch; |
| iree_io_parameter_op_batch_begin(provider, device, queue_affinity, |
| wait_semaphore_list, signal_semaphore_list, |
| &batch); |
| |
| // Process each entry by enqueuing the appropriate operation. |
| iree_status_t status = iree_ok_status(); |
| for (iree_host_size_t i = 0; i < count; ++i) { |
| IREE_TRACE_ZONE_BEGIN_NAMED( |
| z_entry, "iree_io_parameter_index_provider_scatter_entry"); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z_entry, i); |
| |
| // Fetch the next parameter to process. |
| const iree_io_parameter_index_entry_t* target_entry = NULL; |
| iree_io_parameter_span_t span; |
| iree_hal_file_t* target_file = NULL; // retained, NULL if splat |
| status = iree_io_parameter_op_batch_resolve_entry( |
| &batch, target_scope, enumerator, i, IREE_HAL_MEMORY_ACCESS_WRITE, |
| &target_entry, &span, &target_file); |
| if (iree_status_is_ok(status)) { |
| IREE_TRACE_ZONE_APPEND_TEXT(z_entry, target_entry->key.data, |
| target_entry->key.size); |
| IREE_TRACE_ZONE_APPEND_VALUE_I64(z_entry, span.length); |
| } |
| |
| // Enqueue the transfer/file operation. |
| if (iree_status_is_ok(status)) { |
| switch (target_entry->type) { |
| case IREE_IO_PARAMETER_INDEX_ENTRY_STORAGE_TYPE_FILE: { |
| IREE_ASSERT(target_file); |
| status = iree_io_parameter_op_batch_enqueue_file_write( |
| &batch, source_buffer, span.buffer_offset, target_file, |
| target_entry->storage.file.offset + span.parameter_offset, |
| span.length, 0); |
| break; |
| } |
| default: { |
| status = iree_make_status( |
| IREE_STATUS_FAILED_PRECONDITION, |
| "scatter not supported with parameters of type %d", |
| (int)target_entry->type); |
| break; |
| } |
| } |
| } |
| |
| iree_hal_file_release(target_file); |
| |
| IREE_TRACE_ZONE_END(z_entry); |
| if (!iree_status_is_ok(status)) break; |
| } |
| |
| // Flush any outstanding batch operations and end the batch. |
| status = iree_io_parameter_op_batch_end(&batch, status); |
| |
| IREE_TRACE_ZONE_END(z0); |
| return status; |
| } |
| |
| static const iree_io_parameter_provider_vtable_t |
| iree_io_parameter_index_provider_vtable = { |
| .destroy = iree_io_parameter_index_provider_destroy, |
| .notify = iree_io_parameter_index_provider_notify, |
| .query_support = iree_io_parameter_index_provider_query_support, |
| .load = iree_io_parameter_index_provider_load, |
| .gather = iree_io_parameter_index_provider_gather, |
| .scatter = iree_io_parameter_index_provider_scatter, |
| }; |