IREE-HAL中的Device Management

Semaphore

信号量是多线程环境下一种常见的同步机制,常用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前线程必须获取一个信号量,在关键代码段完成时线程必须释放信号量。其他想进入该关键代码段的线程,必须等待直到有线程释放信号量。信号量有4种操作,建立、等待、发信号、清理。

IREE中只定义了时间线信号量,和Vulkan中的时间线信号量类似,为主机到设备、设备到主机、主机到主机、设备到设备之间的通知提供同步机制。有效载荷设计为单调增长的uint64,在所有之前的命令都完成后,信号量会更新为给定的新值,但主机唤醒的时机由实现决定。

信号量的一个用途是管理资源的生命周期,在信号量发出信号前,同一批次的所有资源都被认为是活跃的;另一个用途是设备与设备间同步,在跨队列提交之间建立Command Buffer的有向无环图,从而不唤醒主机,直接在设备上进行调度。

调用者可以通过create、retain、release、destroy操作创建、保持、释放、销毁一个semaphore对象。信号量对象需要提供query、signal、wait和fail四种操作。在操作的实现中,会大量使用同步锁来保证正确性。考虑到载荷单调增长,每次query的结果至少要和上一次查询结果相等。

typedef struct iree_hal_semaphore_t iree_hal_semaphore_t;

IREE_API_EXPORT iree_status_t
iree_hal_semaphore_create(iree_hal_device_t* device, uint64_t initial_value,
                          iree_hal_semaphore_t** out_semaphore);

typedef struct iree_hal_semaphore_vtable_t {
  void(IREE_API_PTR* destroy)(iree_hal_semaphore_t* semaphore);
  iree_status_t(IREE_API_PTR* query)(iree_hal_semaphore_t* semaphore,
                                     uint64_t* out_value);
  iree_status_t(IREE_API_PTR* signal)(iree_hal_semaphore_t* semaphore,
                                      uint64_t new_value);
  void(IREE_API_PTR* fail)(iree_hal_semaphore_t* semaphore,
                           iree_status_t status);
  iree_status_t(IREE_API_PTR* wait)(iree_hal_semaphore_t* semaphore,
                                    uint64_t value, iree_timeout_t timeout);
} iree_hal_semaphore_vtable_t;

Device

Device是对整个设备的抽象,层级比其他具体的组件更高,为设备的所有操作提供了接口。与其他Resource不同,设备对象由Driver创建,因此没有为调用者提供create原语,但提供了retain、release、destroy用于主动管理它的生命周期。

typedef struct iree_hal_device_t iree_hal_device_t;

IREE_API_EXPORT void iree_hal_device_retain(iree_hal_device_t* device);
IREE_API_EXPORT void iree_hal_device_release(iree_hal_device_t* device);
IREE_API_EXPORT void iree_hal_device_destroy(iree_hal_device_t* device);

Device Vtable

列举了注册到HAL的设备需要实现哪些运行时的基本功能,包括查询设备信息、内存分配、绑定资源、同步和阻塞、数据搬运、任务提交等。其中query_i32大致等同于'sysconf'。

typedef struct iree_hal_device_vtable_t {
  void(IREE_API_PTR* destroy)(...);
  iree_string_view_t(IREE_API_PTR* id)(...);
  iree_allocator_t(IREE_API_PTR* host_allocator)(...);
  iree_hal_allocator_t*(IREE_API_PTR* device_allocator)(...);
  iree_status_t(IREE_API_PTR* trim)(...);
  iree_status_t(IREE_API_PTR* query_i32)(...);
  iree_status_t(IREE_API_PTR* create_command_buffer)(...);
  iree_status_t(IREE_API_PTR* create_descriptor_set)(...);
  iree_status_t(IREE_API_PTR* create_descriptor_set_layout)(...);
  iree_status_t(IREE_API_PTR* create_event)(...);
  iree_status_t(IREE_API_PTR* create_executable_cache)(...);
  iree_status_t(IREE_API_PTR* create_executable_layout)(...);
  iree_status_t(IREE_API_PTR* create_semaphore)(...);
  iree_status_t(IREE_API_PTR* transfer_range)(...);
  iree_status_t(IREE_API_PTR* queue_submit)(...);
  iree_status_t(IREE_API_PTR* submit_and_wait)(...);
  iree_status_t(IREE_API_PTR* wait_semaphores)(...);
  iree_status_t(IREE_API_PTR* wait_idle)(...);
} iree_hal_device_vtable_t;

Enumerated HAL Device

运行时系统要提供对设备query的能力,HAL旨在做一层统一硬件抽象,因此需要标识不同设备。在device info中,device_id是一个不透明的、驱动特定的句柄,

