[hip] Re-land #19082 and #19074 (#19101)

Fixes an issue with the async allocation/deallocation where we did not
issue an execution to kick off the worker thread. Depending on workload
ordering this sometimes could cause a deadlock.

---------

Signed-off-by: Andrew Woloszyn <andrew.woloszyn@gmail.com>
diff --git a/runtime/src/iree/hal/drivers/cuda/cuda_device.c b/runtime/src/iree/hal/drivers/cuda/cuda_device.c
index 2ed014b..d5ed448 100644
--- a/runtime/src/iree/hal/drivers/cuda/cuda_device.c
+++ b/runtime/src/iree/hal/drivers/cuda/cuda_device.c
@@ -1171,6 +1171,8 @@
             iree_hal_cuda_deferred_work_queue_device_interface_create_stream_command_buffer,
         .submit_command_buffer =
             iree_hal_cuda_deferred_work_queue_device_interface_submit_command_buffer,
+        .async_alloc = NULL,
+        .async_dealloc = NULL,
 };
 
 static const iree_hal_stream_tracing_device_interface_vtable_t
diff --git a/runtime/src/iree/hal/drivers/hip/dynamic_symbol_tables.h b/runtime/src/iree/hal/drivers/hip/dynamic_symbol_tables.h
index 1a3fc50..963784d 100644
--- a/runtime/src/iree/hal/drivers/hip/dynamic_symbol_tables.h
+++ b/runtime/src/iree/hal/drivers/hip/dynamic_symbol_tables.h
@@ -73,6 +73,7 @@
                                hipMemPool_t, hipStream_t)
 IREE_HAL_HIP_REQUIRED_PFN_DECL(hipMallocManaged, hipDeviceptr_t *, size_t,
                                unsigned int)
+IREE_HAL_HIP_REQUIRED_PFN_DECL(hipMallocAsync, void **, size_t, hipStream_t)
 IREE_HAL_HIP_REQUIRED_PFN_DECL(hipMemcpy, void *, const void *, size_t,
                                hipMemcpyKind)
 IREE_HAL_HIP_REQUIRED_PFN_DECL(hipMemcpyAsync, void *, const void *, size_t,
diff --git a/runtime/src/iree/hal/drivers/hip/hip_allocator.c b/runtime/src/iree/hal/drivers/hip/hip_allocator.c
index 1073ccc..041e011 100644
--- a/runtime/src/iree/hal/drivers/hip/hip_allocator.c
+++ b/runtime/src/iree/hal/drivers/hip/hip_allocator.c
@@ -111,6 +111,10 @@
   IREE_TRACE_ZONE_END(z0);
 }
 
+bool iree_hal_hip_allocator_isa(iree_hal_allocator_t* base_value) {
+  return iree_hal_resource_is(base_value, &iree_hal_hip_allocator_vtable);
+}
+
 static iree_allocator_t iree_hal_hip_allocator_host_allocator(
     const iree_hal_allocator_t* IREE_RESTRICT base_allocator) {
   iree_hal_hip_allocator_t* allocator =
@@ -590,6 +594,53 @@
   }
 }
 
+iree_status_t iree_hal_hip_allocator_alloc_async(
+    iree_hal_allocator_t* base_allocator, hipStream_t stream,
+    iree_hal_buffer_t* buffer) {
+  iree_hal_hip_allocator_t* allocator =
+      iree_hal_hip_allocator_cast(base_allocator);
+
+  hipDeviceptr_t ptr = NULL;
+  iree_status_t status = IREE_HIP_RESULT_TO_STATUS(
+      allocator->symbols,
+      hipMallocAsync(&ptr, (size_t)iree_hal_buffer_allocation_size(buffer),
+                     stream),
+      "hipMallocAsync");
+  if (iree_status_is_ok(status)) {
+    iree_hal_hip_buffer_set_device_pointer(buffer, ptr);
+    IREE_TRACE_ALLOC_NAMED(IREE_HAL_HIP_ALLOCATOR_ID, (void*)ptr,
+                           iree_hal_buffer_allocation_size(buffer));
+    IREE_STATISTICS(iree_hal_allocator_statistics_record_alloc(
+        &allocator->statistics, iree_hal_buffer_memory_type(buffer),
+        iree_hal_buffer_allocation_size(buffer)));
+  } else {
+    iree_hal_hip_buffer_set_allocation_empty(buffer);
+  }
+
+  return status;
+}
+
+iree_status_t iree_hal_hip_allocator_free_async(
+    iree_hal_allocator_t* base_allocator, hipStream_t stream,
+    iree_hal_buffer_t* buffer) {
+  iree_hal_hip_allocator_t* allocator =
+      iree_hal_hip_allocator_cast(base_allocator);
+  hipDeviceptr_t device_ptr = iree_hal_hip_buffer_device_pointer(buffer);
+  if (!device_ptr) {
+    return iree_ok_status();
+  }
+
+  IREE_RETURN_IF_ERROR(IREE_HIP_RESULT_TO_STATUS(
+      allocator->symbols, hipFreeAsync(device_ptr, stream), "hipFreeAsync"));
+  iree_hal_hip_buffer_set_allocation_empty(buffer);
+
+  IREE_TRACE_FREE_NAMED(IREE_HAL_HIP_ALLOCATOR_ID, (void*)device_ptr);
+  IREE_STATISTICS(iree_hal_allocator_statistics_record_free(
+      &allocator->statistics, iree_hal_buffer_memory_type(buffer),
+      iree_hal_buffer_allocation_size(buffer)));
+  return iree_ok_status();
+}
+
 static const iree_hal_allocator_vtable_t iree_hal_hip_allocator_vtable = {
     .destroy = iree_hal_hip_allocator_destroy,
     .host_allocator = iree_hal_hip_allocator_host_allocator,
diff --git a/runtime/src/iree/hal/drivers/hip/hip_allocator.h b/runtime/src/iree/hal/drivers/hip/hip_allocator.h
index 6d51060..bb83ea1 100644
--- a/runtime/src/iree/hal/drivers/hip/hip_allocator.h
+++ b/runtime/src/iree/hal/drivers/hip/hip_allocator.h
@@ -25,6 +25,16 @@
     hipStream_t stream, iree_hal_hip_memory_pools_t* pools,
     iree_allocator_t host_allocator, iree_hal_allocator_t** out_allocator);
 
+bool iree_hal_hip_allocator_isa(iree_hal_allocator_t* base_value);
+
+iree_status_t iree_hal_hip_allocator_alloc_async(
+    iree_hal_allocator_t* base_allocator, hipStream_t stream,
+    iree_hal_buffer_t* buffer);
+
+iree_status_t iree_hal_hip_allocator_free_async(iree_hal_allocator_t* allocator,
+                                                hipStream_t stream,
+                                                iree_hal_buffer_t* buffer);
+
 #ifdef __cplusplus
 }  // extern "C"
 #endif  // __cplusplus
