blob: 9de9cca1dae3752e1dcf4efb45074a1a184cff9f [file]
// 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/hal/utils/memory_file.h"
//===----------------------------------------------------------------------===//
// Configuration
//===----------------------------------------------------------------------===//
// TODO(benvanik): make these either compile-time configuration options so we
// can prune code paths or flags (somehow).
// When 1 a fast-path for importable memory will be used to avoid staging.
#if !defined(IREE_HAL_MEMORY_FILE_CAN_IMPORT)
#define IREE_HAL_MEMORY_FILE_CAN_IMPORT 1
#endif // !IREE_HAL_MEMORY_FILE_CAN_IMPORT
//===----------------------------------------------------------------------===//
// iree_hal_memory_file_storage_t
//===----------------------------------------------------------------------===//
// Reference-counted storage for memory file contents.
// This allows both the memory file and any intermediate/staging buffers that
// may reference it to keep the underlying storage live and not create cycles.
typedef struct iree_hal_memory_file_storage_t {
// Reference count for this storage instance.
iree_atomic_ref_count_t ref_count;
// Used to allocate this structure.
iree_allocator_t host_allocator;
// Base file handle, retained.
iree_io_file_handle_t* handle;
// Host memory contents, unowned.
iree_byte_span_t contents;
} iree_hal_memory_file_storage_t;
static iree_status_t iree_hal_memory_file_storage_create(
iree_io_file_handle_t* handle, iree_byte_span_t contents,
iree_allocator_t host_allocator,
iree_hal_memory_file_storage_t** out_storage) {
IREE_ASSERT_ARGUMENT(handle);
IREE_ASSERT_ARGUMENT(out_storage);
*out_storage = NULL;
IREE_TRACE_ZONE_BEGIN(z0);
iree_hal_memory_file_storage_t* storage = NULL;
IREE_RETURN_AND_END_ZONE_IF_ERROR(
z0, iree_allocator_malloc(host_allocator, sizeof(*storage),
(void**)&storage));
iree_atomic_ref_count_init(&storage->ref_count);
storage->host_allocator = host_allocator;
storage->handle = handle;
iree_io_file_handle_retain(storage->handle);
storage->contents = contents;
*out_storage = storage;
IREE_TRACE_ZONE_END(z0);
return iree_ok_status();
}
static void iree_hal_memory_file_storage_destroy(
iree_hal_memory_file_storage_t* storage) {
IREE_ASSERT_ARGUMENT(storage);
IREE_TRACE_ZONE_BEGIN(z0);
iree_allocator_t host_allocator = storage->host_allocator;
iree_io_file_handle_release(storage->handle);
iree_allocator_free(host_allocator, storage);
IREE_TRACE_ZONE_END(z0);
}
static void iree_hal_memory_file_storage_retain(
iree_hal_memory_file_storage_t* storage) {
if (IREE_LIKELY(storage)) {
iree_atomic_ref_count_inc(&storage->ref_count);
}
}
static void iree_hal_memory_file_storage_release(
iree_hal_memory_file_storage_t* storage) {
if (IREE_LIKELY(storage) &&
iree_atomic_ref_count_dec(&storage->ref_count) == 1) {
iree_hal_memory_file_storage_destroy(storage);
}
}
//===----------------------------------------------------------------------===//
// iree_hal_memory_file_t
//===----------------------------------------------------------------------===//
typedef struct iree_hal_memory_file_t {
iree_hal_resource_t resource;
// Used to allocate this structure.
iree_allocator_t host_allocator;
// Allowed access bits.
iree_hal_memory_access_t access;
// Underlying storage container, retained.
iree_hal_memory_file_storage_t* storage;
// Optional imported buffer if it was possible to do so.
// Not all implementations and not all buffers can be imported.
iree_hal_buffer_t* imported_buffer;
} iree_hal_memory_file_t;
static const iree_hal_file_vtable_t iree_hal_memory_file_vtable;
static iree_hal_memory_file_t* iree_hal_memory_file_cast(
iree_hal_file_t* IREE_RESTRICT base_value) {
return (iree_hal_memory_file_t*)base_value;
}
static void iree_hal_memory_file_try_import_buffer(
iree_hal_memory_file_t* file, iree_hal_queue_affinity_t queue_affinity,
iree_hal_memory_access_t access, iree_byte_span_t contents,
iree_hal_allocator_t* device_allocator);
IREE_API_EXPORT iree_status_t iree_hal_memory_file_wrap(
iree_hal_queue_affinity_t queue_affinity, iree_hal_memory_access_t access,
iree_io_file_handle_t* handle, iree_hal_allocator_t* device_allocator,
iree_allocator_t host_allocator, iree_hal_file_t** out_file) {
IREE_ASSERT_ARGUMENT(out_file);
*out_file = NULL;
IREE_TRACE_ZONE_BEGIN(z0);
// For now we only support host allocations but could open other types that
// may be backed by memory if desired.
if (iree_io_file_handle_type(handle) !=
IREE_IO_FILE_HANDLE_TYPE_HOST_ALLOCATION) {
IREE_TRACE_ZONE_END(z0);
return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
"support for wrapping non-host-allocation file "
"handles with memory files is not yet implemented");
}
iree_byte_span_t contents = iree_io_file_handle_value(handle).host_allocation;
// Note that iree_device_size_t (for device offsets/sizes) may be smaller than
// iree_host_size_t (for host offsets/sizes) - if so we need to ensure the
// bytes passed in will still fit in iree_device_size_t.
if (contents.data_length > IREE_DEVICE_SIZE_MAX) {
IREE_TRACE_ZONE_END(z0);
return iree_make_status(IREE_STATUS_RESOURCE_EXHAUSTED,
"device size too small to represent host contents");
}
// Allocate file handle; this just holds a reference to the storage and
// (optionally) the imported buffer.
iree_hal_memory_file_t* file = NULL;
IREE_RETURN_AND_END_ZONE_IF_ERROR(
z0, iree_allocator_malloc(host_allocator, sizeof(*file), (void**)&file));
iree_hal_resource_initialize(&iree_hal_memory_file_vtable, &file->resource);
file->host_allocator = host_allocator;
file->access = access;
// Create the underlying storage container that we use to manage the storage
// lifetime independently from the file lifetime.
iree_status_t status = iree_hal_memory_file_storage_create(
handle, contents, host_allocator, &file->storage);
#if !IREE_HAL_MEMORY_FILE_CAN_IMPORT
// Importing disabled; useful for testing the slow path.
device_allocator = NULL;
#endif // IREE_HAL_MEMORY_FILE_CAN_IMPORT
// Try importing the buffer as a host-local staging buffer.
// This won't always succeed due to device, platform, HAL implementation, or
// buffer limitations but if it does we can avoid staging ourselves during
// streaming and directly read/write the memory via transfer commands.
if (iree_status_is_ok(status) && device_allocator) {
iree_hal_memory_file_try_import_buffer(file, queue_affinity, access,
contents, device_allocator);
}
if (iree_status_is_ok(status)) {
*out_file = (iree_hal_file_t*)file;
} else {
iree_hal_file_release((iree_hal_file_t*)file);
}
IREE_TRACE_ZONE_END(z0);
return status;
}
static void iree_hal_memory_file_destroy(
iree_hal_file_t* IREE_RESTRICT base_file) {
iree_hal_memory_file_t* file = iree_hal_memory_file_cast(base_file);
iree_allocator_t host_allocator = file->host_allocator;
IREE_TRACE_ZONE_BEGIN(z0);
if (file->imported_buffer) {
iree_hal_buffer_release(file->imported_buffer);
file->imported_buffer = NULL;
}
iree_hal_memory_file_storage_release(file->storage);
iree_allocator_free(host_allocator, file);
IREE_TRACE_ZONE_END(z0);
}
// Releases the underlying file storage after the buffer using it is released.
static void iree_hal_memory_file_buffer_release(void* user_data,
iree_hal_buffer_t* buffer) {
iree_hal_memory_file_storage_release(
(iree_hal_memory_file_storage_t*)user_data);
}
// Tries to import |contents| as a device-accessible HAL buffer.
// If this succeeds we can fast-path copies without needing to allocate any
// staging buffers and directly make use of DMA resources. If it fails we fall
// back to staging from host memory ourselves.
static void iree_hal_memory_file_try_import_buffer(
iree_hal_memory_file_t* file, iree_hal_queue_affinity_t queue_affinity,
iree_hal_memory_access_t access, iree_byte_span_t contents,
iree_hal_allocator_t* device_allocator) {
IREE_TRACE_ZONE_BEGIN(z0);
iree_hal_buffer_params_t staging_buffer_params = {
.access = access,
.queue_affinity = queue_affinity,
.type = IREE_HAL_MEMORY_TYPE_OPTIMAL_FOR_HOST |
IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
.usage = IREE_HAL_BUFFER_USAGE_MAPPING_SCOPED |
IREE_HAL_BUFFER_USAGE_MAPPING_ACCESS_SEQUENTIAL_WRITE |
(iree_any_bit_set(access, IREE_HAL_MEMORY_ACCESS_READ)
? IREE_HAL_BUFFER_USAGE_TRANSFER_SOURCE
: 0) |
(iree_any_bit_set(access, IREE_HAL_MEMORY_ACCESS_WRITE)
? IREE_HAL_BUFFER_USAGE_TRANSFER_TARGET
: 0),
};
iree_hal_external_buffer_t external_buffer = {
.type = IREE_HAL_EXTERNAL_BUFFER_TYPE_HOST_ALLOCATION,
.flags = 0,
.size = (iree_device_size_t)contents.data_length,
.handle =
{
.host_allocation =
{
.ptr = contents.data,
},
},
};
// NOTE: we make the buffer retain the underlying storage.
// We have to handle the case where the import fails and we need to balance
// the retain we did below.
iree_hal_buffer_release_callback_t imported_release_callback = {
.fn = iree_hal_memory_file_buffer_release,
.user_data = file->storage,
};
iree_hal_memory_file_storage_retain(file->storage);
iree_status_t status = iree_hal_allocator_import_buffer(
device_allocator, staging_buffer_params, &external_buffer,
imported_release_callback, &file->imported_buffer);
if (!iree_status_is_ok(status)) {
iree_hal_memory_file_storage_release(file->storage);
}
IREE_TRACE({
if (iree_status_is_ok(status)) {
IREE_TRACE_ZONE_APPEND_TEXT(z0, "import success");
} else {
IREE_TRACE_ZONE_APPEND_TEXT(z0, "import failure");
IREE_TRACE_ZONE_APPEND_TEXT(
z0, iree_status_code_string(iree_status_code(status)));
}
});
IREE_TRACE_ZONE_END(z0);
iree_status_ignore(status);
}
static const iree_hal_file_vtable_t iree_hal_memory_file_vtable = {
.destroy = iree_hal_memory_file_destroy,
};
//===----------------------------------------------------------------------===//
// EXPERIMENTAL: synchronous file read/write API
//===----------------------------------------------------------------------===//
// This is incomplete and may not appear like this on the iree_hal_file_t
// vtable; this does work for memory files though.
IREE_API_EXPORT iree_hal_memory_access_t
iree_hal_file_allowed_access(iree_hal_file_t* base_file) {
IREE_ASSERT_ARGUMENT(base_file);
// EXPERIMENTAL: today only memory files. This should be on the file vtable
// (if supported - not all implementations need to support it).
iree_hal_memory_file_t* file = (iree_hal_memory_file_t*)base_file;
return file->access;
}
IREE_API_EXPORT uint64_t iree_hal_file_length(iree_hal_file_t* base_file) {
IREE_ASSERT_ARGUMENT(base_file);
// EXPERIMENTAL: today only memory files. This should be on the file vtable
// (if supported - not all implementations need to support it).
iree_hal_memory_file_t* file = (iree_hal_memory_file_t*)base_file;
return file->storage->contents.data_length;
}
IREE_API_EXPORT iree_hal_buffer_t* iree_hal_file_storage_buffer(
iree_hal_file_t* base_file) {
IREE_ASSERT_ARGUMENT(base_file);
// EXPERIMENTAL: today only memory files. This should be on the file vtable
// (if supported - not all implementations need to support it).
iree_hal_memory_file_t* file = (iree_hal_memory_file_t*)base_file;
return file->imported_buffer;
}
IREE_API_EXPORT iree_status_t iree_hal_file_read(
iree_hal_file_t* base_file, uint64_t file_offset, iree_hal_buffer_t* buffer,
iree_device_size_t buffer_offset, iree_device_size_t length) {
IREE_ASSERT_ARGUMENT(base_file);
IREE_ASSERT_ARGUMENT(buffer);
IREE_TRACE_ZONE_BEGIN(z0);
IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, file_offset);
IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, (int64_t)buffer_offset);
IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, (int64_t)length);
// EXPERIMENTAL: today only memory files. This should be on the file vtable
// (if supported - not all implementations need to support it).
iree_hal_memory_file_t* file = (iree_hal_memory_file_t*)base_file;
// Copy from the file contents to the staging buffer.
iree_byte_span_t file_contents = file->storage->contents;
iree_status_t status = iree_hal_buffer_map_write(
buffer, buffer_offset, file_contents.data + file_offset, length);
IREE_TRACE_ZONE_END(z0);
return status;
}
IREE_API_EXPORT iree_status_t iree_hal_file_write(
iree_hal_file_t* base_file, uint64_t file_offset, iree_hal_buffer_t* buffer,
iree_device_size_t buffer_offset, iree_device_size_t length) {
IREE_ASSERT_ARGUMENT(base_file);
IREE_ASSERT_ARGUMENT(buffer);
IREE_TRACE_ZONE_BEGIN(z0);
IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, file_offset);
IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, (int64_t)buffer_offset);
IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, (int64_t)length);
// EXPERIMENTAL: today only memory files. This should be on the file vtable
// (if supported - not all implementations need to support it).
iree_hal_memory_file_t* file = (iree_hal_memory_file_t*)base_file;
// Copy from the staging buffer to the file contents.
iree_byte_span_t file_contents = file->storage->contents;
iree_status_t status = iree_hal_buffer_map_read(
buffer, buffer_offset, file_contents.data + file_offset, length);
IREE_TRACE_ZONE_END(z0);
return status;
}