typedef struct iree_hal_device_info_t {
  iree_hal_device_id_t device_id;
  iree_string_view_t name;
} iree_hal_device_info_t;

typedef uintptr_t iree_hal_device_id_t;

// Example
CUdevice device;
out_device_info->device_id = (iree_hal_device_id_t)device;
iree_string_view_append_to_buffer(
    device_name_string, &out_device_info->name, (char*)buffer_ptr);

除了设备信息,还需要描述设备支持的特性。一些特性可能会禁用运行时优化,或者需要编译选项(和iree_hal_executable_caching_mode_t对应)来确保可执行文件中携带需要的metadata。

enum iree_hal_device_features_bits_t {
  IREE_HAL_DEVICE_FEATURE_NONE = 0u,
  IREE_HAL_DEVICE_FEATURE_SUPPORTS_DEBUGGING = 1u << 0,
  IREE_HAL_DEVICE_FEATURE_SUPPORTS_COVERAGE = 1u << 1,
  IREE_HAL_DEVICE_FEATURE_SUPPORTS_PROFILING = 1u << 2,
};
  • 位0 设备支持调试
  • 位1 设备支持Executable覆盖信息
  • 位2 设备支持对Executable和Command Queue分析

Transfer Buffer

为了实现各种搬运操作,IREE中提供了transfer_buffer结构,将主机分配的不透明主机缓冲区指针和设备缓冲区指针打包在一起,设备缓冲区可以是任意内存类型的。通过make*函数可以为搬运操作准备缓冲区。

typedef struct iree_hal_transfer_buffer_t {
  iree_byte_span_t host_buffer;
  iree_hal_buffer_t* device_buffer;
} iree_hal_transfer_buffer_t;

static inline iree_hal_transfer_buffer_t
    iree_hal_make_host_transfer_buffer(iree_byte_span_t host_buffer);
static inline iree_hal_transfer_buffer_t
    iree_hal_make_host_transfer_buffer_span(void* ptr, iree_host_size_t length);
static inline iree_hal_transfer_buffer_t
    iree_hal_make_device_transfer_buffer(iree_hal_buffer_t* device_buffer);

为了兼容性,IREE声明了transfer_range这个同步函数由调用者直接执行搬运,用以支持显式的h2d、d2h、d2d操作,同时也会被Buffer类型支持的特定设备调用。同步搬运存在很大隐患,一方面可能会导致一些临时分配和复制,另一方面对源设备或目标设备上的队列执行,操作的执行时机不确定,可能需要完整的设备刷新之后执行导致额外的缓存刷新,也可能立即执行触发同步阻塞。

IREE_API_EXPORT iree_status_t iree_hal_device_transfer_range(
    iree_hal_device_t* device, iree_hal_transfer_buffer_t source,
    iree_device_size_t source_offset, iree_hal_transfer_buffer_t target,
    iree_device_size_t target_offset, iree_device_size_t data_length,
    iree_hal_transfer_buffer_flags_t flags, iree_timeout_t timeout);

更好的方式是Transfer操作在搬运队列中执行,调用iree_hal_create_transfer_command_buffer创建搬运队列并以正常的提交方式提交,可以细粒度的排序并通过并发有效地进行批处理,分摊提交成本。transfer_and_wait封装了整个过程,对外暴露的行为是同步执行一个或多个传输操作。但是它是一个阻塞操作,会产生大量开销,仅在需要它的实现或在不担心性能的情况下作为后备。

IREE_API_EXPORT iree_status_t iree_hal_device_transfer_and_wait(
    iree_hal_device_t* device, iree_hal_semaphore_t* wait_semaphore,
    uint64_t wait_value, iree_host_size_t transfer_count,
    const iree_hal_transfer_command_t* transfer_commands,
    iree_timeout_t timeout);
/*
  iree_hal_semaphore_query
  iree_hal_create_transfer_command_buffer
  iree_hal_semaphore_create
  iree_hal_device_submit_and_wait
*/

Submission

提交行为和Vulkan类似,允许批次处理。批次间按照定义的顺序执行其携带的Command Buffers,同时允许一个批次内的Command Buffer无序完成。

VulkanIREE
vkQueueSubmitiree_hal_device_queue_submit
VkSubmitInfoiree_hal_submission_batch_t

iree_hal_submission_batch_t封装了提交到device queue的一组command buffers以及wait和signal信号量列表。

typedef struct iree_hal_submission_batch_t {
  iree_hal_semaphore_list_t wait_semaphores;
  iree_host_size_t command_buffer_count;
  iree_hal_command_buffer_t** command_buffers;
  iree_hal_semaphore_list_t signal_semaphores;
} iree_hal_submission_batch_t;