diff --git a/runtime/src/iree/hal/drivers/hip/hip_buffer.c b/runtime/src/iree/hal/drivers/hip/hip_buffer.c
index fb318ba..46e768a 100644
--- a/runtime/src/iree/hal/drivers/hip/hip_buffer.c
+++ b/runtime/src/iree/hal/drivers/hip/hip_buffer.c
@@ -11,6 +11,7 @@
 #include <string.h>
 
 #include "iree/base/api.h"
+#include "iree/base/internal/synchronization.h"
 #include "iree/base/tracing.h"
 
 typedef struct iree_hal_hip_buffer_t {
@@ -19,6 +20,9 @@
   void* host_ptr;
   hipDeviceptr_t device_ptr;
   iree_hal_buffer_release_callback_t release_callback;
+  iree_slim_mutex_t device_ptr_lock;
+  iree_notification_t device_ptr_notification;
+  bool empty;
 } iree_hal_hip_buffer_t;
 
 static const iree_hal_buffer_vtable_t iree_hal_hip_buffer_vtable;
@@ -65,6 +69,9 @@
     buffer->host_ptr = host_ptr;
     buffer->device_ptr = device_ptr;
     buffer->release_callback = release_callback;
+    buffer->empty = false;
+    iree_slim_mutex_initialize(&buffer->device_ptr_lock);
+    iree_notification_initialize(&buffer->device_ptr_notification);
     *out_buffer = &buffer->base;
   }
 
@@ -72,6 +79,26 @@
   return status;
 }
 
+void iree_hal_hip_buffer_set_device_pointer(iree_hal_buffer_t* base_buffer,
+                                            hipDeviceptr_t pointer) {
+  iree_hal_hip_buffer_t* buffer = iree_hal_hip_buffer_cast(base_buffer);
+  IREE_ASSERT(buffer->device_ptr == NULL,
+              "Cannot set a device_ptr to a buffer that already has one");
+  iree_slim_mutex_lock(&buffer->device_ptr_lock);
+  buffer->device_ptr = pointer;
+  iree_slim_mutex_unlock(&buffer->device_ptr_lock);
+  iree_notification_post(&buffer->device_ptr_notification, IREE_ALL_WAITERS);
+}
+
+void iree_hal_hip_buffer_set_allocation_empty(iree_hal_buffer_t* base_buffer) {
+  iree_hal_hip_buffer_t* buffer = iree_hal_hip_buffer_cast(base_buffer);
+  iree_slim_mutex_lock(&buffer->device_ptr_lock);
+  buffer->empty = true;
+  buffer->device_ptr = NULL;
+  iree_slim_mutex_unlock(&buffer->device_ptr_lock);
+  iree_notification_post(&buffer->device_ptr_notification, IREE_ALL_WAITERS);
+}
+
 static void iree_hal_hip_buffer_destroy(iree_hal_buffer_t* base_buffer) {
   iree_hal_hip_buffer_t* buffer = iree_hal_hip_buffer_cast(base_buffer);
   iree_allocator_t host_allocator = base_buffer->host_allocator;
@@ -80,6 +107,8 @@
     buffer->release_callback.fn(buffer->release_callback.user_data,
                                 base_buffer);
   }
+  iree_slim_mutex_deinitialize(&buffer->device_ptr_lock);
+  iree_notification_deinitialize(&buffer->device_ptr_notification);
   iree_allocator_free(host_allocator, buffer);
   IREE_TRACE_ZONE_END(z0);
 }
@@ -143,10 +172,20 @@
   return buffer->type;
 }
 
+static bool iree_hal_hip_buffer_has_device_ptr(void* arg) {
+  iree_hal_hip_buffer_t* buffer = (iree_hal_hip_buffer_t*)arg;
+  iree_slim_mutex_lock(&buffer->device_ptr_lock);
+  bool has_ptr_or_error = buffer->device_ptr || buffer->empty;
+  iree_slim_mutex_unlock(&buffer->device_ptr_lock);
+  return has_ptr_or_error;
+}
+
 hipDeviceptr_t iree_hal_hip_buffer_device_pointer(
-    const iree_hal_buffer_t* base_buffer) {
-  const iree_hal_hip_buffer_t* buffer =
-      iree_hal_hip_buffer_const_cast(base_buffer);
+    iree_hal_buffer_t* base_buffer) {
+  iree_hal_hip_buffer_t* buffer = iree_hal_hip_buffer_cast(base_buffer);
+  iree_notification_await(&buffer->device_ptr_notification,
+                          iree_hal_hip_buffer_has_device_ptr, buffer,
+                          iree_infinite_timeout());
   return buffer->device_ptr;
 }
 
diff --git a/runtime/src/iree/hal/drivers/hip/hip_buffer.h b/runtime/src/iree/hal/drivers/hip/hip_buffer.h
index 8fe1609..4264b54 100644
--- a/runtime/src/iree/hal/drivers/hip/hip_buffer.h
+++ b/runtime/src/iree/hal/drivers/hip/hip_buffer.h
@@ -49,8 +49,16 @@
 // Returns the HIP base pointer for the given |buffer|.
 // This is the entire allocated_buffer and must be offset by the buffer
 // byte_offset and byte_length when used.
-hipDeviceptr_t iree_hal_hip_buffer_device_pointer(
-    const iree_hal_buffer_t* buffer);
+hipDeviceptr_t iree_hal_hip_buffer_device_pointer(iree_hal_buffer_t* buffer);
+
+// Sets the HIP base pointer for the given |buffer|.
+// This is the entire allocated_buffer and must be offset by the buffer
+// byte_offset and byte_length when used.
+void iree_hal_hip_buffer_set_device_pointer(iree_hal_buffer_t* buffer,
+                                            hipDeviceptr_t pointer);
+
+// Marks the buffer as having an intentionally empty allocation.
+void iree_hal_hip_buffer_set_allocation_empty(iree_hal_buffer_t* buffer);
 
 // Returns the HIP host pointer for the given |buffer|, if available.
 void* iree_hal_hip_buffer_host_pointer(const iree_hal_buffer_t* buffer);
diff --git a/runtime/src/iree/hal/drivers/hip/hip_device.c b/runtime/src/iree/hal/drivers/hip/hip_device.c
index 7f42e8d..e48b524 100644
--- a/runtime/src/iree/hal/drivers/hip/hip_device.c
+++ b/runtime/src/iree/hal/drivers/hip/hip_device.c
@@ -19,6 +19,7 @@
 #include "iree/hal/drivers/hip/event_semaphore.h"
 #include "iree/hal/drivers/hip/graph_command_buffer.h"
 #include "iree/hal/drivers/hip/hip_allocator.h"
+#include "iree/hal/drivers/hip/hip_buffer.h"
 #include "iree/hal/drivers/hip/memory_pools.h"
 #include "iree/hal/drivers/hip/nop_executable_cache.h"
 #include "iree/hal/drivers/hip/rccl_channel.h"
@@ -87,6 +88,9 @@
   iree_hal_channel_provider_t* channel_provider;
 } iree_hal_hip_device_t;
 
+static iree_hal_hip_device_t* iree_hal_hip_device_cast(
+    iree_hal_device_t* base_value);
+
 static const iree_hal_device_vtable_t iree_hal_hip_device_vtable;
 static const iree_hal_deferred_work_queue_device_interface_vtable_t
     iree_hal_hip_deferred_work_queue_device_interface_vtable;
@@ -166,6 +170,7 @@
   return IREE_HIP_RESULT_TO_STATUS(device_interface->hip_symbols,
                                    hipEventSynchronize((hipEvent_t)event));
 }
+
 static iree_status_t
 iree_hal_hip_deferred_work_queue_device_interface_destroy_native_event(
     iree_hal_deferred_work_queue_device_interface_t* base_device_interface,
@@ -256,6 +261,45 @@
   return status;
 }
 
+static iree_status_t
+iree_hal_hip_deferred_work_queue_device_interface_async_alloc(
+    iree_hal_deferred_work_queue_device_interface_t* base_device_interface,
+    iree_hal_buffer_t* buffer) {
+  iree_hal_hip_deferred_work_queue_device_interface_t* device_interface =
+      (iree_hal_hip_deferred_work_queue_device_interface_t*)
+          base_device_interface;
+  iree_hal_hip_device_t* device =
+      iree_hal_hip_device_cast(device_interface->device);
+  if (device->supports_memory_pools) {
+    return iree_hal_hip_memory_pools_allocate_pointer(
+        &device->memory_pools, buffer, device->hip_dispatch_stream,
+        iree_hal_buffer_allocation_size(buffer));
+  }
+
+  return iree_hal_hip_allocator_alloc_async(
+      iree_hal_device_allocator(device_interface->device),
+      device->hip_dispatch_stream, buffer);
+}
+
+// Asynchronously frees a buffer.
+static iree_status_t
+iree_hal_hip_deferred_work_queue_device_interface_async_dealloc(
+    iree_hal_deferred_work_queue_device_interface_t* base_device_interface,
+    iree_hal_buffer_t* buffer) {
+  iree_hal_hip_deferred_work_queue_device_interface_t* device_interface =
+      (iree_hal_hip_deferred_work_queue_device_interface_t*)
+          base_device_interface;
+  iree_hal_hip_device_t* device =
+      iree_hal_hip_device_cast(device_interface->device);
+  if (device->supports_memory_pools) {
+    return iree_hal_hip_memory_pools_deallocate(
+        &device->memory_pools, device->hip_dispatch_stream, buffer);
+  }
+  return iree_hal_hip_allocator_free_async(
+      iree_hal_device_allocator(device_interface->device),
+      device->hip_dispatch_stream, buffer);
+}
+
 typedef struct iree_hal_hip_tracing_device_interface_t {
   iree_hal_stream_tracing_device_interface_t base;
   hipDevice_t device;
@@ -922,6 +966,34 @@
   return IREE_HAL_SEMAPHORE_COMPATIBILITY_HOST_ONLY;
 }
 
+static iree_status_t iree_hal_hip_device_pepare_async_alloc(
+    iree_hal_hip_device_t* device, iree_hal_buffer_params_t params,
+    iree_device_size_t allocation_size,
+    iree_hal_buffer_t** IREE_RESTRICT out_buffer) {
+  IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_TRACE_ZONE_APPEND_VALUE_I64(z0, (int64_t)allocation_size);
+
+  iree_hal_buffer_params_canonicalize(&params);
+
+  iree_hal_buffer_t* buffer = NULL;
+  iree_status_t status = iree_hal_hip_buffer_wrap(
+      device->device_allocator, params.type, params.access, params.usage,
+      allocation_size, /*byte_offset=*/0,
+      /*byte_length=*/allocation_size, IREE_HAL_HIP_BUFFER_TYPE_ASYNC,
+      /*device_ptr=*/NULL, /*host_ptr=*/NULL,
+      iree_hal_buffer_release_callback_null(), device->host_allocator, &buffer);
+
+  if (iree_status_is_ok(status)) {
+    *out_buffer = buffer;
+  } else if (buffer) {
+    iree_hal_hip_buffer_set_allocation_empty(buffer);
+    iree_hal_buffer_release(buffer);
+  }
+
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
 // TODO: implement multiple streams; today we only have one and queue_affinity
 //       is ignored.
 // TODO: implement proper semaphores in HIP to ensure ordering and avoid
@@ -935,6 +1007,49 @@
     iree_hal_buffer_t** IREE_RESTRICT out_buffer) {
   iree_hal_hip_device_t* device = iree_hal_hip_device_cast(base_device);
 
+  if (device->supports_memory_pools &&
+      !iree_all_bits_set(params.type, IREE_HAL_MEMORY_TYPE_HOST_VISIBLE)) {
+    iree_hal_buffer_t* buffer = NULL;
+
+    IREE_RETURN_IF_ERROR(iree_hal_hip_memory_pools_prepare_buffer(
+        &device->memory_pools, device->hip_dispatch_stream, pool, params,
+        allocation_size, &buffer));
+
+    iree_status_t status = iree_hal_deferred_work_queue_enqueue_alloc(
+        device->work_queue, wait_semaphore_list, signal_semaphore_list, buffer);
+    if (iree_status_is_ok(status)) {
+      status = iree_hal_deferred_work_queue_issue(device->work_queue);
+    }
+    if (iree_status_is_ok(status)) {
+      *out_buffer = buffer;
+    } else {
+      iree_hal_hip_buffer_set_allocation_empty(buffer);
+      iree_hal_resource_release(&buffer->resource);
+    }
+    return status;
+  } else if (!iree_all_bits_set(params.type,
+                                IREE_HAL_MEMORY_TYPE_HOST_VISIBLE) &&
+             iree_hal_hip_allocator_isa(
+                 iree_hal_device_allocator(base_device))) {
+    iree_hal_buffer_t* buffer = NULL;
+
+    IREE_RETURN_IF_ERROR(iree_hal_hip_device_pepare_async_alloc(
+        device, params, allocation_size, &buffer));
+
+    iree_status_t status = iree_hal_deferred_work_queue_enqueue_alloc(
+        device->work_queue, wait_semaphore_list, signal_semaphore_list, buffer);
+    if (iree_status_is_ok(status)) {
+      status = iree_hal_deferred_work_queue_issue(device->work_queue);
+    }
+    if (iree_status_is_ok(status)) {
+      *out_buffer = buffer;
+    } else {
+      iree_hal_hip_buffer_set_allocation_empty(buffer);
+      iree_hal_resource_release(&buffer->resource);
+    }
+    return status;
+  }
+
   // NOTE: block on the semaphores here; we could avoid this by properly
   // sequencing device work with semaphores. The HIP HAL is not currently
   // asynchronous.
@@ -945,17 +1060,9 @@
   // exhaustion but the error may be deferred until a later synchronization.
   // If pools are not supported we allocate a buffer as normal from whatever
   // allocator is set on the device.
-  iree_status_t status = iree_ok_status();
-  if (device->supports_memory_pools &&
-      !iree_all_bits_set(params.type, IREE_HAL_MEMORY_TYPE_HOST_VISIBLE)) {
-    status = iree_hal_hip_memory_pools_allocate(
-        &device->memory_pools, device->hip_dispatch_stream, pool, params,
-        allocation_size, out_buffer);
-  } else {
-    status = iree_hal_allocator_allocate_buffer(
-        iree_hal_device_allocator(base_device), params, allocation_size,
-        out_buffer);
-  }
+  iree_status_t status =
+      iree_hal_allocator_allocate_buffer(iree_hal_device_allocator(base_device),
+                                         params, allocation_size, out_buffer);
 
   // Only signal if not returning a synchronous error - synchronous failure
   // indicates that the stream is unchanged (it's not really since we waited
@@ -976,6 +1083,14 @@
     const iree_hal_semaphore_list_t signal_semaphore_list,
     iree_hal_buffer_t* buffer) {
   iree_hal_hip_device_t* device = iree_hal_hip_device_cast(base_device);
+  if (iree_hal_hip_allocator_isa(iree_hal_device_allocator(base_device))) {
+    iree_status_t status = iree_hal_deferred_work_queue_enqueue_dealloc(
+        device->work_queue, wait_semaphore_list, signal_semaphore_list, buffer);
+    if (iree_status_is_ok(status)) {
+      status = iree_hal_deferred_work_queue_issue(device->work_queue);
+    }
+    return status;
+  }
 
   // NOTE: block on the semaphores here; we could avoid this by properly
   // sequencing device work with semaphores. The HIP HAL is not currently
@@ -1168,6 +1283,10 @@
             iree_hal_hip_deferred_work_queue_device_interface_create_stream_command_buffer,
         .submit_command_buffer =
             iree_hal_hip_deferred_work_queue_device_interface_submit_command_buffer,
+        .async_alloc =
+            iree_hal_hip_deferred_work_queue_device_interface_async_alloc,
+        .async_dealloc =
+            iree_hal_hip_deferred_work_queue_device_interface_async_dealloc,
 };
 
 static const iree_hal_stream_tracing_device_interface_vtable_t