typedef struct iree_hal_semaphore_list_t {
  iree_host_size_t count;
  iree_hal_semaphore_t** semaphores;
  uint64_t* payload_values;
} iree_hal_semaphore_list_t;

semaphore_list中包含了信号量列表机器对应的有效载荷,

  • signal 每个信号量都将设置为新的载荷值
  • wait 每个信号量都必须到达或者超过有效载荷

批处理前,所有的wait_semaphores必须符合等待要求。Command Buffers会按照在列表中的顺序执行,但在实际执行中是并发的。如果Command Buffers之间存在依赖关系,需要通过Event进行内部同步。所有处理完成后,将处罚signal_semaphores的signal操作。

iree_hal_device_queue_submit用于将一个或多个批次工作提交到设备队列中。首先要根据指定的命令类型和队列亲和位图选择合适的设备队列。可用队列的数量会实时改变,queue_affinity可用于将Command哈希到指定类型的可用队列,用来表明多个同类提交是否必须放在同一个队列。具体的哈希函数由实现定义,例如Vulkan中用取余的方式进行选择,如果两个队列支持搬运命令,亲和度是5,那么结果队列hash(5)=1。

IREE_API_EXPORT iree_status_t iree_hal_device_queue_submit(
    iree_hal_device_t* device, iree_hal_command_category_t command_categories,
    iree_hal_queue_affinity_t queue_affinity, iree_host_size_t batch_count,
    const iree_hal_submission_batch_t* batches);

// Example
static CommandQueue* iree_hal_vulkan_device_select_queue(...) {
  return device->transfer_queues[queue_affinity % device->transfer_queue_count];
}

iree_hal_device_submit_and_wait是一种控制执行时序的手段,等效于iree_hal_device_queue_submit + iree_hal_semaphore_wait,提交一批次工作后立即阻塞,对于产生的overhead可能可以通过阻止线程唤醒、kernel调用和内部tracking帮助减少。

IREE_API_EXPORT iree_status_t iree_hal_device_submit_and_wait(
    iree_hal_device_t* device, iree_hal_command_category_t command_categories,
    iree_hal_queue_affinity_t queue_affinity, iree_host_size_t batch_count,
    const iree_hal_submission_batch_t* batches,
    iree_hal_semaphore_t* wait_semaphore, uint64_t wait_value,
    iree_timeout_t timeout);

Wait

这里提供了两种阻塞方式,信号量阻塞和时间阻塞。iree_hal_device_wait_semaphores可以阻塞调用者直到信号量满足要求,或者超时,信号量列表必须由同一个设备创建,等待模式决定了多等待操作的继续条件:等到全部或任一信号量。

IREE_API_EXPORT iree_status_t iree_hal_device_wait_semaphores(
    iree_hal_device_t* device, iree_hal_wait_mode_t wait_mode,
    const iree_hal_semaphore_list_t* semaphore_list, iree_timeout_t timeout);

typedef enum iree_hal_wait_mode_e {
  IREE_HAL_WAIT_MODE_ALL = 0,
  IREE_HAL_WAIT_MODE_ANY = 1,
} iree_hal_wait_mode_t;

iree_hal_device_wait_idle会阻塞调用者直到设备空闲,所有队列上的所有请求完成或是超时。等同于等待全部信号量,如果新工作由另一个线程提交,那么在此调用返回前可能不会被阻塞。

IREE_API_EXPORT iree_status_t
iree_hal_device_wait_idle(iree_hal_device_t* device, iree_timeout_t timeout);

Driver

驱动主要用于设备管理,要提供查询和创建设备的基本功能。遵循HAL的整体设计,可以通过retain、release和destroy接口管理驱动对象的生命周期。

typedef struct iree_hal_driver_t iree_hal_driver_t;

IREE_API_EXPORT void iree_hal_driver_retain(iree_hal_driver_t* driver);
IREE_API_EXPORT void iree_hal_driver_release(iree_hal_driver_t* driver);
IREE_API_EXPORT void iree_hal_driver_destroy(iree_hal_driver_t* driver);

Driver Info

iree_hal_driver_info_t描述了一个驱动的身份信息,其中driver id是一个唯一的不透明句柄,用于标识不同的驱动。对这个结构的引用的生命周期取决于如何调用,

  • iree_hal_driver_registry_enumerate,driver info会被拷贝到调用者持有的内存
  • iree_hal_driver_info查询一个alive的驱动时,内存只会在驱动存活期间保持
  • 通过驱动工厂枚举时,driver info只有在驱动注册锁之间有效
typedef struct iree_hal_driver_info_t {
  iree_hal_driver_id_t driver_id;
  iree_string_view_t driver_name;
  iree_string_view_t full_name;
} iree_hal_driver_info_t;

typedef uint64_t iree_hal_driver_id_t;

// Example
static const iree_hal_driver_info_t driver_infos[1] = {{
   .driver_id = IREE_HAL_CUDA_DRIVER_ID,
   .driver_name = iree_string_view_literal("cuda"),
   .full_name = iree_string_view_literat("CUDA dynamic");
}};

Driver Factory

驱动管理的设计思想采用了简单的“工厂模式”。通过注册接口将特定设备的驱动工厂注册,在创建时用工厂创建合适的驱动,进而创建合适的设备。

驱动工厂用于枚举、创建驱动。多数情况下,可以静态知道驱动可用,工厂将存在于只读数据段,当基于配置动态驱动动态可用时,工厂发现并在枚举期创建一个驱动实例;而对于延迟加载的驱动程序,例如从动态库或通过rpc实现的驱动程序,则需要大量的setup时间,工厂需要先推测性枚举,在用户明确之后,通过尝试创建实际执行。

typedef struct iree_hal_driver_factory_t {
  void* self;
  iree_status_t(IREE_API_PTR* enumerate)(
      void* self, const iree_hal_driver_info_t** out_driver_infos,
      iree_host_size_t* out_driver_info_count);
  iree_status_t(IREE_API_PTR& try_create)(void* self,
                                          iree_hal_driver_id_t driver_id,
                                          iree_allocator_t allocator,
                                          iree_hal_driver_t** out_driver);
} iree_hal_driver_factory_t;
  • 枚举:查询工厂提供的可用驱动列表,返回一个对工厂数据结构的引用。实现必须保证他们的工厂枚举结果在注册期间不可变。,如果工厂要改变其枚举设备集,那么必须先取消注册,然后重新注册
  • 尝试创建:尝试创建驱动,枚举和创建之间,可能花费大量时间,驱动注册锁可能释放

Driver Registry

驱动注册表用来管理成功注册的工厂。在设计上,为MAX COUNT设固定值是为了避免动态分配。

typedef struct iree_hal_driver_registry_t iree_hal_driver_registry_t;

#define IREE_HAL_MAX_DRIVER_FACTORY_COUNT 8

struct iree_hal_driver_registry_t {
  iree_allocator_t host_allocator;
  iree_slim_mutex_t mutex;
  iree_host_size_t factory_count;
  const iree_hal_driver_factory_t* factories[IREE_HAL_MAX_DRIVER_FACTORY_COUNT];
}

每个进程内存在一个默认的注册表,可以调用iree_hal_driver_registry_default获取它的指针。而当更复杂的应用程序希望严格地控制驱动程序对调用者可见性时,例如为不同用户的请求选择相应的注册表处理,或希望手动管理表的生命周期时,可以通过iree_hal_driver_registry_allocate/iree_hal_driver_registry_free创建/析构一个驱动注册表。

获取到一个注册表实例后,可以向注册表中注册或取消注册一个驱动工厂。iree_hal_driver_registry_register_factory/iree_hal_driver_registry_unregister_factory都是线程安全的,当一个工厂取消注册后,注册表中将会删除,不能再创建对应的新的驱动,但已有的驱动可能仍然存活。

调用iree_hal_driver_registry_enumerate将以列表形式枚举全部的驱动信息,这是一个超集,可能只有其中部分驱动能够成功被创建。创建驱动可以通过driver id也可以通过名字,当不同工厂提供的驱动重名时,最好用driver id,而按名字创建时会按照最常添加的顺序搜索工厂。

Create Device

驱动要提供查询可用设备以及创建设备的方法。除了根据唯一标识device id创建设备对象,还提供了不需要提供标识创建默认设备的方法。

typedef struct iree_hal_driver_vtable_t {
  void(IREE_API_PTR* destroy)(...);
  iree_status_t(IREE_API_PTR* query_available_devices)(...);
  iree_status_t(IREE_API_PTR* create_device)(...);
} iree_hal_driver_vtable_t;

IREE_API_EXPORT iree_status_t iree_hal_driver_create_device(
    iree_hal_driver_t* driver, iree_hal_device_id_t device_id,
    iree_allocator_t allocator, iree_hal_device_t** out_device);

IREE_API_EXPORT iree_status_t iree_hal_driver_create_device_default(
    iree_hal_driver_t* driver, iree_allocator_t allocator,
    iree_hal_device_t** out_device);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值