diff --git a/runtime/src/iree/hal/drivers/hip/memory_pools.c b/runtime/src/iree/hal/drivers/hip/memory_pools.c
index 89e27fa..0258fa0 100644
--- a/runtime/src/iree/hal/drivers/hip/memory_pools.c
+++ b/runtime/src/iree/hal/drivers/hip/memory_pools.c
@@ -202,13 +202,41 @@
   IREE_TRACE_ZONE_BEGIN(z0);
 
   hipDeviceptr_t device_ptr = iree_hal_hip_buffer_device_pointer(buffer);
-  IREE_HIP_IGNORE_ERROR(pools->hip_symbols, hipFree(device_ptr));
+  if (device_ptr) {
+    IREE_HIP_IGNORE_ERROR(pools->hip_symbols, hipFree(device_ptr));
+  }
   iree_hal_hip_memory_pool_track_free(pools, buffer);
 
   IREE_TRACE_ZONE_END(z0);
 }
 
-iree_status_t iree_hal_hip_memory_pools_allocate(
+iree_status_t iree_hal_hip_memory_pools_allocate_pointer(
+    iree_hal_hip_memory_pools_t* pools, iree_hal_buffer_t* buffer,
+    hipStream_t stream, iree_device_size_t allocation_size) {
+  // TODO: more pools and better selection; this is coarsely deciding between
+  // only device local (variables, constants, transients) and other (staging,
+  // external) but could use more buffer properties (including usage/export
+  // flags) to better isolate the different usage patterns and keep the pools
+  // operating with reasonable limits. We should be using the |pool| arg.
+  hipMemPool_t memory_pool =
+      iree_all_bits_set(iree_hal_buffer_memory_type(buffer),
+                        IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL)
+          ? pools->device_local
+          : pools->other;
+
+  hipDeviceptr_t device_ptr = NULL;
+  IREE_RETURN_IF_ERROR(IREE_HIP_RESULT_TO_STATUS(
+      pools->hip_symbols,
+      hipMallocFromPoolAsync(&device_ptr, (size_t)allocation_size, memory_pool,
+                             stream),
+      "hipMallocFromPoolAsync"));
+
+  iree_hal_hip_buffer_set_device_pointer(buffer, device_ptr);
+  iree_hal_hip_memory_pool_track_alloc(pools, buffer);
+  return iree_ok_status();
+}
+
+iree_status_t iree_hal_hip_memory_pools_prepare_buffer(
     iree_hal_hip_memory_pools_t* pools, hipStream_t stream,
     iree_hal_allocator_pool_t pool, iree_hal_buffer_params_t params,
     iree_device_size_t allocation_size,
@@ -218,49 +246,27 @@
 
   iree_hal_buffer_params_canonicalize(&params);
 
-  // TODO: more pools and better selection; this is coarsely deciding between
-  // only device local (variables, constants, transients) and other (staging,
-  // external) but could use more buffer properties (including usage/export
-  // flags) to better isolate the different usage patterns and keep the pools
-  // operating with reasonable limits. We should be using the |pool| arg.
-  hipMemPool_t memory_pool =
-      iree_all_bits_set(params.type, IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL)
-          ? pools->device_local
-          : pools->other;
-
-  hipDeviceptr_t device_ptr = NULL;
-  iree_status_t status = IREE_HIP_RESULT_TO_STATUS(
-      pools->hip_symbols,
-      hipMallocFromPoolAsync(&device_ptr, (size_t)allocation_size, memory_pool,
-                             stream),
-      "hipMallocFromPoolAsync");
-
-  // Wrap the allocated HIP buffer in a HAL buffer.
   // NOTE: we don't provide a device allocator because we didn't allocate from
   // one and instead we use a release callback to perform the free if the user
   // doesn't dealloca the buffer.
   iree_hal_buffer_t* buffer = NULL;
-  if (iree_status_is_ok(status)) {
-    iree_hal_buffer_release_callback_t release_callback = {
-        .fn = iree_hal_hip_async_buffer_release_callback,
-        .user_data = pools,
-    };
-    status = iree_hal_hip_buffer_wrap(
-        /*device_allocator=*/NULL, params.type, params.access, params.usage,
-        allocation_size, /*byte_offset=*/0,
-        /*byte_length=*/allocation_size, IREE_HAL_HIP_BUFFER_TYPE_ASYNC,
-        device_ptr, /*host_ptr=*/NULL, release_callback, pools->host_allocator,
-        &buffer);
-  }
+  iree_hal_buffer_release_callback_t release_callback = {
+      .fn = iree_hal_hip_async_buffer_release_callback,
+      .user_data = pools,
+  };
+  iree_status_t status = iree_hal_hip_buffer_wrap(
+      /*device_allocator=*/NULL, params.type, params.access, params.usage,
+      allocation_size, /*byte_offset=*/0,
+      /*byte_length=*/allocation_size, IREE_HAL_HIP_BUFFER_TYPE_ASYNC,
+      /*device_ptr*/ NULL, /*host_ptr=*/NULL, release_callback,
+      pools->host_allocator, &buffer);
 
   if (iree_status_is_ok(status)) {
     // Update statistics (note that it may not yet be accurate).
-    iree_hal_hip_memory_pool_track_alloc(pools, buffer);
     *out_buffer = buffer;
   } else if (buffer) {
+    iree_hal_hip_buffer_set_allocation_empty(buffer);
     iree_hal_buffer_release(buffer);
-  } else {
-    IREE_HIP_IGNORE_ERROR(pools->hip_symbols, hipFreeAsync(device_ptr, stream));
   }
 
   IREE_TRACE_ZONE_END(z0);
@@ -282,8 +288,10 @@
   if (iree_hal_hip_buffer_type(buffer) == IREE_HAL_HIP_BUFFER_TYPE_ASYNC) {
     // Try to schedule the buffer for freeing.
     hipDeviceptr_t device_ptr = iree_hal_hip_buffer_device_pointer(buffer);
-    status = IREE_HIP_RESULT_TO_STATUS(
-        pools->hip_symbols, hipFreeAsync(device_ptr, stream), "hipFreeAsync");
+    if (device_ptr) {
+      status = IREE_HIP_RESULT_TO_STATUS(
+          pools->hip_symbols, hipFreeAsync(device_ptr, stream), "hipFreeAsync");
+    }
     if (iree_status_is_ok(status)) {
       // Drop the release callback so that we don't try to double-free the
       // buffer. Note that we only do this if the HIP free succeeded as
diff --git a/runtime/src/iree/hal/drivers/hip/memory_pools.h b/runtime/src/iree/hal/drivers/hip/memory_pools.h
index e39370a..c505bbe 100644
--- a/runtime/src/iree/hal/drivers/hip/memory_pools.h
+++ b/runtime/src/iree/hal/drivers/hip/memory_pools.h
@@ -63,9 +63,15 @@
     iree_hal_hip_memory_pools_t* pools,
     const iree_hal_hip_memory_pooling_params_t* pooling_params);
 
-// Asynchronously allocates a buffer from an appropriate pool.
-// The allocation will be stream-ordered on |stream|.
-iree_status_t iree_hal_hip_memory_pools_allocate(
+iree_status_t iree_hal_hip_memory_pools_allocate_pointer(
+    iree_hal_hip_memory_pools_t* pools, iree_hal_buffer_t* buffer,
+    hipStream_t stream, iree_device_size_t allocation_size);
+
+// Prepares a buffer to be allocated from the given pool.
+// It does no actual allocations, they must happen on another thread.
+// Any calls to get the device_ptr from the buffer will
+// result in a wait until the allocation is available.
+iree_status_t iree_hal_hip_memory_pools_prepare_buffer(
     iree_hal_hip_memory_pools_t* pools, hipStream_t stream,
     iree_hal_allocator_pool_t pool, iree_hal_buffer_params_t params,
     iree_device_size_t allocation_size,
diff --git a/runtime/src/iree/hal/utils/deferred_work_queue.c b/runtime/src/iree/hal/utils/deferred_work_queue.c
index e41fe35..c783bbe 100644
--- a/runtime/src/iree/hal/utils/deferred_work_queue.c
+++ b/runtime/src/iree/hal/utils/deferred_work_queue.c
@@ -19,14 +19,30 @@
 
 // The maximal number of events a command buffer can wait on.
 #define IREE_HAL_MAX_WAIT_EVENT_COUNT 32
+#define IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS 0
 
+#if IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+IREE_TRACE(static const char* IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_PLOT_NAME =
+               "iree_hal_work_queue_pending");
+IREE_TRACE(static const char* IREE_HAL_DEFERRED_WORKER_QUEUE_READY_PLOT_NAME =
+               "iree_hal_work_queue_ready");
+IREE_TRACE(
+    static const char* IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_ALLOC_PLOT_NAME =
+        "iree_hal_work_queue_pending_alloc");
+IREE_TRACE(static const char*
+               IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_DEALLOC_PLOT_NAME =
+                   "iree_hal_work_queue_pending_dealloc");
+#endif  // IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
 //===----------------------------------------------------------------------===//
 // Queue action
 //===----------------------------------------------------------------------===//
 
 typedef enum iree_hal_deferred_work_queue_action_kind_e {
   IREE_HAL_QUEUE_ACTION_TYPE_EXECUTION,
-  // TODO: Add support for queue alloca and dealloca.
+  IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_ALLOC,
+  IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_DEALLOC,
+  IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_MAX =
+      IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_DEALLOC,
 } iree_hal_deferred_work_queue_action_kind_t;
 
 typedef enum iree_hal_deferred_work_queue_action_state_e {
@@ -67,6 +83,12 @@
       iree_hal_command_buffer_t** command_buffers;
       iree_hal_buffer_binding_table_t* binding_tables;
     } execution;
+    struct {
+      iree_hal_buffer_t* buffer;
+    } alloc;
+    struct {
+      iree_hal_buffer_t* buffer;
+    } dealloc;
   } payload;
 
   // Resource set to retain all associated resources by the payload.
@@ -501,6 +523,21 @@
   IREE_ASSERT_ARGUMENT(out_actions);
   IREE_TRACE_ZONE_BEGIN(z0);
 
+#if IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+  IREE_TRACE_SET_PLOT_TYPE(IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_PLOT_NAME,
+                           IREE_TRACING_PLOT_TYPE_NUMBER, /*step=*/true,
+                           /*fill=*/true, /*color=*/0);
+  IREE_TRACE_SET_PLOT_TYPE(IREE_HAL_DEFERRED_WORKER_QUEUE_READY_PLOT_NAME,
+                           IREE_TRACING_PLOT_TYPE_NUMBER, /*step=*/true,
+                           /*fill=*/true, /*color=*/0);
+  IREE_TRACE_SET_PLOT_TYPE(
+      IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_ALLOC_PLOT_NAME,
+      IREE_TRACING_PLOT_TYPE_NUMBER, /*step=*/true, /*fill=*/true, /*color=*/0);
+  IREE_TRACE_SET_PLOT_TYPE(
+      IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_DEALLOC_PLOT_NAME,
+      IREE_TRACING_PLOT_TYPE_NUMBER, /*step=*/true, /*fill=*/true, /*color=*/0);
+#endif  //  IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+
   iree_hal_deferred_work_queue_t* actions = NULL;
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
       z0, iree_allocator_malloc(host_allocator, sizeof(*actions),
@@ -812,6 +849,140 @@
   return status;
 }
 
+static iree_status_t iree_hal_deferred_work_queue_enqueue_buffer_operation(
+    iree_hal_deferred_work_queue_t* actions,
+    iree_hal_deferred_work_queue_action_kind_t kind,
+    const iree_hal_semaphore_list_t wait_semaphore_list,
+    const iree_hal_semaphore_list_t signal_semaphore_list,
+    iree_hal_buffer_t* buffer) {
+  IREE_ASSERT_ARGUMENT(actions);
+  IREE_ASSERT_ARGUMENT(buffer);
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  // Embed captured tables in the action allocation.
+  iree_hal_deferred_work_queue_action_t* action = NULL;
+  const iree_host_size_t wait_semaphore_list_size =
+      wait_semaphore_list.count * sizeof(*wait_semaphore_list.semaphores) +
+      wait_semaphore_list.count * sizeof(*wait_semaphore_list.payload_values);
+  const iree_host_size_t signal_semaphore_list_size =
+      signal_semaphore_list.count * sizeof(*signal_semaphore_list.semaphores) +
+      signal_semaphore_list.count *
+          sizeof(*signal_semaphore_list.payload_values);
+
+  const iree_host_size_t total_action_size =
+      sizeof(*action) + wait_semaphore_list_size + signal_semaphore_list_size;
+
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_allocator_malloc(actions->host_allocator, total_action_size,
+                                (void**)&action));
+  uint8_t* action_ptr = (uint8_t*)action + sizeof(*action);
+
+  action->owning_actions = actions;
+  action->device_interface = actions->device_interface;
+  action->state = IREE_HAL_QUEUE_ACTION_STATE_ALIVE;
+  action->cleanup_callback = NULL;
+  action->callback_user_data = NULL;
+  action->kind = kind;
+
+  // Initialize scratch fields.
+  action->event_count = 0;
+  action->is_pending = true;
+
+  // Copy wait list for later access.
+  action->wait_semaphore_list.count = wait_semaphore_list.count;
+  action->wait_semaphore_list.semaphores = (iree_hal_semaphore_t**)action_ptr;
+  memcpy(action->wait_semaphore_list.semaphores, wait_semaphore_list.semaphores,
+         wait_semaphore_list.count * sizeof(*wait_semaphore_list.semaphores));
+  action->wait_semaphore_list.payload_values =
+      (uint64_t*)(action_ptr + wait_semaphore_list.count *
+                                   sizeof(*wait_semaphore_list.semaphores));
+  memcpy(
+      action->wait_semaphore_list.payload_values,
+      wait_semaphore_list.payload_values,
+      wait_semaphore_list.count * sizeof(*wait_semaphore_list.payload_values));
+  action_ptr += wait_semaphore_list_size;
+
+  // Copy signal list for later access.
+  action->signal_semaphore_list.count = signal_semaphore_list.count;
+  action->signal_semaphore_list.semaphores = (iree_hal_semaphore_t**)action_ptr;
+  memcpy(
+      action->signal_semaphore_list.semaphores,
+      signal_semaphore_list.semaphores,
+      signal_semaphore_list.count * sizeof(*signal_semaphore_list.semaphores));
+  action->signal_semaphore_list.payload_values =
+      (uint64_t*)(action_ptr + signal_semaphore_list.count *
+                                   sizeof(*signal_semaphore_list.semaphores));
+  memcpy(action->signal_semaphore_list.payload_values,
+         signal_semaphore_list.payload_values,
+         signal_semaphore_list.count *
+             sizeof(*signal_semaphore_list.payload_values));
+  action_ptr += signal_semaphore_list_size;
+
+  // Copy the execution resources for later access.
+  action->payload.alloc.buffer = buffer;
+
+  // Retain all command buffers and semaphores.
+  iree_status_t status = iree_hal_resource_set_allocate(actions->block_pool,
+                                                        &action->resource_set);
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_resource_set_insert(action->resource_set,
+                                          wait_semaphore_list.count,
+                                          wait_semaphore_list.semaphores);
+  }
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_resource_set_insert(action->resource_set,
+                                          signal_semaphore_list.count,
+                                          signal_semaphore_list.semaphores);
+  }
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_resource_set_insert(action->resource_set, 1, &buffer);
+  }
+
+  if (iree_status_is_ok(status)) {
+    // Now everything is okay and we can enqueue the action.
+    iree_slim_mutex_lock(&actions->action_mutex);
+    if (actions->exit_requested) {
+      status = iree_make_status(
+          IREE_STATUS_ABORTED,
+          "can not issue more executions, exit already requested");
+      iree_hal_deferred_work_queue_action_fail_locked(action, status);
+    } else {
+      iree_hal_deferred_work_queue_action_list_push_back(&actions->action_list,
+                                                         action);
+      // One work item is the callback that makes it across from the
+      // completion thread.
+      actions->pending_work_items_count += 1;
+    }
+    iree_slim_mutex_unlock(&actions->action_mutex);
+  } else {
+    iree_hal_resource_set_free(action->resource_set);
+    iree_allocator_free(actions->host_allocator, action);
+  }
+
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+iree_status_t iree_hal_deferred_work_queue_enqueue_alloc(
+    iree_hal_deferred_work_queue_t* actions,
+    const iree_hal_semaphore_list_t wait_semaphore_list,
+    const iree_hal_semaphore_list_t signal_semaphore_list,
+    iree_hal_buffer_t* buffer) {
+  return iree_hal_deferred_work_queue_enqueue_buffer_operation(
+      actions, IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_ALLOC, wait_semaphore_list,
+      signal_semaphore_list, buffer);
+}
+
+iree_status_t iree_hal_deferred_work_queue_enqueue_dealloc(
+    iree_hal_deferred_work_queue_t* actions,
+    const iree_hal_semaphore_list_t wait_semaphore_list,
+    const iree_hal_semaphore_list_t signal_semaphore_list,
+    iree_hal_buffer_t* buffer) {
+  return iree_hal_deferred_work_queue_enqueue_buffer_operation(
+      actions, IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_DEALLOC, wait_semaphore_list,
+      signal_semaphore_list, buffer);
+}
+
 // Does not consume |status|.
 static void iree_hal_deferred_work_queue_fail_status_locked(
     iree_hal_deferred_work_queue_t* actions, iree_status_t status) {
@@ -913,7 +1084,7 @@
   IREE_TRACE_ZONE_BEGIN(z0);
   iree_hal_deferred_work_queue_action_t* action =
       (iree_hal_deferred_work_queue_action_t*)user_data;
-  IREE_ASSERT_EQ(action->kind, IREE_HAL_QUEUE_ACTION_TYPE_EXECUTION);
+  IREE_ASSERT_LE(action->kind, IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_MAX);
   IREE_ASSERT_EQ(action->state, IREE_HAL_QUEUE_ACTION_STATE_ALIVE);
   if (IREE_UNLIKELY(!iree_status_is_ok(status))) {
     iree_hal_deferred_work_queue_action_fail(action, status);
@@ -941,7 +1112,7 @@
 // Issues the given kernel dispatch |action| to the GPU.
 static iree_status_t iree_hal_deferred_work_queue_issue_execution(
     iree_hal_deferred_work_queue_action_t* action) {
-  IREE_ASSERT_EQ(action->kind, IREE_HAL_QUEUE_ACTION_TYPE_EXECUTION);
+  IREE_ASSERT_LE(action->kind, IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_MAX);
   IREE_ASSERT_EQ(action->is_pending, false);
   iree_hal_deferred_work_queue_t* actions = action->owning_actions;
   iree_hal_deferred_work_queue_device_interface_t* device_interface =
@@ -958,56 +1129,75 @@
                 device_interface, action->wait_events[i]));
   }
 
-  // Then launch all command buffers to the dispatch stream.
-  IREE_TRACE_ZONE_BEGIN(z_dispatch_command_buffers);
-  IREE_TRACE_ZONE_APPEND_TEXT(z_dispatch_command_buffers,
-                              "dispatch_command_buffers");
+  switch (action->kind) {
+    case IREE_HAL_QUEUE_ACTION_TYPE_EXECUTION: {
+      // Then launch all command buffers to the dispatch stream.
+      IREE_TRACE_ZONE_BEGIN(z_dispatch_command_buffers);
+      IREE_TRACE_ZONE_APPEND_TEXT(z_dispatch_command_buffers,
+                                  "dispatch_command_buffers");
 
-  for (iree_host_size_t i = 0; i < action->payload.execution.count; ++i) {
-    iree_hal_command_buffer_t* command_buffer =
-        action->payload.execution.command_buffers[i];
-    iree_hal_buffer_binding_table_t binding_table =
-        action->payload.execution.binding_tables
-            ? action->payload.execution.binding_tables[i]
-            : iree_hal_buffer_binding_table_empty();
-    if (iree_hal_deferred_command_buffer_isa(command_buffer)) {
-      iree_hal_command_buffer_t* stream_command_buffer = NULL;
-      iree_hal_command_buffer_mode_t mode =
-          iree_hal_command_buffer_mode(command_buffer) |
-          IREE_HAL_COMMAND_BUFFER_MODE_ONE_SHOT |
-          // NOTE: we need to validate if a binding table is provided as the
-          // bindings were not known when it was originally recorded.
-          (iree_hal_buffer_binding_table_is_empty(binding_table)
-               ? IREE_HAL_COMMAND_BUFFER_MODE_UNVALIDATED
-               : 0);
-      IREE_RETURN_AND_END_ZONE_IF_ERROR(
-          z0, device_interface->vtable->create_stream_command_buffer(
-                  device_interface, mode, IREE_HAL_COMMAND_CATEGORY_ANY,
-                  &stream_command_buffer))
-      IREE_RETURN_AND_END_ZONE_IF_ERROR(
-          z0, iree_hal_resource_set_insert(action->resource_set, 1,
-                                           &stream_command_buffer));
+      for (iree_host_size_t i = 0; i < action->payload.execution.count; ++i) {
+        iree_hal_command_buffer_t* command_buffer =
+            action->payload.execution.command_buffers[i];
+        iree_hal_buffer_binding_table_t binding_table =
+            action->payload.execution.binding_tables
+                ? action->payload.execution.binding_tables[i]
+                : iree_hal_buffer_binding_table_empty();
+        if (iree_hal_deferred_command_buffer_isa(command_buffer)) {
+          iree_hal_command_buffer_t* stream_command_buffer = NULL;
+          iree_hal_command_buffer_mode_t mode =
+              iree_hal_command_buffer_mode(command_buffer) |
+              IREE_HAL_COMMAND_BUFFER_MODE_ONE_SHOT |
+              // NOTE: we need to validate if a binding table is provided as the
+              // bindings were not known when it was originally recorded.
+              (iree_hal_buffer_binding_table_is_empty(binding_table)
+                   ? IREE_HAL_COMMAND_BUFFER_MODE_UNVALIDATED
+                   : 0);
+          IREE_RETURN_AND_END_ZONE_IF_ERROR(
+              z0, device_interface->vtable->create_stream_command_buffer(
+                      device_interface, mode, IREE_HAL_COMMAND_CATEGORY_ANY,
+                      &stream_command_buffer))
+          IREE_RETURN_AND_END_ZONE_IF_ERROR(
+              z0, iree_hal_resource_set_insert(action->resource_set, 1,
+                                               &stream_command_buffer));
 
-      IREE_RETURN_AND_END_ZONE_IF_ERROR(
-          z0, iree_hal_deferred_command_buffer_apply(
-                  command_buffer, stream_command_buffer, binding_table));
-      command_buffer = stream_command_buffer;
-    } else {
-      iree_hal_resource_retain(command_buffer);
+          IREE_RETURN_AND_END_ZONE_IF_ERROR(
+              z0, iree_hal_deferred_command_buffer_apply(
+                      command_buffer, stream_command_buffer, binding_table));
+          command_buffer = stream_command_buffer;
+        } else {
+          iree_hal_resource_retain(command_buffer);
+        }
+
+        IREE_RETURN_AND_END_ZONE_IF_ERROR(
+            z0, device_interface->vtable->submit_command_buffer(
+                    device_interface, command_buffer));
+
+        // The stream_command_buffer is going to be retained by
+        // the action->resource_set and deleted after the action
+        // completes.
+        iree_hal_resource_release(command_buffer);
+      }
+
+      IREE_TRACE_ZONE_END(z_dispatch_command_buffers);
+      break;
     }
-
-    IREE_RETURN_AND_END_ZONE_IF_ERROR(
-        z0, device_interface->vtable->submit_command_buffer(device_interface,
-                                                            command_buffer));
-
-    // The stream_command_buffer is going to be retained by
-    // the action->resource_set and deleted after the action
-    // completes.
-    iree_hal_resource_release(command_buffer);
+    case IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_ALLOC: {
+      IREE_TRACE_ZONE_APPEND_TEXT(z0, "queue_alloc");
+      IREE_RETURN_AND_END_ZONE_IF_ERROR(
+          z0, device_interface->vtable->async_alloc(
+                  device_interface, action->payload.alloc.buffer));
+      break;
+    }
+    case IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_DEALLOC: {
+      IREE_TRACE_ZONE_APPEND_TEXT(z0, "queue_dealloc");
+      IREE_RETURN_AND_END_ZONE_IF_ERROR(
+          z0, device_interface->vtable->async_dealloc(
+                  device_interface, action->payload.dealloc.buffer));
+      break;
+    }
   }
 
-  IREE_TRACE_ZONE_END(z_dispatch_command_buffers);
-
   iree_hal_deferred_work_queue_native_event_t completion_event = NULL;
   // Last record event signals in the dispatch stream.
   for (iree_host_size_t i = 0; i < action->signal_semaphore_list.count; ++i) {
@@ -1094,6 +1284,12 @@
     return iree_ok_status();
   }
 
+  IREE_TRACE_ZONE_BEGIN(z1);
+#if IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+  IREE_TRACE(uint32_t num_pending = 0; uint32_t num_pending_alloc = 0;
+             uint32_t num_pending_dealloc = 0; uint32_t num_ready = 0;);
+#endif  //  IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+
   iree_status_t status = iree_ok_status();
   // Scan through the list and categorize actions into pending and ready lists.
   while (!iree_hal_deferred_work_queue_action_list_is_empty(
@@ -1185,12 +1381,44 @@
     }
 
     if (action->is_pending) {
+#if IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+      IREE_TRACE({
+        ++num_pending;
+        switch (action->kind) {
+          case IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_ALLOC:
+            ++num_pending_alloc;
+            break;
+          case IREE_HAL_QUEUE_ACTION_TYPE_QUEUE_DEALLOC:
+            ++num_pending_dealloc;
+            break;
+          default:
+            break;
+        }
+      });
+#endif  //  IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
       iree_hal_deferred_work_queue_action_list_push_back(&pending_list, action);
     } else {
+#if IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+      IREE_TRACE(++num_ready;);
+#endif  //  IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
       iree_hal_deferred_work_queue_action_list_push_back(&ready_list, action);
     }
   }
 
+  IREE_TRACE_ZONE_END(z1);
+#if IREE_HAL_DEFERRED_WORKER_QUEUE_VERBOSE_PLOTS
+  IREE_TRACE_PLOT_VALUE_I64(IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_PLOT_NAME,
+                            num_pending);
+  IREE_TRACE_PLOT_VALUE_I64(IREE_HAL_DEFERRED_WORKER_QUEUE_READY_PLOT_NAME,
+                            num_ready);
+  IREE_TRACE_PLOT_VALUE_I64(
+      IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_ALLOC_PLOT_NAME,
+      num_pending_alloc);
+  IREE_TRACE_PLOT_VALUE_I64(
+      IREE_HAL_DEFERRED_WORKER_QUEUE_PENDING_DEALLOC_PLOT_NAME,
+      num_pending_dealloc);
+#endif
+
   // Preserve pending timepoints.
   actions->action_list = pending_list;
 
@@ -1325,8 +1553,10 @@
       IREE_TRACE_ZONE_END(z1);
     }
 
-    status =
-        iree_status_join(status, entry->callback(status, entry->user_data));
+    if (entry->callback) {
+      status =
+          iree_status_join(status, entry->callback(status, entry->user_data));
+    }
 
     if (IREE_UNLIKELY(entry->created_event)) {
       status = iree_status_join(
diff --git a/runtime/src/iree/hal/utils/deferred_work_queue.h b/runtime/src/iree/hal/utils/deferred_work_queue.h
index 3a25886..c076a8d 100644
--- a/runtime/src/iree/hal/utils/deferred_work_queue.h
+++ b/runtime/src/iree/hal/utils/deferred_work_queue.h
@@ -109,6 +109,24 @@
   iree_status_t(IREE_API_PTR* submit_command_buffer)(
       iree_hal_deferred_work_queue_device_interface_t* device_interface,
       iree_hal_command_buffer_t* command_buffer);
+
+  // Asynchronously allocates a pointer and assigns it to the given buffer.
+  //
+  // This is optional, and is only required to be valid if
+  // iree_hal_deferred_work_queue_enqueue_alloc is ever called on the work
+  // queue.
+  iree_status_t(IREE_API_PTR* async_alloc)(
+      iree_hal_deferred_work_queue_device_interface_t* device_interface,
+      iree_hal_buffer_t* buffer);
+
+  // Asynchronously frees a buffer.
+  //
+  // This is optional, and is only required to be valid if
+  // iree_hal_deferred_work_queue_enqueue_dealloc is ever called on the work
+  // queue.
+  iree_status_t(IREE_API_PTR* async_dealloc)(
+      iree_hal_deferred_work_queue_device_interface_t* device_interface,
+      iree_hal_buffer_t* buffer);
 } iree_hal_deferred_work_queue_device_interface_vtable_t;
 
 iree_status_t iree_hal_deferred_work_queue_create(
@@ -134,6 +152,22 @@
     iree_hal_command_buffer_t* const* command_buffers,
     iree_hal_buffer_binding_table_t const* binding_tables);
 
+// Enqueues allocations into the work queue to be executed
+// once all semaphores have been satisfied.
+iree_status_t iree_hal_deferred_work_queue_enqueue_alloc(
+    iree_hal_deferred_work_queue_t* deferred_work_queue,
+    const iree_hal_semaphore_list_t wait_semaphore_list,
+    const iree_hal_semaphore_list_t signal_semaphore_list,
+    iree_hal_buffer_t* buffer);
+
+// Enqueues deallocations into the work queue to be executed
+// once all semaphores have been satisfied.
+iree_status_t iree_hal_deferred_work_queue_enqueue_dealloc(
+    iree_hal_deferred_work_queue_t* deferred_work_queue,
+    const iree_hal_semaphore_list_t wait_semaphore_list,
+    const iree_hal_semaphore_list_t signal_semaphore_list,
+    iree_hal_buffer_t* buffer);
+
 // Attempts to advance the work queue by processing using
 // the current thread, rather than the worker thread.
 iree_status_t iree_hal_deferred_work_queue_issue(