Merge pull request #7900 from google/benvanik-hal-map-cleanup

Metal, WebGPU, and remoting HAL backends map memory in special ways we haven't encountered yet in CUDA/Vulkan/local CPU execution. Many of the tools and samples we have were written assuming that mapping would always succeed, also requiring that the compiler emit buffers that were mappable. This set of changes is designed to make mapping needed less frequently, allow HAL backends to override transfer behavior, and add utilities for users to make it easier to work with data.

There's still some places that are doing extremely shady things (like the e2e matmul tests) but most are now in a form that is compatible with unmappable memory.
diff --git a/bindings/python/iree/runtime/hal.h b/bindings/python/iree/runtime/hal.h
index 011e9d6..72debbb 100644
--- a/bindings/python/iree/runtime/hal.h
+++ b/bindings/python/iree/runtime/hal.h
@@ -134,11 +134,12 @@
   static HalMappedMemory Create(HalBufferView& bv) {
     iree_hal_buffer_t* buffer = iree_hal_buffer_view_buffer(bv.raw_ptr());
     iree_device_size_t byte_length = iree_hal_buffer_byte_length(buffer);
-    iree_hal_buffer_mapping_t mapped_memory;
-    CheckApiStatus(iree_hal_buffer_map_range(
-                       buffer, IREE_HAL_MEMORY_ACCESS_READ,
-                       0 /* element_offset */, byte_length, &mapped_memory),
-                   "Could not map memory");
+    iree_hal_buffer_mapping_t mapped_memory = {{0}};
+    CheckApiStatus(
+        iree_hal_buffer_map_range(buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                                  IREE_HAL_MEMORY_ACCESS_READ, 0, byte_length,
+                                  &mapped_memory),
+        "Could not map memory");
     return HalMappedMemory(mapped_memory, bv.raw_ptr());
   }
 
@@ -168,8 +169,8 @@
   }
 
  private:
-  iree_hal_buffer_mapping_t mapped_memory_;
-  iree_hal_buffer_view_t* bv_;
+  iree_hal_buffer_mapping_t mapped_memory_ = {{0}};
+  iree_hal_buffer_view_t* bv_ = nullptr;
 };
 
 void SetupHalBindings(pybind11::module m);
diff --git a/bindings/python/iree/runtime/vm.cc b/bindings/python/iree/runtime/vm.cc
index d3d9dfa..15a6365 100644
--- a/bindings/python/iree/runtime/vm.cc
+++ b/bindings/python/iree/runtime/vm.cc
@@ -218,16 +218,15 @@
   // TODO(laurenzo): Expand to other layouts as needed.
   // TODO(laurenzo): Wrap and retain original buffer (depends_on_pyobject=true).
   iree_hal_buffer_t* raw_buffer;
-  CheckApiStatus(iree_hal_allocator_allocate_buffer(
-                     device.allocator(),
-                     static_cast<iree_hal_memory_type_t>(
-                         IREE_HAL_MEMORY_TYPE_HOST_LOCAL |
-                         IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE),
-                     IREE_HAL_BUFFER_USAGE_ALL, py_view.len, &raw_buffer),
-                 "Failed to allocate device visible buffer");
   CheckApiStatus(
-      iree_hal_buffer_write_data(raw_buffer, 0, py_view.buf, py_view.len),
-      "Error writing to input buffer");
+      iree_hal_allocator_allocate_buffer(
+          device.allocator(),
+          static_cast<iree_hal_memory_type_t>(
+              IREE_HAL_MEMORY_TYPE_HOST_LOCAL |
+              IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE),
+          IREE_HAL_BUFFER_USAGE_ALL, py_view.len,
+          iree_make_const_byte_span(py_view.buf, py_view.len), &raw_buffer),
+      "Failed to allocate device visible buffer");
 
   // Only capture the reference to the exporting object (incrementing it)
   // once guaranteed successful.
@@ -390,10 +389,11 @@
 
       // Map memory.
       iree_device_size_t byte_length = iree_hal_buffer_byte_length(raw_buffer);
-      iree_hal_buffer_mapping_t mapped_memory;
+      iree_hal_buffer_mapping_t mapped_memory = {{0}};
       CheckApiStatus(iree_hal_buffer_map_range(
-                         raw_buffer, IREE_HAL_MEMORY_ACCESS_READ,
-                         0 /* element_offset */, byte_length, &mapped_memory),
+                         raw_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                         IREE_HAL_MEMORY_ACCESS_READ, 0 /* element_offset */,
+                         byte_length, &mapped_memory),
                      "Could not map memory");
       record["contents"] =
           py::bytes(reinterpret_cast<const char*>(mapped_memory.contents.data),
@@ -489,10 +489,11 @@
   // Map memory.
   iree_device_size_t byte_length =
       iree_hal_buffer_byte_length(buffer.raw_ptr());
-  iree_hal_buffer_mapping_t mapped_memory;
+  iree_hal_buffer_mapping_t mapped_memory = {{0}};
   CheckApiStatus(iree_hal_buffer_map_range(
-                     buffer.raw_ptr(), IREE_HAL_MEMORY_ACCESS_READ,
-                     0 /* element_offset */, byte_length, &mapped_memory),
+                     buffer.raw_ptr(), IREE_HAL_MAPPING_MODE_SCOPED,
+                     IREE_HAL_MEMORY_ACCESS_READ, 0 /* element_offset */,
+                     byte_length, &mapped_memory),
                  "Could not map memory");
 
   // Turn the mapping into a python object that retains until the array is
diff --git a/bindings/tflite/tensor.c b/bindings/tflite/tensor.c
index 1fdb16d..0112fd6 100644
--- a/bindings/tflite/tensor.c
+++ b/bindings/tflite/tensor.c
@@ -140,7 +140,8 @@
       iree_hal_allocator_allocate_buffer(
           buffer_allocator,
           IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
-          IREE_HAL_BUFFER_USAGE_ALL, allocation_size, &tensor->buffer));
+          IREE_HAL_BUFFER_USAGE_ALL, allocation_size,
+          iree_const_byte_span_empty(), &tensor->buffer));
 
   // Map the buffer memory immediately. The tflite API doesn't let us know if
   // this is a buffer the user will actually touch or some state buffer that is
@@ -149,7 +150,8 @@
   // puts potential errors in the same easy to find place.
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
       z0,
-      iree_hal_buffer_map_range(tensor->buffer, IREE_HAL_MEMORY_ACCESS_ALL, 0,
+      iree_hal_buffer_map_range(tensor->buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                                IREE_HAL_MEMORY_ACCESS_ALL, 0,
                                 IREE_WHOLE_BUFFER, &tensor->buffer_mapping));
 
   IREE_TRACE_ZONE_END(z0);
@@ -173,10 +175,10 @@
   iree_device_size_t byte_offset = 0;
   iree_device_size_t byte_length = IREE_WHOLE_BUFFER;
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
-      z0,
-      iree_hal_buffer_map_range(
-          buffer, IREE_HAL_MEMORY_ACCESS_READ | IREE_HAL_MEMORY_ACCESS_WRITE,
-          byte_offset, byte_length, &tensor->buffer_mapping));
+      z0, iree_hal_buffer_map_range(
+              buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+              IREE_HAL_MEMORY_ACCESS_READ | IREE_HAL_MEMORY_ACCESS_WRITE,
+              byte_offset, byte_length, &tensor->buffer_mapping));
 
   // Retain the buffer view until discarded/reset.
   tensor->buffer = buffer;
diff --git a/experimental/rocm/CMakeLists.txt b/experimental/rocm/CMakeLists.txt
index 69943a7..0b4438d 100644
--- a/experimental/rocm/CMakeLists.txt
+++ b/experimental/rocm/CMakeLists.txt
@@ -53,6 +53,7 @@
     iree::base::internal::synchronization
     iree::base::tracing
     iree::hal
+    iree::hal::utils::buffer_transfer
     iree::schemas::rocm_executable_def_c_fbs
   PUBLIC
 )
diff --git a/experimental/rocm/rocm_allocator.c b/experimental/rocm/rocm_allocator.c
index bcb80b8..85a6a3d 100644
--- a/experimental/rocm/rocm_allocator.c
+++ b/experimental/rocm/rocm_allocator.c
@@ -16,6 +16,7 @@
 
 typedef struct iree_hal_rocm_allocator_t {
   iree_hal_resource_t resource;
+  iree_hal_device_t* base_device;
   iree_hal_rocm_context_wrapper_t* context;
 
   IREE_STATISTICS(iree_hal_allocator_statistics_t statistics;)
@@ -30,8 +31,9 @@
 }
 
 iree_status_t iree_hal_rocm_allocator_create(
-    iree_hal_rocm_context_wrapper_t* context,
+    iree_hal_device_t* base_device, iree_hal_rocm_context_wrapper_t* context,
     iree_hal_allocator_t** out_allocator) {
+  IREE_ASSERT_ARGUMENT(base_device);
   IREE_ASSERT_ARGUMENT(context);
   IREE_TRACE_ZONE_BEGIN(z0);
   iree_hal_rocm_allocator_t* allocator = NULL;
@@ -128,7 +130,7 @@
 static iree_status_t iree_hal_rocm_allocator_allocate_buffer(
     iree_hal_allocator_t* base_allocator, iree_hal_memory_type_t memory_type,
     iree_hal_buffer_usage_t allowed_usage, iree_host_size_t allocation_size,
-    iree_hal_buffer_t** out_buffer) {
+    iree_const_byte_span_t initial_data, iree_hal_buffer_t** out_buffer) {
   iree_hal_rocm_allocator_t* allocator =
       iree_hal_rocm_allocator_cast(base_allocator);
   // Guard against the corner case where the requested buffer size is 0. The
@@ -165,6 +167,7 @@
     }
   }
 
+  iree_hal_buffer_t* buffer = NULL;
   if (iree_status_is_ok(status)) {
     status = iree_hal_rocm_buffer_wrap(
         (iree_hal_allocator_t*)allocator, memory_type,
@@ -172,12 +175,30 @@
         /*byte_offset=*/0,
         /*byte_length=*/allocation_size, device_ptr, host_ptr, out_buffer);
   }
+
+  // Copy the initial contents into the buffer. This may require staging.
+  if (iree_status_is_ok(status) &&
+      !iree_const_byte_span_is_empty(initial_data)) {
+    status = iree_hal_device_transfer_range(
+        allocator->base_device,
+        iree_hal_make_host_transfer_buffer_span((void*)initial_data.data,
+                                                initial_data.data_length),
+        0, iree_hal_make_device_transfer_buffer(buffer), 0,
+        initial_data.data_length, IREE_HAL_TRANSFER_BUFFER_FLAG_DEFAULT,
+        iree_infinite_timeout());
+  }
+
   if (iree_status_is_ok(status)) {
     IREE_STATISTICS(iree_hal_allocator_statistics_record_alloc(
         &allocator->statistics, memory_type, allocation_size));
+    *out_buffer = buffer;
   } else {
-    iree_hal_rocm_buffer_free(allocator->context, memory_type, device_ptr,
-                              host_ptr);
+    if (!buffer) {
+      iree_hal_rocm_buffer_free(allocator->context, memory_type, device_ptr,
+                                host_ptr);
+    } else {
+      iree_hal_buffer_release(buffer);
+    }
   }
   return status;
 }
diff --git a/experimental/rocm/rocm_allocator.h b/experimental/rocm/rocm_allocator.h
index a2a89ea..9156b70 100644
--- a/experimental/rocm/rocm_allocator.h
+++ b/experimental/rocm/rocm_allocator.h
@@ -18,7 +18,7 @@
 
 // Create a ROCM allocator.
 iree_status_t iree_hal_rocm_allocator_create(
-    iree_hal_rocm_context_wrapper_t* context,
+    iree_hal_device_t* base_device, iree_hal_rocm_context_wrapper_t* context,
     iree_hal_allocator_t** out_allocator);
 
 #ifdef __cplusplus
diff --git a/experimental/rocm/rocm_buffer.c b/experimental/rocm/rocm_buffer.c
index b39a40e..047f251 100644
--- a/experimental/rocm/rocm_buffer.c
+++ b/experimental/rocm/rocm_buffer.c
@@ -68,14 +68,16 @@
     iree_hal_buffer_t* base_buffer, iree_hal_mapping_mode_t mapping_mode,
     iree_hal_memory_access_t memory_access,
     iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
-    void** out_data_ptr) {
+    iree_hal_buffer_mapping_t* mapping) {
   iree_hal_rocm_buffer_t* buffer = iree_hal_rocm_buffer_cast(base_buffer);
 
-  if (!iree_all_bits_set(buffer->base.memory_type,
-                         IREE_HAL_MEMORY_TYPE_HOST_VISIBLE)) {
-    return iree_make_status(IREE_STATUS_INTERNAL,
-                            "trying to map memory not host visible");
-  }
+  // TODO(benvanik): add upload/download for unmapped buffers.
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_memory_type(
+      iree_hal_buffer_memory_type(base_buffer),
+      IREE_HAL_MEMORY_TYPE_HOST_VISIBLE));
+  IREE_RETURN_IF_ERROR(
+      iree_hal_buffer_validate_usage(iree_hal_buffer_allowed_usage(base_buffer),
+                                     IREE_HAL_BUFFER_USAGE_MAPPING));
 
   uint8_t* data_ptr = (uint8_t*)(buffer->host_ptr) + local_byte_offset;
   // If we mapped for discard scribble over the bytes. This is not a mandated
@@ -87,14 +89,16 @@
     memset(data_ptr, 0xCD, local_byte_length);
   }
 #endif  // !NDEBUG
-  *out_data_ptr = data_ptr;
+
+  mapping->contents = iree_make_byte_span(data_ptr, local_byte_length);
   return iree_ok_status();
 }
 
-static void iree_hal_rocm_buffer_unmap_range(
+static iree_status_t iree_hal_rocm_buffer_unmap_range(
     iree_hal_buffer_t* base_buffer, iree_device_size_t local_byte_offset,
-    iree_device_size_t local_byte_length, void* data_ptr) {
-  // nothing to do.
+    iree_device_size_t local_byte_length, iree_hal_buffer_mapping_t* mapping) {
+  // Nothing to do (today).
+  return iree_ok_status();
 }
 
 static iree_status_t iree_hal_rocm_buffer_invalidate_range(
diff --git a/experimental/rocm/rocm_device.c b/experimental/rocm/rocm_device.c
index bd8cb6d..026650f 100644
--- a/experimental/rocm/rocm_device.c
+++ b/experimental/rocm/rocm_device.c
@@ -21,6 +21,7 @@
 #include "experimental/rocm/rocm_event.h"
 #include "experimental/rocm/status_util.h"
 #include "iree/base/tracing.h"
+#include "iree/hal/utils/buffer_transfer.h"
 
 //===----------------------------------------------------------------------===//
 // iree_hal_rocm_device_t
@@ -91,7 +92,8 @@
   device->context_wrapper.host_allocator = host_allocator;
   device->context_wrapper.syms = syms;
   iree_status_t status = iree_hal_rocm_allocator_create(
-      &device->context_wrapper, &device->device_allocator);
+      (iree_hal_device_t*)device, &device->context_wrapper,
+      &device->device_allocator);
   if (iree_status_is_ok(status)) {
     *out_device = (iree_hal_device_t*)device;
   } else {
@@ -305,6 +307,7 @@
     .create_executable_cache = iree_hal_rocm_device_create_executable_cache,
     .create_executable_layout = iree_hal_rocm_device_create_executable_layout,
     .create_semaphore = iree_hal_rocm_device_create_semaphore,
+    .transfer_range = iree_hal_device_submit_transfer_range_and_wait,
     .queue_submit = iree_hal_rocm_device_queue_submit,
     .submit_and_wait = iree_hal_rocm_device_submit_and_wait,
     .wait_semaphores = iree_hal_rocm_device_wait_semaphores,
diff --git a/iree/base/allocator.h b/iree/base/allocator.h
index ac06123..ea2acf6 100644
--- a/iree/base/allocator.h
+++ b/iree/base/allocator.h
@@ -69,6 +69,15 @@
   return v;
 }
 
+static inline iree_byte_span_t iree_byte_span_empty() {
+  iree_byte_span_t v = {NULL, 0};
+  return v;
+}
+
+static bool iree_byte_span_is_empty(iree_byte_span_t span) {
+  return span.data == NULL || span.data_length == 0;
+}
+
 // A span of constant bytes (ala std::span of const uint8_t).
 typedef struct iree_const_byte_span_t {
   const uint8_t* data;
@@ -81,6 +90,15 @@
   return v;
 }
 
+static inline iree_const_byte_span_t iree_const_byte_span_empty() {
+  iree_const_byte_span_t v = {NULL, 0};
+  return v;
+}
+
+static bool iree_const_byte_span_is_empty(iree_const_byte_span_t span) {
+  return span.data == NULL || span.data_length == 0;
+}
+
 //===----------------------------------------------------------------------===//
 // Totally shady stack allocation
 //===----------------------------------------------------------------------===//
diff --git a/iree/base/status.c b/iree/base/status.c
index 4942eb6..7c6784d 100644
--- a/iree/base/status.c
+++ b/iree/base/status.c
@@ -525,6 +525,18 @@
   return iree_ok_status();
 }
 
+IREE_API_EXPORT iree_status_t iree_status_join(iree_status_t base_status,
+                                               iree_status_t new_status) {
+  // TODO(benvanik): annotate |base_status| with |new_status| so we see it?
+  // This is intended for failure handling and usually the first failure is the
+  // root cause and most important to see.
+  if (!iree_status_is_ok(base_status)) {
+    iree_status_ignore(new_status);
+    return base_status;
+  }
+  return new_status;
+}
+
 IREE_API_EXPORT IREE_ATTRIBUTE_NORETURN void iree_status_abort(
     iree_status_t status) {
   IREE_ASSERT(!iree_status_is_ok(status),
diff --git a/iree/base/status.h b/iree/base/status.h
index 81baad3..a26352c 100644
--- a/iree/base/status.h
+++ b/iree/base/status.h
@@ -379,6 +379,16 @@
 // Returns an OK status that can be used when chaining.
 IREE_API_EXPORT iree_status_t iree_status_ignore(iree_status_t status);
 
+// Returns a new status that is |base_status| if not OK and otherwise returns
+// |new_status|. This allows for chaining failure handling code that may also
+// return statuses.
+//
+// Example:
+//   iree_status_t status = do_something();
+//   return iree_status_join(status, do_cleanup());
+IREE_API_EXPORT iree_status_t iree_status_join(iree_status_t base_status,
+                                               iree_status_t new_status);
+
 // Aborts the program with a failing |status|.
 // This will trigger a SIGABRT. It's best not to use this at all outside of
 // demos or tools.
diff --git a/iree/compiler/ConstEval/Runtime.cpp b/iree/compiler/ConstEval/Runtime.cpp
index 00c6758..cf6d155 100644
--- a/iree/compiler/ConstEval/Runtime.cpp
+++ b/iree/compiler/ConstEval/Runtime.cpp
@@ -230,15 +230,17 @@
       iree_hal_buffer_t* buffer = iree_hal_buffer_view_buffer(bufferView);
 
       // Map the memory and construct.
-      Attribute convertedAttr;
+      // TODO(benvanik): fallback to alloc + iree_hal_buffer_read_data if
+      // mapping is not available. Today with the CPU backends it's always
+      // possible but would not work with accelerators.
       iree_hal_buffer_mapping_t mapping;
-      IREE_CHECK_OK(
-          iree_hal_buffer_map_range(buffer, IREE_HAL_MEMORY_ACCESS_READ,
-                                    /*byte_offset=*/0, length, &mapping));
+      IREE_CHECK_OK(iree_hal_buffer_map_range(
+          buffer, IREE_HAL_MAPPING_MODE_SCOPED, IREE_HAL_MEMORY_ACCESS_READ,
+          /*byte_offset=*/0, length, &mapping));
       MutableArrayRef<char> rawBufferArray(
           reinterpret_cast<char*>(mapping.contents.data),
           mapping.contents.data_length);
-      convertedAttr =
+      auto convertedAttr =
           createAttributeFromRawData(loc, tensorType, rawBufferArray);
       iree_hal_buffer_unmap_range(&mapping);
       return convertedAttr;
diff --git a/iree/hal/BUILD b/iree/hal/BUILD
index c87a07d..ece606e 100644
--- a/iree/hal/BUILD
+++ b/iree/hal/BUILD
@@ -30,6 +30,8 @@
         "buffer_heap_impl.h",
         "buffer_view.c",
         "buffer_view.h",
+        "buffer_view_util.c",
+        "buffer_view_util.h",
         "command_buffer.c",
         "command_buffer.h",
         "command_buffer_validation.c",
diff --git a/iree/hal/CMakeLists.txt b/iree/hal/CMakeLists.txt
index 53528cb..eb0afe7 100644
--- a/iree/hal/CMakeLists.txt
+++ b/iree/hal/CMakeLists.txt
@@ -25,6 +25,8 @@
     "buffer_heap_impl.h"
     "buffer_view.c"
     "buffer_view.h"
+    "buffer_view_util.c"
+    "buffer_view_util.h"
     "command_buffer.c"
     "command_buffer.h"
     "command_buffer_validation.c"
diff --git a/iree/hal/allocator.c b/iree/hal/allocator.c
index e37a603..c6f4794 100644
--- a/iree/hal/allocator.c
+++ b/iree/hal/allocator.c
@@ -86,13 +86,14 @@
 IREE_API_EXPORT iree_status_t iree_hal_allocator_allocate_buffer(
     iree_hal_allocator_t* allocator, iree_hal_memory_type_t memory_type,
     iree_hal_buffer_usage_t allowed_usage, iree_host_size_t allocation_size,
-    iree_hal_buffer_t** out_buffer) {
+    iree_const_byte_span_t initial_data, iree_hal_buffer_t** out_buffer) {
   IREE_ASSERT_ARGUMENT(allocator);
   IREE_ASSERT_ARGUMENT(out_buffer);
   *out_buffer = NULL;
   IREE_TRACE_ZONE_BEGIN(z0);
   iree_status_t status = _VTABLE_DISPATCH(allocator, allocate_buffer)(
-      allocator, memory_type, allowed_usage, allocation_size, out_buffer);
+      allocator, memory_type, allowed_usage, allocation_size, initial_data,
+      out_buffer);
   IREE_TRACE_ZONE_END(z0);
   return status;
 }
diff --git a/iree/hal/allocator.h b/iree/hal/allocator.h
index d5e7a2d..a7d9d1d 100644
--- a/iree/hal/allocator.h
+++ b/iree/hal/allocator.h
@@ -125,6 +125,9 @@
     iree_hal_buffer_usage_t intended_usage, iree_device_size_t allocation_size);
 
 // Allocates a buffer from the allocator.
+// If |initial_data| is provided then the bytes will be copied into the device
+// buffer. To avoid the copy when constant data is used prefer
+// iree_hal_allocator_wrap_buffer when available.
 // Fails if the memory type requested for the given usage cannot be serviced.
 // Callers can use iree_hal_allocator_can_allocate to decide their memory use
 // strategy.
@@ -142,7 +145,7 @@
 IREE_API_EXPORT iree_status_t iree_hal_allocator_allocate_buffer(
     iree_hal_allocator_t* allocator, iree_hal_memory_type_t memory_type,
     iree_hal_buffer_usage_t allowed_usage, iree_host_size_t allocation_size,
-    iree_hal_buffer_t** out_buffer);
+    iree_const_byte_span_t initial_data, iree_hal_buffer_t** out_buffer);
 
 // Wraps an existing host allocation in a buffer.
 //
@@ -211,7 +214,7 @@
   iree_status_t(IREE_API_PTR* allocate_buffer)(
       iree_hal_allocator_t* allocator, iree_hal_memory_type_t memory_type,
       iree_hal_buffer_usage_t allowed_usage, iree_host_size_t allocation_size,
-      iree_hal_buffer_t** out_buffer);
+      iree_const_byte_span_t initial_data, iree_hal_buffer_t** out_buffer);
 
   iree_status_t(IREE_API_PTR* wrap_buffer)(
       iree_hal_allocator_t* allocator, iree_hal_memory_type_t memory_type,
diff --git a/iree/hal/allocator_heap.c b/iree/hal/allocator_heap.c
index 84ce645..7513803 100644
--- a/iree/hal/allocator_heap.c
+++ b/iree/hal/allocator_heap.c
@@ -153,7 +153,7 @@
 static iree_status_t iree_hal_heap_allocator_allocate_buffer(
     iree_hal_allocator_t* base_allocator, iree_hal_memory_type_t memory_type,
     iree_hal_buffer_usage_t allowed_usage, iree_host_size_t allocation_size,
-    iree_hal_buffer_t** out_buffer) {
+    iree_const_byte_span_t initial_data, iree_hal_buffer_t** out_buffer) {
   iree_hal_heap_allocator_t* allocator =
       iree_hal_heap_allocator_cast(base_allocator);
 
@@ -165,10 +165,24 @@
   // Allocate the buffer (both the wrapper and the contents).
   iree_hal_heap_allocator_statistics_t* statistics = NULL;
   IREE_STATISTICS(statistics = &allocator->statistics);
-  return iree_hal_heap_buffer_create(base_allocator, statistics, memory_type,
-                                     allowed_access, allowed_usage,
-                                     allocation_size, allocator->data_allocator,
-                                     allocator->host_allocator, out_buffer);
+  iree_hal_buffer_t* buffer = NULL;
+  IREE_RETURN_IF_ERROR(iree_hal_heap_buffer_create(
+      base_allocator, statistics, memory_type, allowed_access, allowed_usage,
+      allocation_size, allocator->data_allocator, allocator->host_allocator,
+      &buffer));
+
+  iree_status_t status = iree_ok_status();
+  if (!iree_const_byte_span_is_empty(initial_data)) {
+    status = iree_hal_buffer_write_data(buffer, 0, initial_data.data,
+                                        initial_data.data_length);
+  }
+
+  if (iree_status_is_ok(status)) {
+    *out_buffer = buffer;
+  } else {
+    iree_hal_buffer_release(buffer);
+  }
+  return status;
 }
 
 static iree_status_t iree_hal_heap_allocator_wrap_buffer(
diff --git a/iree/hal/api.h b/iree/hal/api.h
index 33e2bdd..0ca7171 100644
--- a/iree/hal/api.h
+++ b/iree/hal/api.h
@@ -12,6 +12,7 @@
 #include "iree/hal/allocator.h"              // IWYU pragma: export
 #include "iree/hal/buffer.h"                 // IWYU pragma: export
 #include "iree/hal/buffer_view.h"            // IWYU pragma: export
+#include "iree/hal/buffer_view_util.h"       // IWYU pragma: export
 #include "iree/hal/command_buffer.h"         // IWYU pragma: export
 #include "iree/hal/descriptor_set.h"         // IWYU pragma: export
 #include "iree/hal/descriptor_set_layout.h"  // IWYU pragma: export
diff --git a/iree/hal/buffer.c b/iree/hal/buffer.c
index b83d809..81876ed 100644
--- a/iree/hal/buffer.c
+++ b/iree/hal/buffer.c
@@ -115,18 +115,18 @@
     iree_hal_buffer_t* buffer, iree_hal_mapping_mode_t mapping_mode,
     iree_hal_memory_access_t memory_access,
     iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
-    void** out_data_ptr) {
+    iree_hal_buffer_mapping_t* mapping) {
   return _VTABLE_DISPATCH(buffer->allocated_buffer, map_range)(
       buffer->allocated_buffer, mapping_mode, memory_access, local_byte_offset,
-      local_byte_length, out_data_ptr);
+      local_byte_length, mapping);
 }
 
-static void iree_hal_subspan_buffer_unmap_range(
+static iree_status_t iree_hal_subspan_buffer_unmap_range(
     iree_hal_buffer_t* buffer, iree_device_size_t local_byte_offset,
-    iree_device_size_t local_byte_length, void* data_ptr) {
-  if (!buffer->allocated_buffer) return;
-  _VTABLE_DISPATCH(buffer->allocated_buffer, unmap_range)
-  (buffer->allocated_buffer, local_byte_offset, local_byte_length, data_ptr);
+    iree_device_size_t local_byte_length, iree_hal_buffer_mapping_t* mapping) {
+  if (!buffer->allocated_buffer) return iree_ok_status();
+  return _VTABLE_DISPATCH(buffer->allocated_buffer, unmap_range)(
+      buffer->allocated_buffer, local_byte_offset, local_byte_length, mapping);
 }
 
 static iree_status_t iree_hal_subspan_buffer_invalidate_range(
@@ -481,6 +481,10 @@
   return buffer->allowed_usage;
 }
 
+//===----------------------------------------------------------------------===//
+// Transfer
+//===----------------------------------------------------------------------===//
+
 IREE_API_EXPORT iree_status_t
 iree_hal_buffer_zero(iree_hal_buffer_t* buffer, iree_device_size_t byte_offset,
                      iree_device_size_t byte_length) {
@@ -507,18 +511,18 @@
   }
 
   IREE_TRACE_ZONE_BEGIN(z0);
-  iree_hal_buffer_mapping_t target_mapping;
+  iree_hal_buffer_mapping_t target_mapping = {{0}};
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
-      z0,
-      iree_hal_buffer_map_range(buffer, IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE,
-                                byte_offset, byte_length, &target_mapping));
+      z0, iree_hal_buffer_map_range(buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                                    IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE,
+                                    byte_offset, byte_length, &target_mapping));
   if (byte_length == IREE_WHOLE_BUFFER) {
     byte_length = target_mapping.contents.data_length;
   }
 
   if (IREE_UNLIKELY((byte_offset % pattern_length) != 0) ||
       IREE_UNLIKELY((byte_length % pattern_length) != 0)) {
-    iree_hal_buffer_unmap_range(&target_mapping);
+    iree_status_ignore(iree_hal_buffer_unmap_range(&target_mapping));
     IREE_TRACE_ZONE_END(z0);
     return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                             "attempting to fill a range with %zu byte values "
@@ -572,7 +576,8 @@
     status = iree_hal_buffer_flush_range(&target_mapping, 0, IREE_WHOLE_BUFFER);
   }
 
-  iree_hal_buffer_unmap_range(&target_mapping);
+  status =
+      iree_status_join(status, iree_hal_buffer_unmap_range(&target_mapping));
   IREE_TRACE_ZONE_END(z0);
   return status;
 }
@@ -587,11 +592,12 @@
   IREE_ASSERT_ARGUMENT(target_buffer);
 
   IREE_TRACE_ZONE_BEGIN(z0);
-  iree_hal_buffer_mapping_t source_mapping;
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, data_length);
+  iree_hal_buffer_mapping_t source_mapping = {{0}};
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
-      z0,
-      iree_hal_buffer_map_range(source_buffer, IREE_HAL_MEMORY_ACCESS_READ,
-                                source_offset, data_length, &source_mapping));
+      z0, iree_hal_buffer_map_range(source_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                                    IREE_HAL_MEMORY_ACCESS_READ, source_offset,
+                                    data_length, &source_mapping));
 
   memcpy(target_buffer, source_mapping.contents.data, data_length);
 
@@ -610,11 +616,13 @@
   IREE_ASSERT_ARGUMENT(source_buffer);
 
   IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, data_length);
   iree_hal_buffer_mapping_t target_mapping;
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
-      z0, iree_hal_buffer_map_range(
-              target_buffer, IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE,
-              target_offset, data_length, &target_mapping));
+      z0,
+      iree_hal_buffer_map_range(target_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                                IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE,
+                                target_offset, data_length, &target_mapping));
 
   memcpy(target_mapping.contents.data, source_buffer, data_length);
 
@@ -650,19 +658,21 @@
   }
 
   IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, data_length);
 
   // Map source, which may have IREE_WHOLE_BUFFER length.
   iree_hal_buffer_mapping_t source_mapping;
   IREE_RETURN_AND_END_ZONE_IF_ERROR(
-      z0,
-      iree_hal_buffer_map_range(source_buffer, IREE_HAL_MEMORY_ACCESS_READ,
-                                source_offset, data_length, &source_mapping));
+      z0, iree_hal_buffer_map_range(source_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                                    IREE_HAL_MEMORY_ACCESS_READ, source_offset,
+                                    data_length, &source_mapping));
 
   // Map target, which may also have IREE_WHOLE_BUFFER length.
   iree_hal_buffer_mapping_t target_mapping;
-  iree_status_t status = iree_hal_buffer_map_range(
-      target_buffer, IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, target_offset,
-      data_length, &target_mapping);
+  iree_status_t status =
+      iree_hal_buffer_map_range(target_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+                                IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE,
+                                target_offset, data_length, &target_mapping);
   if (!iree_status_is_ok(status)) {
     iree_hal_buffer_unmap_range(&source_mapping);
     IREE_TRACE_ZONE_END(z0);
@@ -682,8 +692,10 @@
     adjusted_data_length = target_mapping.contents.data_length;
   }
 
-  // Elide zero length copies.
-  if (adjusted_data_length == 0) {
+  // Elide zero length copies. It's been expensive to get to this point just to
+  // bail but we need to have mapped to resolve IREE_WHOLE_BUFFERs that may
+  // result in zero lengths.
+  if (IREE_UNLIKELY(adjusted_data_length == 0)) {
     IREE_TRACE_ZONE_END(z0);
     return iree_ok_status();
   }
@@ -704,126 +716,123 @@
 }
 
 //===----------------------------------------------------------------------===//
-// Mapping / iree_hal_buffer_mapping_impl_t
+// Mapping
 //===----------------------------------------------------------------------===//
 
-typedef struct iree_hal_buffer_mapping_impl_t {
-  // Must be first (as in iree_hal_buffer_mapping_t).
-  // Stores both the offset data pointer and the byte_length of the mapping.
-  iree_byte_span_t contents;
-  // Retained buffer providing the backing storage for the mapping.
-  iree_hal_buffer_t* backing_buffer;
-  // Byte offset within the buffer where the mapped data begins.
-  iree_device_size_t byte_offset;
-  // Used for validation only.
-  iree_hal_memory_access_t allowed_access;
-  uint32_t reserved0;  // unused
-  uint64_t reserved1;  // unused
-} iree_hal_buffer_mapping_impl_t;
-
-// We overlay the impl onto the external iree_hal_buffer_mapping_t struct;
-// ensure we match the fields that are exposed.
-static_assert(sizeof(iree_hal_buffer_mapping_impl_t) <=
-                  sizeof(iree_hal_buffer_mapping_t),
-              "buffer mapping impl must fit inside the external struct");
-static_assert(offsetof(iree_hal_buffer_mapping_impl_t, contents) ==
-                  offsetof(iree_hal_buffer_mapping_t, contents),
-              "contents byte span must match the external struct offset");
-
 IREE_API_EXPORT iree_status_t iree_hal_buffer_map_range(
-    iree_hal_buffer_t* buffer, iree_hal_memory_access_t memory_access,
-    iree_device_size_t byte_offset, iree_device_size_t byte_length,
+    iree_hal_buffer_t* buffer, iree_hal_mapping_mode_t mapping_mode,
+    iree_hal_memory_access_t memory_access, iree_device_size_t byte_offset,
+    iree_device_size_t byte_length,
     iree_hal_buffer_mapping_t* out_buffer_mapping) {
   IREE_ASSERT_ARGUMENT(buffer);
   IREE_ASSERT_ARGUMENT(out_buffer_mapping);
-  memset(out_buffer_mapping, 0, sizeof(*out_buffer_mapping));
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_memory_type(
-      iree_hal_buffer_memory_type(buffer), IREE_HAL_MEMORY_TYPE_HOST_VISIBLE));
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_access(
-      iree_hal_buffer_allowed_access(buffer), memory_access));
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_usage(
-      iree_hal_buffer_allowed_usage(buffer), IREE_HAL_BUFFER_USAGE_MAPPING));
-
-  iree_hal_buffer_mapping_impl_t* buffer_mapping =
-      (iree_hal_buffer_mapping_impl_t*)out_buffer_mapping;
-  buffer_mapping->backing_buffer = buffer;
-  buffer_mapping->allowed_access = memory_access;
-  iree_device_size_t data_length;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_calculate_range(
-      iree_hal_buffer_byte_offset(buffer), iree_hal_buffer_byte_length(buffer),
-      byte_offset, byte_length, &buffer_mapping->byte_offset, &data_length));
-  buffer_mapping->contents.data_length = data_length;
-
-  // TODO(benvanik): add mode arg to the HAL API.
-  iree_hal_mapping_mode_t mapping_mode = IREE_HAL_MAPPING_MODE_SCOPED;
-
   IREE_TRACE_ZONE_BEGIN(z0);
+  memset(out_buffer_mapping, 0, sizeof(*out_buffer_mapping));
+
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_hal_buffer_validate_access(
+              iree_hal_buffer_allowed_access(buffer), memory_access));
+
+  // Persistent mapping requires the buffer was allocated to support it.
+  const bool is_persistent =
+      iree_all_bits_set(mapping_mode, IREE_HAL_MAPPING_MODE_PERSISTENT);
+  if (is_persistent) {
+    IREE_RETURN_AND_END_ZONE_IF_ERROR(z0,
+                                      iree_hal_buffer_validate_memory_type(
+                                          iree_hal_buffer_memory_type(buffer),
+                                          IREE_HAL_MEMORY_TYPE_HOST_VISIBLE));
+    IREE_RETURN_AND_END_ZONE_IF_ERROR(
+        z0,
+        iree_hal_buffer_validate_usage(iree_hal_buffer_allowed_usage(buffer),
+                                       IREE_HAL_BUFFER_USAGE_MAPPING));
+  }
+
+  iree_device_size_t local_byte_offset = 0;
+  iree_device_size_t local_byte_length = 0;
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_hal_buffer_calculate_range(
+              iree_hal_buffer_byte_offset(buffer),
+              iree_hal_buffer_byte_length(buffer), byte_offset, byte_length,
+              &local_byte_offset, &local_byte_length));
+
+  out_buffer_mapping->buffer = buffer;
+  out_buffer_mapping->impl.allowed_access = memory_access;
+  out_buffer_mapping->impl.is_persistent = is_persistent ? 1 : 0;
+  out_buffer_mapping->impl.byte_offset = local_byte_offset;
+
   iree_status_t status = _VTABLE_DISPATCH(buffer, map_range)(
-      buffer, mapping_mode, buffer_mapping->allowed_access,
-      buffer_mapping->byte_offset, buffer_mapping->contents.data_length,
-      (void**)&buffer_mapping->contents.data);
+      buffer, mapping_mode, memory_access, out_buffer_mapping->impl.byte_offset,
+      local_byte_length, out_buffer_mapping);
+
+  if (iree_status_is_ok(status)) {
+    // Scoped mappings retain the buffer until unmapped.
+    if (!is_persistent) iree_hal_buffer_retain(buffer);
+  } else {
+    memset(out_buffer_mapping, 0, sizeof(*out_buffer_mapping));
+  }
+
   IREE_TRACE_ZONE_END(z0);
   return status;
 }
 
-IREE_API_EXPORT void iree_hal_buffer_unmap_range(
-    iree_hal_buffer_mapping_t* base_buffer_mapping) {
-  IREE_ASSERT_ARGUMENT(base_buffer_mapping);
-  iree_hal_buffer_mapping_impl_t* buffer_mapping =
-      (iree_hal_buffer_mapping_impl_t*)base_buffer_mapping;
-  iree_hal_buffer_t* buffer = buffer_mapping->backing_buffer;
-  if (!buffer) return;
+IREE_API_EXPORT iree_status_t
+iree_hal_buffer_unmap_range(iree_hal_buffer_mapping_t* buffer_mapping) {
+  IREE_ASSERT_ARGUMENT(buffer_mapping);
+  iree_hal_buffer_t* buffer = buffer_mapping->buffer;
+  if (!buffer) return iree_ok_status();
   IREE_TRACE_ZONE_BEGIN(z0);
-  _VTABLE_DISPATCH(buffer, unmap_range)
-  (buffer, buffer_mapping->byte_offset, buffer_mapping->contents.data_length,
-   buffer_mapping->contents.data);
+
+  iree_status_t status = _VTABLE_DISPATCH(buffer, unmap_range)(
+      buffer, buffer_mapping->impl.byte_offset,
+      buffer_mapping->contents.data_length, buffer_mapping);
+
+  if (!buffer_mapping->impl.is_persistent) {
+    iree_hal_buffer_release(buffer);
+  }
+  memset(buffer_mapping, 0, sizeof(*buffer_mapping));
+
   IREE_TRACE_ZONE_END(z0);
+  return status;
 }
 
 IREE_API_EXPORT iree_status_t iree_hal_buffer_invalidate_range(
-    iree_hal_buffer_mapping_t* base_buffer_mapping,
-    iree_device_size_t byte_offset, iree_device_size_t byte_length) {
-  IREE_ASSERT_ARGUMENT(base_buffer_mapping);
-  iree_hal_buffer_mapping_impl_t* buffer_mapping =
-      (iree_hal_buffer_mapping_impl_t*)base_buffer_mapping;
-  iree_hal_buffer_t* buffer = buffer_mapping->backing_buffer;
+    iree_hal_buffer_mapping_t* buffer_mapping, iree_device_size_t byte_offset,
+    iree_device_size_t byte_length) {
+  IREE_ASSERT_ARGUMENT(buffer_mapping);
+  iree_hal_buffer_t* buffer = buffer_mapping->buffer;
   IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_access(
-      buffer_mapping->allowed_access, IREE_HAL_MEMORY_ACCESS_READ));
+      buffer_mapping->impl.allowed_access, IREE_HAL_MEMORY_ACCESS_READ));
   IREE_RETURN_IF_ERROR(iree_hal_buffer_calculate_range(
-      buffer_mapping->byte_offset, buffer_mapping->contents.data_length,
+      buffer_mapping->impl.byte_offset, buffer_mapping->contents.data_length,
       byte_offset, byte_length, &byte_offset, &byte_length));
   return _VTABLE_DISPATCH(buffer, invalidate_range)(buffer, byte_offset,
                                                     byte_length);
 }
 
 IREE_API_EXPORT iree_status_t iree_hal_buffer_flush_range(
-    iree_hal_buffer_mapping_t* base_buffer_mapping,
-    iree_device_size_t byte_offset, iree_device_size_t byte_length) {
-  IREE_ASSERT_ARGUMENT(base_buffer_mapping);
-  iree_hal_buffer_mapping_impl_t* buffer_mapping =
-      (iree_hal_buffer_mapping_impl_t*)base_buffer_mapping;
-  iree_hal_buffer_t* buffer = buffer_mapping->backing_buffer;
+    iree_hal_buffer_mapping_t* buffer_mapping, iree_device_size_t byte_offset,
+    iree_device_size_t byte_length) {
+  IREE_ASSERT_ARGUMENT(buffer_mapping);
+  iree_hal_buffer_t* buffer = buffer_mapping->buffer;
   IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_access(
-      buffer_mapping->allowed_access, IREE_HAL_MEMORY_ACCESS_WRITE));
+      buffer_mapping->impl.allowed_access, IREE_HAL_MEMORY_ACCESS_WRITE));
   IREE_RETURN_IF_ERROR(iree_hal_buffer_calculate_range(
-      buffer_mapping->byte_offset, buffer_mapping->contents.data_length,
+      buffer_mapping->impl.byte_offset, buffer_mapping->contents.data_length,
       byte_offset, byte_length, &byte_offset, &byte_length));
   return _VTABLE_DISPATCH(buffer, flush_range)(buffer, byte_offset,
                                                byte_length);
 }
 
 IREE_API_EXPORT iree_status_t iree_hal_buffer_mapping_subspan(
-    iree_hal_buffer_mapping_t* base_buffer_mapping,
+    iree_hal_buffer_mapping_t* buffer_mapping,
     iree_hal_memory_access_t memory_access, iree_device_size_t byte_offset,
     iree_device_size_t byte_length, iree_byte_span_t* out_span) {
-  IREE_ASSERT_ARGUMENT(base_buffer_mapping);
-  iree_hal_buffer_mapping_impl_t* buffer_mapping =
-      (iree_hal_buffer_mapping_impl_t*)base_buffer_mapping;
+  IREE_ASSERT_ARGUMENT(buffer_mapping);
   IREE_ASSERT_ARGUMENT(out_span);
   memset(out_span, 0, sizeof(*out_span));
   IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_access(
-      buffer_mapping->allowed_access, memory_access));
-  iree_device_size_t data_length;
+      buffer_mapping->impl.allowed_access, memory_access));
+  iree_device_size_t data_length = 0;
   IREE_RETURN_IF_ERROR(iree_hal_buffer_calculate_range(
       0, buffer_mapping->contents.data_length, byte_offset, byte_length,
       &byte_offset, &data_length));
diff --git a/iree/hal/buffer.h b/iree/hal/buffer.h
index cc355d1..82dfece 100644
--- a/iree/hal/buffer.h
+++ b/iree/hal/buffer.h
@@ -140,7 +140,8 @@
   // accesses will happen via command buffers.
   IREE_HAL_BUFFER_USAGE_TRANSFER = 1u << 1,
 
-  // The buffer can be mapped by the host application for reading and writing.
+  // The buffer can be mapped by the host application for reading and writing
+  // without a copy.
   //
   // As mapping may require placement in special address ranges or system
   // calls to enable visibility the driver can use the presence (or lack of)
@@ -169,12 +170,48 @@
   IREE_HAL_BUFFER_OVERLAP_COMPLETE,
 } iree_hal_buffer_overlap_t;
 
+// A bitfield specifying buffer transfer behavior.
+enum iree_hal_transfer_buffer_flag_bits_t {
+  // TODO(benvanik): flags controlling blocking, flushing, invalidation, and
+  // persistence. We may also want to set a bit that causes failure on emulated
+  // transfers that would otherwise be really expensive.
+  IREE_HAL_TRANSFER_BUFFER_FLAG_DEFAULT = 0,
+};
+typedef uint32_t iree_hal_transfer_buffer_flags_t;
+
+// Determines buffer mapping behavior.
 enum iree_hal_mapping_mode_bits_t {
+  // Buffers are mapped as part of a scoped map-access-unmap sequence.
+  // If there are any in-flight operations using the buffer contents are
+  // undefined though they may deceivingly still seem correct under certain
+  // implementations.
   IREE_HAL_MAPPING_MODE_SCOPED = 1u << 0,
+
+  // Buffers are mapped persistently and concurrently accessible by both the
+  // host and device. Mapping happens once and so long as there are any live
+  // mappings the buffer will remain accessible. Not all implementations or
+  // buffer memory types support this, and even ones that do may not support
+  // coherent cross-device sharing.
   IREE_HAL_MAPPING_MODE_PERSISTENT = 1u << 1,
 };
 typedef uint32_t iree_hal_mapping_mode_t;
 
+// Implementation-specific mapping data.
+typedef struct iree_hal_buffer_mapping_impl_t {
+  // Byte offset within the buffer where the mapped data begins.
+  iree_device_size_t byte_offset;
+  // Used for validation only.
+  iree_hal_memory_access_t allowed_access;
+  // Tracking flags.
+  uint32_t is_persistent : 1;
+  uint32_t reserved_flags : 31;
+  // Backing implementation data.
+  // For backends that require additional tracking (shadow data structures/etc)
+  // this can be used to store references to them for the duration of the
+  // mapping.
+  uint64_t reserved[1];
+} iree_hal_buffer_mapping_impl_t;
+
 // Reference to a buffer's mapped memory.
 typedef struct iree_hal_buffer_mapping_t {
   // Contents of the buffer. Behavior is undefined if an access is performed
@@ -185,8 +222,16 @@
   // accessed.
   iree_byte_span_t contents;
 
+  // Buffer providing the backing storage for the mapping.
+  // When mapped with IREE_HAL_MAPPING_MODE_SCOPED the buffer will be retained
+  // until it is unmapped. When mapped with IREE_HAL_MAPPING_MODE_PERSISTENT the
+  // caller is responsible for retaining the buffer.
+  struct iree_hal_buffer_t* buffer;
+
   // Used internally - do not modify.
-  uint64_t reserved[4];
+  // Implementations are allowed to use the reserved fields for their own
+  // storage but should otherwise ignore the remaining parts.
+  iree_hal_buffer_mapping_impl_t impl;
 } iree_hal_buffer_mapping_t;
 
 // Formats a memory type bitfield as a string.
@@ -389,7 +434,8 @@
 // Copies data from the provided |source_buffer| into the |target_buffer|.
 //
 // Requires that both buffers have the IREE_HAL_BUFFER_USAGE_MAPPING bit set.
-// The byte range in |target_buffer| will be flushed if needed.
+// The byte range in |target_buffer| will be flushed if needed. Both buffers
+// need not come from the same device.
 //
 // It is strongly recommended that buffer operations are performed on transfer
 // queues; using this synchronous function may incur additional cache flushes
@@ -411,16 +457,21 @@
 // invalidate the byte range they want to access to update the visibility of the
 // mapped memory.
 IREE_API_EXPORT iree_status_t iree_hal_buffer_map_range(
-    iree_hal_buffer_t* buffer, iree_hal_memory_access_t memory_access,
-    iree_device_size_t byte_offset, iree_device_size_t byte_length,
+    iree_hal_buffer_t* buffer, iree_hal_mapping_mode_t mapping_mode,
+    iree_hal_memory_access_t memory_access, iree_device_size_t byte_offset,
+    iree_device_size_t byte_length,
     iree_hal_buffer_mapping_t* out_buffer_mapping);
 
 // Unmaps the buffer as was previously mapped to |buffer_mapping|.
 //
 // If the buffer is not IREE_HAL_MEMORY_TYPE_HOST_COHERENT then the caller must
 // flush the byte range they want to make available to other threads/devices.
-IREE_API_EXPORT void iree_hal_buffer_unmap_range(
-    iree_hal_buffer_mapping_t* buffer_mapping);
+//
+// May fail, though unlikely to do so for read-only mapping and the result can
+// be safely ignored using iree_status_ignore. If writing then users must check
+// the status to ensure their writes succeeded.
+IREE_API_EXPORT iree_status_t
+iree_hal_buffer_unmap_range(iree_hal_buffer_mapping_t* buffer_mapping);
 
 // Invalidates ranges of non-coherent memory from the host caches.
 // This guarantees that device writes to the memory ranges provided are
@@ -493,12 +544,12 @@
                                          iree_hal_memory_access_t memory_access,
                                          iree_device_size_t local_byte_offset,
                                          iree_device_size_t local_byte_length,
-                                         void** out_data_ptr);
+                                         iree_hal_buffer_mapping_t* mapping);
 
-  void(IREE_API_PTR* unmap_range)(iree_hal_buffer_t* buffer,
-                                  iree_device_size_t local_byte_offset,
-                                  iree_device_size_t local_byte_length,
-                                  void* data_ptr);
+  iree_status_t(IREE_API_PTR* unmap_range)(iree_hal_buffer_t* buffer,
+                                           iree_device_size_t local_byte_offset,
+                                           iree_device_size_t local_byte_length,
+                                           iree_hal_buffer_mapping_t* mapping);
 
   iree_status_t(IREE_API_PTR* invalidate_range)(
       iree_hal_buffer_t* buffer, iree_device_size_t local_byte_offset,
diff --git a/iree/hal/buffer_heap.c b/iree/hal/buffer_heap.c
index 6649a8c..7486af2 100644
--- a/iree/hal/buffer_heap.c
+++ b/iree/hal/buffer_heap.c
@@ -174,9 +174,10 @@
     iree_hal_buffer_t* base_buffer, iree_hal_mapping_mode_t mapping_mode,
     iree_hal_memory_access_t memory_access,
     iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
-    void** out_data_ptr) {
+    iree_hal_buffer_mapping_t* mapping) {
   iree_hal_heap_buffer_t* buffer = (iree_hal_heap_buffer_t*)base_buffer;
-  *out_data_ptr = buffer->data.data + local_byte_offset;
+  mapping->contents = iree_make_byte_span(buffer->data.data + local_byte_offset,
+                                          local_byte_length);
 
   // If we mapped for discard scribble over the bytes. This is not a mandated
   // behavior but it will make debugging issues easier. Alternatively for
@@ -184,17 +185,18 @@
   // would only work if the entire buffer was discarded.
 #ifndef NDEBUG
   if (iree_any_bit_set(memory_access, IREE_HAL_MEMORY_ACCESS_DISCARD)) {
-    memset(*out_data_ptr, 0xCD, local_byte_length);
+    memset(mapping->contents.data, 0xCD, local_byte_length);
   }
 #endif  // !NDEBUG
 
   return iree_ok_status();
 }
 
-static void iree_hal_heap_buffer_unmap_range(
+static iree_status_t iree_hal_heap_buffer_unmap_range(
     iree_hal_buffer_t* base_buffer, iree_device_size_t local_byte_offset,
-    iree_device_size_t local_byte_length, void* data_ptr) {
+    iree_device_size_t local_byte_length, iree_hal_buffer_mapping_t* mapping) {
   // No-op here as we always have the pointer.
+  return iree_ok_status();
 }
 
 static iree_status_t iree_hal_heap_buffer_invalidate_range(
diff --git a/iree/hal/buffer_view.c b/iree/hal/buffer_view.c
index 3c32045..c338235 100644
--- a/iree/hal/buffer_view.c
+++ b/iree/hal/buffer_view.c
@@ -6,14 +6,11 @@
 
 #include "iree/hal/buffer_view.h"
 
-#include <inttypes.h>
-#include <stdbool.h>
-
 #include "iree/base/api.h"
 #include "iree/base/tracing.h"
 #include "iree/hal/allocator.h"
+#include "iree/hal/buffer_view_util.h"
 #include "iree/hal/resource.h"
-#include "iree/hal/string_util.h"
 
 struct iree_hal_buffer_view_t {
   iree_atomic_ref_count_t ref_count;
@@ -94,131 +91,6 @@
   IREE_TRACE_ZONE_END(z0);
 }
 
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_allocate_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_buffer_usage_t allowed_usage,
-    iree_hal_buffer_view_t** out_buffer_view) {
-  IREE_ASSERT_ARGUMENT(allocator);
-  IREE_ASSERT_ARGUMENT(out_buffer_view);
-  IREE_TRACE_ZONE_BEGIN(z0);
-
-  iree_device_size_t allocation_size = 0;
-  iree_status_t status = iree_hal_buffer_compute_view_size(
-      shape, shape_rank, element_type, encoding_type, &allocation_size);
-
-  iree_hal_buffer_t* buffer = NULL;
-  if (iree_status_is_ok(status)) {
-    status = iree_hal_allocator_allocate_buffer(
-        allocator, memory_type, allowed_usage, allocation_size, &buffer);
-  }
-
-  if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_create(
-        buffer, shape, shape_rank, element_type, encoding_type,
-        iree_hal_allocator_host_allocator(allocator), out_buffer_view);
-  }
-
-  iree_hal_buffer_release(buffer);
-  IREE_TRACE_ZONE_END(z0);
-  return status;
-}
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_clone_heap_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_buffer_usage_t allowed_usage, iree_const_byte_span_t data,
-    iree_hal_buffer_view_t** out_buffer_view) {
-  IREE_ASSERT_ARGUMENT(allocator);
-  IREE_ASSERT_ARGUMENT(out_buffer_view);
-  IREE_TRACE_ZONE_BEGIN(z0);
-
-  // Allocate the buffer.
-  iree_hal_buffer_view_t* buffer_view = NULL;
-  IREE_RETURN_AND_END_ZONE_IF_ERROR(
-      z0, iree_hal_buffer_view_allocate_buffer(
-              allocator, shape, shape_rank, element_type, encoding_type,
-              memory_type, allowed_usage, &buffer_view));
-
-  // Copy all of the data into it in the worst way possible.
-  // If you find yourself coming here from profiling:
-  //   Don't clone data. Allocate and then populate it in-place.
-  //   -or-
-  //   Schedule sequences of DMA transfers using iree_hal_command_buffer_t.
-  iree_status_t status = iree_hal_buffer_write_data(
-      iree_hal_buffer_view_buffer(buffer_view), 0, data.data, data.data_length);
-
-  if (iree_status_is_ok(status)) {
-    *out_buffer_view = buffer_view;
-  } else {
-    iree_hal_buffer_view_release(buffer_view);
-  }
-  IREE_TRACE_ZONE_END(z0);
-  return status;
-}
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_heap_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_memory_access_t allowed_access,
-    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
-    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view) {
-  IREE_ASSERT_ARGUMENT(allocator);
-  IREE_ASSERT_ARGUMENT(out_buffer_view);
-  IREE_TRACE_ZONE_BEGIN(z0);
-
-  // NOTE: this will fail if the data cannot be imported into the allocator.
-  iree_hal_buffer_t* buffer = NULL;
-  iree_status_t status = iree_hal_allocator_wrap_buffer(
-      allocator, memory_type, allowed_access, allowed_usage, data,
-      data_allocator, &buffer);
-
-  if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_create(
-        buffer, shape, shape_rank, element_type, encoding_type,
-        iree_hal_allocator_host_allocator(allocator), out_buffer_view);
-  }
-
-  iree_hal_buffer_release(buffer);
-  IREE_TRACE_ZONE_END(z0);
-  return status;
-}
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_or_clone_heap_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_memory_access_t allowed_access,
-    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
-    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view) {
-  IREE_ASSERT_ARGUMENT(allocator);
-
-  // Not all HAL implementations support wrapping buffers, and of those that do
-  // some may only support it in special situations such as when the buffer is
-  // not DEVICE_VISIBLE. The user application can query whether the wrapping is
-  // possible and decide to use alternative means of upload if it is not; we
-  // make no policy (other than validity) over what's best here.
-  iree_hal_buffer_compatibility_t compatibility =
-      iree_hal_allocator_query_buffer_compatibility(
-          allocator, memory_type, allowed_usage, IREE_HAL_BUFFER_USAGE_MAPPING,
-          (iree_device_size_t)data.data_length);
-  bool wrap_allowed = iree_all_bits_set(
-      compatibility, IREE_HAL_BUFFER_COMPATIBILITY_IMPORTABLE);
-  if (wrap_allowed) {
-    return iree_hal_buffer_view_wrap_heap_buffer(
-        allocator, shape, shape_rank, element_type, encoding_type, memory_type,
-        allowed_access, allowed_usage, data, data_allocator, out_buffer_view);
-  } else {
-    return iree_hal_buffer_view_clone_heap_buffer(
-        allocator, shape, shape_rank, element_type, encoding_type, memory_type,
-        allowed_usage, iree_make_const_byte_span(data.data, data.data_length),
-        out_buffer_view);
-  }
-}
-
 IREE_API_EXPORT iree_hal_buffer_t* iree_hal_buffer_view_buffer(
     const iree_hal_buffer_view_t* buffer_view) {
   IREE_ASSERT_ARGUMENT(buffer_view);
@@ -287,6 +159,10 @@
   IREE_ASSERT_ARGUMENT(shape);
 
   if (shape_rank != buffer_view->shape_rank) {
+    // Rank changes require reallocation of the structure as we inline the
+    // shape dimensions. We could lighten this restriction to allow for rank
+    // reduction but knowing that rank changes aren't allowed is easier than
+    // remembering all the conditions in which they may be.
     return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                             "buffer view reshapes must have the same rank; "
                             "target=%zu, existing=%zu",
@@ -357,406 +233,3 @@
       buffer_view->encoding_type, start_indices, indices_count, lengths,
       lengths_count, out_start_offset, out_length);
 }
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_size(
-    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
-    iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type,
-    iree_device_size_t* out_allocation_size) {
-  IREE_ASSERT_ARGUMENT(!shape_rank || shape);
-  IREE_ASSERT_ARGUMENT(out_allocation_size);
-  *out_allocation_size = 0;
-
-  iree_device_size_t byte_length = 0;
-
-  switch (encoding_type) {
-    case IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR: {
-      if (IREE_UNLIKELY(iree_hal_element_bit_count(element_type) == 0) ||
-          IREE_UNLIKELY(!iree_hal_element_is_byte_aligned(element_type))) {
-        return iree_make_status(
-            IREE_STATUS_INVALID_ARGUMENT,
-            "opaque and sub-byte aligned element types cannot be indexed");
-      }
-      byte_length = iree_hal_element_dense_byte_count(element_type);
-      for (iree_host_size_t i = 0; i < shape_rank; ++i) {
-        byte_length *= shape[i];
-      }
-      break;
-    }
-    default:
-      return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
-                              "unimplemented encoding type size calculation");
-  }
-
-  *out_allocation_size = byte_length;
-  return iree_ok_status();
-}
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_offset(
-    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
-    iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* indices,
-    iree_host_size_t indices_count, iree_device_size_t* out_offset) {
-  IREE_ASSERT_ARGUMENT(shape);
-  IREE_ASSERT_ARGUMENT(indices);
-  IREE_ASSERT_ARGUMENT(out_offset);
-  *out_offset = 0;
-  if (IREE_UNLIKELY(encoding_type != IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR)) {
-    return iree_make_status(
-        IREE_STATUS_INVALID_ARGUMENT,
-        "only dense encodings support view range computation");
-  } else if (IREE_UNLIKELY(iree_hal_element_bit_count(element_type) == 0) ||
-             IREE_UNLIKELY(!iree_hal_element_is_byte_aligned(element_type))) {
-    return iree_make_status(
-        IREE_STATUS_INVALID_ARGUMENT,
-        "opaque and sub-byte aligned element types cannot be indexed");
-  } else if (IREE_UNLIKELY(shape_rank != indices_count)) {
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "shape rank/indices mismatch: %zu != %zu",
-                            shape_rank, indices_count);
-  }
-
-  iree_device_size_t offset = 0;
-  for (iree_host_size_t i = 0; i < indices_count; ++i) {
-    if (IREE_UNLIKELY(indices[i] >= shape[i])) {
-      return iree_make_status(IREE_STATUS_OUT_OF_RANGE,
-                              "index[%zu] out of bounds: %d >= %d", i,
-                              indices[i], shape[i]);
-    }
-    iree_device_size_t axis_offset = indices[i];
-    for (iree_host_size_t j = i + 1; j < shape_rank; ++j) {
-      axis_offset *= shape[j];
-    }
-    offset += axis_offset;
-  }
-  offset *= iree_hal_element_dense_byte_count(element_type);
-
-  *out_offset = offset;
-  return iree_ok_status();
-}
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_range(
-    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
-    iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* start_indices,
-    iree_host_size_t indices_count, const iree_hal_dim_t* lengths,
-    iree_host_size_t lengths_count, iree_device_size_t* out_start_offset,
-    iree_device_size_t* out_length) {
-  IREE_ASSERT_ARGUMENT(shape);
-  IREE_ASSERT_ARGUMENT(start_indices);
-  IREE_ASSERT_ARGUMENT(lengths);
-  IREE_ASSERT_ARGUMENT(out_start_offset);
-  IREE_ASSERT_ARGUMENT(out_length);
-  *out_start_offset = 0;
-  *out_length = 0;
-  if (IREE_UNLIKELY(encoding_type != IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR)) {
-    return iree_make_status(
-        IREE_STATUS_INVALID_ARGUMENT,
-        "only dense encodings support view range computation");
-  } else if (IREE_UNLIKELY(iree_hal_element_bit_count(element_type) == 0) ||
-             IREE_UNLIKELY(!iree_hal_element_is_byte_aligned(element_type))) {
-    return iree_make_status(
-        IREE_STATUS_INVALID_ARGUMENT,
-        "opaque and sub-byte aligned element types cannot be indexed");
-  } else if (IREE_UNLIKELY(indices_count != lengths_count)) {
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "indices/lengths mismatch: %zu != %zu",
-                            indices_count, lengths_count);
-  } else if (IREE_UNLIKELY(shape_rank != indices_count)) {
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                            "shape rank/indices mismatch: %zu != %zu",
-                            shape_rank, indices_count);
-  }
-
-  iree_hal_dim_t* end_indices =
-      iree_alloca(shape_rank * sizeof(iree_hal_dim_t));
-  iree_device_size_t element_size =
-      iree_hal_element_dense_byte_count(element_type);
-  iree_device_size_t subspan_length = element_size;
-  for (iree_host_size_t i = 0; i < lengths_count; ++i) {
-    subspan_length *= lengths[i];
-    end_indices[i] = start_indices[i] + lengths[i] - 1;
-  }
-
-  iree_device_size_t start_byte_offset = 0;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_compute_view_offset(
-      shape, shape_rank, element_type, encoding_type, start_indices,
-      indices_count, &start_byte_offset));
-  iree_device_size_t end_byte_offset = 0;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_compute_view_offset(
-      shape, shape_rank, element_type, encoding_type, end_indices, shape_rank,
-      &end_byte_offset));
-
-  // Non-contiguous regions not yet implemented. Will be easier to detect when
-  // we have strides.
-  iree_device_size_t offset_length =
-      end_byte_offset - start_byte_offset + element_size;
-  if (subspan_length != offset_length) {
-    return iree_make_status(
-        IREE_STATUS_UNIMPLEMENTED,
-        "non-contiguous range region computation not implemented");
-  }
-
-  *out_start_offset = start_byte_offset;
-  *out_length = subspan_length;
-  return iree_ok_status();
-}
-
-static iree_status_t iree_hal_buffer_view_parse_impl(
-    iree_string_view_t value, iree_hal_allocator_t* buffer_allocator,
-    iree_hal_buffer_view_t** out_buffer_view) {
-  // Strip whitespace that may come along (linefeeds/etc).
-  value = iree_string_view_trim(value);
-  value = iree_string_view_strip_prefix(value, IREE_SV("\""));
-  value = iree_string_view_strip_suffix(value, IREE_SV("\""));
-  if (iree_string_view_is_empty(value)) {
-    // Empty lines are invalid; need at least the shape/type information.
-    *out_buffer_view = NULL;
-    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, "empty string input");
-  }
-
-  // The part of the string corresponding to the shape, e.g. 1x2x3.
-  iree_string_view_t shape_str = iree_string_view_empty();
-  // The part of the string corresponding to the type, e.g. f32
-  iree_string_view_t type_str = iree_string_view_empty();
-  // The part of the string corresponding to the buffer data, e.g. 1 2 3 4 5 6
-  iree_string_view_t data_str = iree_string_view_empty();
-
-  iree_string_view_t shape_and_type_str = value;
-  iree_string_view_split(value, '=', &shape_and_type_str, &data_str);
-  iree_host_size_t last_x_index = iree_string_view_find_last_of(
-      shape_and_type_str, IREE_SV("x"), IREE_STRING_VIEW_NPOS);
-  if (last_x_index == IREE_STRING_VIEW_NPOS) {
-    // Scalar.
-    type_str = shape_and_type_str;
-  } else {
-    // Has a shape.
-    shape_str = iree_string_view_substr(shape_and_type_str, 0, last_x_index);
-    type_str = iree_string_view_substr(shape_and_type_str, last_x_index + 1,
-                                       IREE_STRING_VIEW_NPOS);
-  }
-
-  // AxBxC...
-  iree_host_size_t shape_rank = 0;
-  iree_status_t shape_result =
-      iree_hal_parse_shape(shape_str, 0, NULL, &shape_rank);
-  if (!iree_status_is_ok(shape_result) &&
-      !iree_status_is_out_of_range(shape_result)) {
-    return shape_result;
-  } else if (shape_rank > 128) {
-    return iree_make_status(
-        IREE_STATUS_RESOURCE_EXHAUSTED,
-        "a shape rank of %zu is just a little bit excessive, eh?", shape_rank);
-  }
-  shape_result = iree_status_ignore(shape_result);
-  iree_hal_dim_t* shape =
-      (iree_hal_dim_t*)iree_alloca(shape_rank * sizeof(iree_hal_dim_t));
-  IREE_RETURN_IF_ERROR(
-      iree_hal_parse_shape(shape_str, shape_rank, shape, &shape_rank));
-
-  // f32, i32, etc
-  iree_hal_element_type_t element_type = IREE_HAL_ELEMENT_TYPE_NONE;
-  IREE_RETURN_IF_ERROR(iree_hal_parse_element_type(type_str, &element_type));
-
-  // TODO(benvanik): allow specifying the encoding.
-  iree_hal_encoding_type_t encoding_type =
-      IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR;
-
-  // Allocate the buffer we will parse into from the provided allocator.
-  iree_device_size_t buffer_length = 0;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_compute_view_size(
-      shape, shape_rank, element_type, encoding_type, &buffer_length));
-  iree_hal_buffer_t* buffer = NULL;
-  IREE_RETURN_IF_ERROR(iree_hal_allocator_allocate_buffer(
-      buffer_allocator,
-      IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
-      IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING |
-          IREE_HAL_BUFFER_USAGE_DISPATCH,
-      buffer_length, &buffer));
-
-  // Parse the elements directly into the buffer.
-  iree_hal_buffer_mapping_t buffer_mapping;
-  iree_status_t status =
-      iree_hal_buffer_map_range(buffer, IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, 0,
-                                buffer_length, &buffer_mapping);
-  if (!iree_status_is_ok(status)) {
-    iree_hal_buffer_release(buffer);
-    return status;
-  }
-  status = iree_hal_parse_buffer_elements(data_str, element_type,
-                                          buffer_mapping.contents);
-  iree_hal_buffer_unmap_range(&buffer_mapping);
-  if (!iree_status_is_ok(status)) {
-    iree_hal_buffer_release(buffer);
-    return status;
-  }
-
-  // Wrap and pass ownership of the buffer to the buffer view.
-  status = iree_hal_buffer_view_create(
-      buffer, shape, shape_rank, element_type, encoding_type,
-      iree_hal_allocator_host_allocator(buffer_allocator), out_buffer_view);
-  iree_hal_buffer_release(buffer);
-  return status;
-}
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_parse(
-    iree_string_view_t value, iree_hal_allocator_t* buffer_allocator,
-    iree_hal_buffer_view_t** out_buffer_view) {
-  IREE_ASSERT_ARGUMENT(buffer_allocator);
-  IREE_ASSERT_ARGUMENT(out_buffer_view);
-  *out_buffer_view = NULL;
-  IREE_TRACE_ZONE_BEGIN(z0);
-  iree_status_t status =
-      iree_hal_buffer_view_parse_impl(value, buffer_allocator, out_buffer_view);
-  IREE_TRACE_ZONE_END(z0);
-  return status;
-}
-
-#define APPEND_CHAR(c)                           \
-  {                                              \
-    if (buffer) {                                \
-      if (buffer_length < buffer_capacity - 1) { \
-        buffer[buffer_length] = c;               \
-        buffer[buffer_length + 1] = '\0';        \
-      } else {                                   \
-        buffer = NULL;                           \
-      }                                          \
-    }                                            \
-    ++buffer_length;                             \
-  }
-
-static iree_status_t iree_hal_buffer_view_format_impl(
-    const iree_hal_buffer_view_t* buffer_view,
-    iree_host_size_t max_element_count, iree_host_size_t buffer_capacity,
-    char* buffer, iree_host_size_t* out_buffer_length) {
-  if (out_buffer_length) {
-    *out_buffer_length = 0;
-  }
-  if (buffer && buffer_capacity) {
-    buffer[0] = 0;
-  }
-
-  iree_host_size_t buffer_length = 0;
-  if (iree_hal_buffer_view_shape_rank(buffer_view) > 0) {
-    // Shape: 1x2x3
-    iree_host_size_t shape_length = 0;
-    iree_status_t status = iree_hal_format_shape(
-        iree_hal_buffer_view_shape_dims(buffer_view),
-        iree_hal_buffer_view_shape_rank(buffer_view),
-        buffer ? buffer_capacity - buffer_length : 0,
-        buffer ? buffer + buffer_length : NULL, &shape_length);
-    buffer_length += shape_length;
-    if (iree_status_is_out_of_range(status)) {
-      status = iree_status_ignore(status);
-      buffer = NULL;
-    } else if (!iree_status_is_ok(status)) {
-      return status;
-    }
-
-    // Separator: <shape>x<format>
-    APPEND_CHAR('x');
-  }
-
-  // Element type: f32
-  iree_host_size_t element_type_length = 0;
-  iree_status_t status = iree_hal_format_element_type(
-      iree_hal_buffer_view_element_type(buffer_view),
-      buffer ? buffer_capacity - buffer_length : 0,
-      buffer ? buffer + buffer_length : NULL, &element_type_length);
-  buffer_length += element_type_length;
-  if (iree_status_is_out_of_range(status)) {
-    status = iree_status_ignore(status);
-    buffer = NULL;
-  } else if (!iree_status_is_ok(status)) {
-    return status;
-  }
-
-  // TODO(benvanik): allow printing the encoding.
-
-  // Separator: <meta>=<value>
-  APPEND_CHAR('=');
-
-  // Buffer contents: 0 1 2 3 ...
-  iree_hal_buffer_mapping_t buffer_mapping;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-      iree_hal_buffer_view_buffer(buffer_view), IREE_HAL_MEMORY_ACCESS_READ, 0,
-      IREE_WHOLE_BUFFER, &buffer_mapping));
-  iree_host_size_t elements_length = 0;
-  status = iree_hal_format_buffer_elements(
-      iree_make_const_byte_span(buffer_mapping.contents.data,
-                                buffer_mapping.contents.data_length),
-      iree_hal_buffer_view_shape_dims(buffer_view),
-      iree_hal_buffer_view_shape_rank(buffer_view),
-      iree_hal_buffer_view_element_type(buffer_view), max_element_count,
-      buffer ? buffer_capacity - buffer_length : 0,
-      buffer ? buffer + buffer_length : NULL, &elements_length);
-  buffer_length += elements_length;
-  iree_hal_buffer_unmap_range(&buffer_mapping);
-  if (iree_status_is_out_of_range(status)) {
-    status = iree_status_ignore(status);
-    buffer = NULL;
-  } else if (!iree_status_is_ok(status)) {
-    return status;
-  }
-
-  if (out_buffer_length) {
-    *out_buffer_length = buffer_length;
-  }
-  return buffer ? iree_ok_status()
-                : iree_status_from_code(IREE_STATUS_OUT_OF_RANGE);
-}
-
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_format(
-    const iree_hal_buffer_view_t* buffer_view,
-    iree_host_size_t max_element_count, iree_host_size_t buffer_capacity,
-    char* buffer, iree_host_size_t* out_buffer_length) {
-  IREE_ASSERT_ARGUMENT(buffer_view);
-  IREE_TRACE_ZONE_BEGIN(z0);
-  iree_status_t status = iree_hal_buffer_view_format_impl(
-      buffer_view, max_element_count, buffer_capacity, buffer,
-      out_buffer_length);
-  IREE_TRACE_ZONE_END(z0);
-  return status;
-}
-
-// TODO(benvanik): streaming all the way down (needs string_util updates).
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_fprint(
-    FILE* file, const iree_hal_buffer_view_t* buffer_view,
-    iree_host_size_t max_element_count) {
-  IREE_ASSERT_ARGUMENT(file);
-  IREE_ASSERT_ARGUMENT(buffer_view);
-  IREE_TRACE_ZONE_BEGIN(z0);
-
-  // Query the string length (in characters).
-  iree_host_size_t buffer_length = 0;
-  iree_status_t status = iree_hal_buffer_view_format(
-      buffer_view, max_element_count, 0, NULL, &buffer_length);
-  if (!iree_status_is_out_of_range(status)) {
-    IREE_TRACE_ZONE_END(z0);
-    return status;
-  }
-
-  // Allocate scratch space to format in to.
-  // We should be streaming.
-  iree_allocator_t host_allocator = buffer_view->host_allocator;
-  iree_host_size_t buffer_capacity = buffer_length + 1;  // NUL
-  char* buffer = NULL;
-  status =
-      iree_allocator_malloc(host_allocator, buffer_capacity, (void**)&buffer);
-
-  // Format the buffer into the string storage.
-  if (iree_status_is_ok(status)) {
-    status =
-        iree_hal_buffer_view_format(buffer_view, max_element_count,
-                                    buffer_capacity, buffer, &buffer_length);
-  }
-
-  // Dump to the file.
-  if (iree_status_is_ok(status)) {
-    fprintf(file, "%.*s", (int)buffer_length, buffer);
-  }
-
-  iree_allocator_free(host_allocator, buffer);
-  IREE_TRACE_ZONE_END(z0);
-  return status;
-}
diff --git a/iree/hal/buffer_view.h b/iree/hal/buffer_view.h
index 9a0c623..a6ee1d6 100644
--- a/iree/hal/buffer_view.h
+++ b/iree/hal/buffer_view.h
@@ -7,9 +7,7 @@
 #ifndef IREE_HAL_BUFFER_VIEW_H_
 #define IREE_HAL_BUFFER_VIEW_H_
 
-#include <stdbool.h>
 #include <stdint.h>
-#include <stdio.h>
 
 #include "iree/base/api.h"
 #include "iree/hal/buffer.h"
@@ -156,35 +154,6 @@
 typedef int32_t iree_hal_dim_t;
 
 //===----------------------------------------------------------------------===//
-// Buffer view math
-//===----------------------------------------------------------------------===//
-
-// Calculates the allocation size of a buffer view.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_size(
-    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
-    iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type,
-    iree_device_size_t* out_allocation_size);
-
-// Calculates a byte offset into a buffer at the given indices.
-// Only works with densely-packed representations.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_offset(
-    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
-    iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* indices,
-    size_t indices_count, iree_device_size_t* out_offset);
-
-// Calculates a byte range into a buffer of the given contiguous range.
-// Only works with densely-packed representations.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_range(
-    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
-    iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* start_indices,
-    iree_host_size_t indices_count, const iree_hal_dim_t* lengths,
-    iree_host_size_t lengths_count, iree_device_size_t* out_start_offset,
-    iree_device_size_t* out_length);
-
-//===----------------------------------------------------------------------===//
 // iree_hal_buffer_view_t
 //===----------------------------------------------------------------------===//
 
@@ -204,69 +173,6 @@
     iree_hal_encoding_type_t encoding_type, iree_allocator_t host_allocator,
     iree_hal_buffer_view_t** out_buffer_view);
 
-// Allocates a buffer from |allocator| and wraps it in a buffer view.
-// This is equivalent to:
-//   1. iree_hal_buffer_compute_view_size
-//   2. iree_hal_allocator_allocate_buffer
-//   3. iree_hal_buffer_view_create
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_allocate_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_buffer_usage_t allowed_usage,
-    iree_hal_buffer_view_t** out_buffer_view);
-
-// Clones a host buffer using |allocator| and wraps it in a buffer view.
-// This is equivalent to:
-//   1. iree_hal_allocator_allocate_buffer
-//   2. iree_hal_buffer_write_data
-//   3. iree_hal_buffer_view_create
-//
-// Always prefer allocating a device buffer and populating it in place.
-// If cloning multiple buffers it is better to use iree_hal_command_buffer_ts to
-// batch up the memory transfer operations.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_clone_heap_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_buffer_usage_t allowed_usage, iree_const_byte_span_t data,
-    iree_hal_buffer_view_t** out_buffer_view);
-
-// Imports a host buffer using |allocator| and wraps it in a buffer view.
-// This is equivalent to:
-//   1. iree_hal_allocator_wrap_buffer
-//   2. iree_hal_buffer_view_create
-//
-// NOTE: not all buffers can be imported and not all allocators support
-// importing. See iree_hal_allocator_wrap_buffer for more information.
-// Fails if the buffer cannot be imported.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_heap_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_memory_access_t allowed_access,
-    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
-    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view);
-
-// Tries to import a host buffer using |allocator| and wrap it in a buffer view.
-// If the buffer cannot be imported then a new buffer will be allocated and the
-// source data will be copied into it.
-// This is equivalent to:
-//   if iree_hal_allocator_query_buffer_compatibility ok:
-//     1. iree_hal_allocator_wrap_buffer
-//     2. iree_hal_buffer_view_create
-//   else:
-//     1. iree_hal_allocator_allocate_buffer
-//     2. iree_hal_buffer_write_data
-//     3. iree_hal_buffer_view_create
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_or_clone_heap_buffer(
-    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
-    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
-    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
-    iree_hal_memory_access_t allowed_access,
-    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
-    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view);
-
 // Retains the given |buffer_view| for the caller.
 IREE_API_EXPORT void iree_hal_buffer_view_retain(
     iree_hal_buffer_view_t* buffer_view);
@@ -351,42 +257,6 @@
     const iree_hal_dim_t* lengths, iree_host_size_t lengths_count,
     iree_device_size_t* out_start_offset, iree_device_size_t* out_length);
 
-// Parses a serialized set of buffer elements in the canonical tensor format
-// (the same as produced by iree_hal_buffer_view_format). The underlying buffer
-// will be allocated with |buffer_allocator| as a host-local/device-visible
-// buffer.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_parse(
-    iree_string_view_t value, iree_hal_allocator_t* buffer_allocator,
-    iree_hal_buffer_view_t** out_buffer_view);
-
-// TODO(#5413): enum for printing mode (include shape, precision).
-
-// Converts buffer view elements into a fully-specified string-form format like
-// `2x4xi16=[[1 2][3 4]]`.
-//
-// |max_element_count| can be used to limit the total number of elements printed
-// when the count may be large. Elided elements will be replaced with `...`.
-//
-// |buffer_capacity| defines the size of |buffer| in bytes and
-// |out_buffer_length| will return the string length in characters. Returns
-// IREE_STATUS_OUT_OF_RANGE if the buffer capacity is insufficient to hold the
-// formatted elements and |out_buffer_length| will contain the required size.
-//
-// Follows the standard API string formatting rules. See iree/base/api.h.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_format(
-    const iree_hal_buffer_view_t* buffer_view,
-    iree_host_size_t max_element_count, iree_host_size_t buffer_capacity,
-    char* buffer, iree_host_size_t* out_buffer_length);
-
-// Prints buffer view elements into a fully-specified string-form format like
-// `2x4xi16=[[1 2][3 4]]`.
-//
-// |max_element_count| can be used to limit the total number of elements printed
-// when the count may be large. Elided elements will be replaced with `...`.
-IREE_API_EXPORT iree_status_t iree_hal_buffer_view_fprint(
-    FILE* file, const iree_hal_buffer_view_t* buffer_view,
-    iree_host_size_t max_element_count);
-
 //===----------------------------------------------------------------------===//
 // iree_hal_buffer_view_t implementation details
 //===----------------------------------------------------------------------===//
diff --git a/iree/hal/buffer_view_util.c b/iree/hal/buffer_view_util.c
new file mode 100644
index 0000000..ae2cdac
--- /dev/null
+++ b/iree/hal/buffer_view_util.c
@@ -0,0 +1,626 @@
+// Copyright 2020 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/buffer_view_util.h"
+
+#include <inttypes.h>
+#include <stdbool.h>
+
+#include "iree/base/api.h"
+#include "iree/base/tracing.h"
+#include "iree/hal/allocator.h"
+#include "iree/hal/resource.h"
+#include "iree/hal/string_util.h"
+
+//===----------------------------------------------------------------------===//
+// Buffer view math
+//===----------------------------------------------------------------------===//
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_size(
+    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
+    iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type,
+    iree_device_size_t* out_allocation_size) {
+  IREE_ASSERT_ARGUMENT(!shape_rank || shape);
+  IREE_ASSERT_ARGUMENT(out_allocation_size);
+  *out_allocation_size = 0;
+
+  iree_device_size_t byte_length = 0;
+
+  switch (encoding_type) {
+    case IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR: {
+      if (IREE_UNLIKELY(iree_hal_element_bit_count(element_type) == 0) ||
+          IREE_UNLIKELY(!iree_hal_element_is_byte_aligned(element_type))) {
+        return iree_make_status(
+            IREE_STATUS_INVALID_ARGUMENT,
+            "opaque and sub-byte aligned element types cannot be indexed");
+      }
+      byte_length = iree_hal_element_dense_byte_count(element_type);
+      for (iree_host_size_t i = 0; i < shape_rank; ++i) {
+        byte_length *= shape[i];
+      }
+      break;
+    }
+    default:
+      return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+                              "unimplemented encoding type size calculation");
+  }
+
+  *out_allocation_size = byte_length;
+  return iree_ok_status();
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_offset(
+    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
+    iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* indices,
+    iree_host_size_t indices_count, iree_device_size_t* out_offset) {
+  IREE_ASSERT_ARGUMENT(shape);
+  IREE_ASSERT_ARGUMENT(indices);
+  IREE_ASSERT_ARGUMENT(out_offset);
+  *out_offset = 0;
+  if (IREE_UNLIKELY(encoding_type != IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR)) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "only dense encodings support view range computation");
+  } else if (IREE_UNLIKELY(iree_hal_element_bit_count(element_type) == 0) ||
+             IREE_UNLIKELY(!iree_hal_element_is_byte_aligned(element_type))) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "opaque and sub-byte aligned element types cannot be indexed");
+  } else if (IREE_UNLIKELY(shape_rank != indices_count)) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "shape rank/indices mismatch: %zu != %zu",
+                            shape_rank, indices_count);
+  }
+
+  iree_device_size_t offset = 0;
+  for (iree_host_size_t i = 0; i < indices_count; ++i) {
+    if (IREE_UNLIKELY(indices[i] >= shape[i])) {
+      return iree_make_status(IREE_STATUS_OUT_OF_RANGE,
+                              "index[%zu] out of bounds: %d >= %d", i,
+                              indices[i], shape[i]);
+    }
+    iree_device_size_t axis_offset = indices[i];
+    for (iree_host_size_t j = i + 1; j < shape_rank; ++j) {
+      axis_offset *= shape[j];
+    }
+    offset += axis_offset;
+  }
+  offset *= iree_hal_element_dense_byte_count(element_type);
+
+  *out_offset = offset;
+  return iree_ok_status();
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_range(
+    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
+    iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* start_indices,
+    iree_host_size_t indices_count, const iree_hal_dim_t* lengths,
+    iree_host_size_t lengths_count, iree_device_size_t* out_start_offset,
+    iree_device_size_t* out_length) {
+  IREE_ASSERT_ARGUMENT(shape);
+  IREE_ASSERT_ARGUMENT(start_indices);
+  IREE_ASSERT_ARGUMENT(lengths);
+  IREE_ASSERT_ARGUMENT(out_start_offset);
+  IREE_ASSERT_ARGUMENT(out_length);
+  *out_start_offset = 0;
+  *out_length = 0;
+  if (IREE_UNLIKELY(encoding_type != IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR)) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "only dense encodings support view range computation");
+  } else if (IREE_UNLIKELY(iree_hal_element_bit_count(element_type) == 0) ||
+             IREE_UNLIKELY(!iree_hal_element_is_byte_aligned(element_type))) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "opaque and sub-byte aligned element types cannot be indexed");
+  } else if (IREE_UNLIKELY(indices_count != lengths_count)) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "indices/lengths mismatch: %zu != %zu",
+                            indices_count, lengths_count);
+  } else if (IREE_UNLIKELY(shape_rank != indices_count)) {
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                            "shape rank/indices mismatch: %zu != %zu",
+                            shape_rank, indices_count);
+  }
+
+  iree_hal_dim_t* end_indices =
+      iree_alloca(shape_rank * sizeof(iree_hal_dim_t));
+  iree_device_size_t element_size =
+      iree_hal_element_dense_byte_count(element_type);
+  iree_device_size_t subspan_length = element_size;
+  for (iree_host_size_t i = 0; i < lengths_count; ++i) {
+    subspan_length *= lengths[i];
+    end_indices[i] = start_indices[i] + lengths[i] - 1;
+  }
+
+  iree_device_size_t start_byte_offset = 0;
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_compute_view_offset(
+      shape, shape_rank, element_type, encoding_type, start_indices,
+      indices_count, &start_byte_offset));
+  iree_device_size_t end_byte_offset = 0;
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_compute_view_offset(
+      shape, shape_rank, element_type, encoding_type, end_indices, shape_rank,
+      &end_byte_offset));
+
+  // Non-contiguous regions not yet implemented. Will be easier to detect when
+  // we have strides.
+  iree_device_size_t offset_length =
+      end_byte_offset - start_byte_offset + element_size;
+  if (subspan_length != offset_length) {
+    return iree_make_status(
+        IREE_STATUS_UNIMPLEMENTED,
+        "non-contiguous range region computation not implemented");
+  }
+
+  *out_start_offset = start_byte_offset;
+  *out_length = subspan_length;
+  return iree_ok_status();
+}
+
+//===----------------------------------------------------------------------===//
+// Buffer view allocation and generation
+//===----------------------------------------------------------------------===//
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_allocate_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_buffer_usage_t allowed_usage, iree_const_byte_span_t initial_data,
+    iree_hal_buffer_view_t** out_buffer_view) {
+  IREE_ASSERT_ARGUMENT(allocator);
+  IREE_ASSERT_ARGUMENT(out_buffer_view);
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  iree_device_size_t allocation_size = 0;
+  iree_status_t status = iree_hal_buffer_compute_view_size(
+      shape, shape_rank, element_type, encoding_type, &allocation_size);
+
+  iree_hal_buffer_t* buffer = NULL;
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_allocator_allocate_buffer(allocator, memory_type,
+                                                allowed_usage, allocation_size,
+                                                initial_data, &buffer);
+  }
+
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_buffer_view_create(
+        buffer, shape, shape_rank, element_type, encoding_type,
+        iree_hal_allocator_host_allocator(allocator), out_buffer_view);
+  }
+
+  iree_hal_buffer_release(buffer);
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_heap_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_memory_access_t allowed_access,
+    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
+    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view) {
+  IREE_ASSERT_ARGUMENT(allocator);
+  IREE_ASSERT_ARGUMENT(out_buffer_view);
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  // NOTE: this will fail if the data cannot be imported into the allocator.
+  iree_hal_buffer_t* buffer = NULL;
+  iree_status_t status = iree_hal_allocator_wrap_buffer(
+      allocator, memory_type, allowed_access, allowed_usage, data,
+      data_allocator, &buffer);
+
+  if (iree_status_is_ok(status)) {
+    status = iree_hal_buffer_view_create(
+        buffer, shape, shape_rank, element_type, encoding_type,
+        iree_hal_allocator_host_allocator(allocator), out_buffer_view);
+  }
+
+  iree_hal_buffer_release(buffer);
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_or_clone_heap_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_memory_access_t allowed_access,
+    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
+    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view) {
+  IREE_ASSERT_ARGUMENT(allocator);
+
+  // Not all HAL implementations support wrapping buffers, and of those that do
+  // some may only support it in special situations such as when the buffer is
+  // not DEVICE_VISIBLE. The user application can query whether the wrapping is
+  // possible and decide to use alternative means of upload if it is not; we
+  // make no policy (other than validity) over what's best here.
+  iree_hal_buffer_compatibility_t compatibility =
+      iree_hal_allocator_query_buffer_compatibility(
+          allocator, memory_type, allowed_usage, IREE_HAL_BUFFER_USAGE_MAPPING,
+          (iree_device_size_t)data.data_length);
+  bool wrap_allowed = iree_all_bits_set(
+      compatibility, IREE_HAL_BUFFER_COMPATIBILITY_IMPORTABLE);
+  if (wrap_allowed) {
+    return iree_hal_buffer_view_wrap_heap_buffer(
+        allocator, shape, shape_rank, element_type, encoding_type, memory_type,
+        allowed_access, allowed_usage, data, data_allocator, out_buffer_view);
+  } else {
+    return iree_hal_buffer_view_allocate_buffer(
+        allocator, shape, shape_rank, element_type, encoding_type, memory_type,
+        allowed_usage, iree_make_const_byte_span(data.data, data.data_length),
+        out_buffer_view);
+  }
+}
+
+static iree_status_t iree_hal_buffer_view_generate_buffer_in_situ(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_buffer_usage_t allowed_usage,
+    iree_hal_buffer_view_generator_callback_t callback, void* user_data,
+    iree_hal_buffer_view_t** out_buffer_view) {
+  // Allocate the buffer view and entire buffer contents with the target memory
+  // type and the mapping bits.
+  iree_hal_buffer_view_t* buffer_view = NULL;
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_view_allocate_buffer(
+      allocator, shape, shape_rank, element_type, encoding_type, memory_type,
+      allowed_usage | IREE_HAL_BUFFER_USAGE_MAPPING,
+      iree_const_byte_span_empty(), &buffer_view));
+
+  // Map the buffer into host-visible memory.
+  iree_hal_buffer_mapping_t buffer_mapping = {{0}};
+  iree_status_t status = iree_hal_buffer_map_range(
+      iree_hal_buffer_view_buffer(buffer_view), IREE_HAL_MAPPING_MODE_SCOPED,
+      IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, 0, IREE_WHOLE_BUFFER,
+      &buffer_mapping);
+
+  // Generate using the callback directly into the buffer.
+  if (iree_status_is_ok(status)) {
+    status = callback(&buffer_mapping, user_data);
+  }
+
+  status =
+      iree_status_join(status, iree_hal_buffer_unmap_range(&buffer_mapping));
+  if (iree_status_is_ok(status)) {
+    *out_buffer_view = buffer_view;
+  } else {
+    iree_hal_buffer_view_release(buffer_view);
+  }
+  return status;
+}
+
+static iree_status_t iree_hal_buffer_view_generate_buffer_on_host(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_buffer_usage_t allowed_usage, iree_device_size_t allocation_size,
+    iree_hal_buffer_view_generator_callback_t callback, void* user_data,
+    iree_hal_buffer_view_t** out_buffer_view) {
+  // Allocate the host memory and generate the contents.
+  iree_allocator_t host_allocator =
+      iree_hal_allocator_host_allocator(allocator);
+  void* host_ptr = NULL;
+  IREE_RETURN_IF_ERROR(
+      iree_allocator_malloc(host_allocator, allocation_size, &host_ptr));
+  iree_hal_buffer_mapping_t mapping = {
+      .contents = iree_make_byte_span(host_ptr, allocation_size),
+  };
+  iree_status_t status = callback(&mapping, user_data);
+  if (!iree_status_is_ok(status)) {
+    iree_allocator_free(host_allocator, host_ptr);
+    return status;
+  }
+
+  // Try to wrap the host allocation to avoid the extra allocation and copy -
+  // this call will either hang on to the memory or do the copy and immediately
+  // free it.
+  return iree_hal_buffer_view_wrap_or_clone_heap_buffer(
+      allocator, shape, shape_rank, element_type, encoding_type, memory_type,
+      IREE_HAL_MEMORY_ACCESS_ALL, allowed_usage,
+      iree_make_byte_span(host_ptr, allocation_size), host_allocator,
+      out_buffer_view);
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_generate_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_buffer_usage_t allowed_usage,
+    iree_hal_buffer_view_generator_callback_t callback, void* user_data,
+    iree_hal_buffer_view_t** out_buffer_view) {
+  IREE_ASSERT_ARGUMENT(allocator);
+  IREE_ASSERT_ARGUMENT(callback);
+  IREE_ASSERT_ARGUMENT(out_buffer_view);
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  // Compute how large of an allocation we need to hold the whole view.
+  iree_device_size_t allocation_size = 0;
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_hal_buffer_compute_view_size(shape, shape_rank, element_type,
+                                            encoding_type, &allocation_size));
+
+  // If we can create the requested memory type with mapping then we'll do that
+  // and avoid needing to allocate the staging memory. If we can't get that
+  // memory type (or the allocator doesn't want us using it) then we'll fall
+  // back to allocation -> generation -> copy.
+  iree_hal_buffer_compatibility_t compatibility =
+      iree_hal_allocator_query_buffer_compatibility(
+          allocator, memory_type, allowed_usage, IREE_HAL_BUFFER_USAGE_MAPPING,
+          allocation_size);
+  bool is_mappable = iree_all_bits_set(
+      compatibility, IREE_HAL_BUFFER_COMPATIBILITY_ALLOCATABLE);
+
+  iree_status_t status = iree_ok_status();
+  if (is_mappable) {
+    // Compatible with allocate -> map -> generate.
+    status = iree_hal_buffer_view_generate_buffer_in_situ(
+        allocator, shape, shape_rank, element_type, encoding_type, memory_type,
+        allowed_usage, callback, user_data, out_buffer_view);
+  } else {
+    // Allocate host-local memory first and generate into that.
+    status = iree_hal_buffer_view_generate_buffer_on_host(
+        allocator, shape, shape_rank, element_type, encoding_type, memory_type,
+        allowed_usage, allocation_size, callback, user_data, out_buffer_view);
+  }
+
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+//===----------------------------------------------------------------------===//
+// Buffer view parsing and printing
+//===----------------------------------------------------------------------===//
+
+typedef struct iree_hal_buffer_view_parse_params_t {
+  iree_string_view_t data_str;
+  iree_hal_element_type_t element_type;
+} iree_hal_buffer_view_parse_params_t;
+static iree_status_t iree_hal_buffer_view_parse_into(
+    iree_hal_buffer_mapping_t* mapping, void* user_data) {
+  iree_hal_buffer_view_parse_params_t* params =
+      (iree_hal_buffer_view_parse_params_t*)user_data;
+  return iree_hal_parse_buffer_elements(params->data_str, params->element_type,
+                                        mapping->contents);
+}
+
+static iree_status_t iree_hal_buffer_view_parse_impl(
+    iree_string_view_t value, iree_hal_allocator_t* buffer_allocator,
+    iree_hal_buffer_view_t** out_buffer_view) {
+  // Strip whitespace that may come along (linefeeds/etc).
+  value = iree_string_view_trim(value);
+  value = iree_string_view_strip_prefix(value, IREE_SV("\""));
+  value = iree_string_view_strip_suffix(value, IREE_SV("\""));
+  if (iree_string_view_is_empty(value)) {
+    // Empty lines are invalid; need at least the shape/type information.
+    *out_buffer_view = NULL;
+    return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, "empty string input");
+  }
+
+  // The part of the string corresponding to the shape, e.g. 1x2x3.
+  iree_string_view_t shape_str = iree_string_view_empty();
+  // The part of the string corresponding to the type, e.g. f32
+  iree_string_view_t type_str = iree_string_view_empty();
+  // The part of the string corresponding to the buffer data, e.g. 1 2 3 4 5 6
+  iree_string_view_t data_str = iree_string_view_empty();
+
+  iree_string_view_t shape_and_type_str = value;
+  iree_string_view_split(value, '=', &shape_and_type_str, &data_str);
+  iree_host_size_t last_x_index = iree_string_view_find_last_of(
+      shape_and_type_str, IREE_SV("x"), IREE_STRING_VIEW_NPOS);
+  if (last_x_index == IREE_STRING_VIEW_NPOS) {
+    // Scalar.
+    type_str = shape_and_type_str;
+  } else {
+    // Has a shape.
+    shape_str = iree_string_view_substr(shape_and_type_str, 0, last_x_index);
+    type_str = iree_string_view_substr(shape_and_type_str, last_x_index + 1,
+                                       IREE_STRING_VIEW_NPOS);
+  }
+
+  // AxBxC...
+  iree_host_size_t shape_rank = 0;
+  iree_status_t shape_result =
+      iree_hal_parse_shape(shape_str, 0, NULL, &shape_rank);
+  if (!iree_status_is_ok(shape_result) &&
+      !iree_status_is_out_of_range(shape_result)) {
+    return shape_result;
+  } else if (shape_rank > 128) {
+    return iree_make_status(
+        IREE_STATUS_RESOURCE_EXHAUSTED,
+        "a shape rank of %zu is just a little bit excessive, eh?", shape_rank);
+  }
+  shape_result = iree_status_ignore(shape_result);
+  iree_hal_dim_t* shape =
+      (iree_hal_dim_t*)iree_alloca(shape_rank * sizeof(iree_hal_dim_t));
+  IREE_RETURN_IF_ERROR(
+      iree_hal_parse_shape(shape_str, shape_rank, shape, &shape_rank));
+
+  // f32, i32, etc
+  iree_hal_element_type_t element_type = IREE_HAL_ELEMENT_TYPE_NONE;
+  IREE_RETURN_IF_ERROR(iree_hal_parse_element_type(type_str, &element_type));
+
+  // TODO(benvanik): allow specifying the encoding.
+  iree_hal_encoding_type_t encoding_type =
+      IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR;
+
+  // Allocate the buffer from the provided allocator and parse directly into it.
+  iree_hal_buffer_view_parse_params_t params = {
+      .data_str = data_str,
+      .element_type = element_type,
+  };
+  return iree_hal_buffer_view_generate_buffer(
+      buffer_allocator, shape, shape_rank, element_type, encoding_type,
+      IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
+      IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER |
+          IREE_HAL_BUFFER_USAGE_MAPPING,
+      iree_hal_buffer_view_parse_into, &params, out_buffer_view);
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_parse(
+    iree_string_view_t value, iree_hal_allocator_t* buffer_allocator,
+    iree_hal_buffer_view_t** out_buffer_view) {
+  IREE_ASSERT_ARGUMENT(buffer_allocator);
+  IREE_ASSERT_ARGUMENT(out_buffer_view);
+  *out_buffer_view = NULL;
+  IREE_TRACE_ZONE_BEGIN(z0);
+  iree_status_t status =
+      iree_hal_buffer_view_parse_impl(value, buffer_allocator, out_buffer_view);
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+#define APPEND_CHAR(c)                           \
+  {                                              \
+    if (buffer) {                                \
+      if (buffer_length < buffer_capacity - 1) { \
+        buffer[buffer_length] = c;               \
+        buffer[buffer_length + 1] = '\0';        \
+      } else {                                   \
+        buffer = NULL;                           \
+      }                                          \
+    }                                            \
+    ++buffer_length;                             \
+  }
+
+static iree_status_t iree_hal_buffer_view_format_impl(
+    const iree_hal_buffer_view_t* buffer_view,
+    iree_host_size_t max_element_count, iree_host_size_t buffer_capacity,
+    char* buffer, iree_host_size_t* out_buffer_length) {
+  if (out_buffer_length) {
+    *out_buffer_length = 0;
+  }
+  if (buffer && buffer_capacity) {
+    buffer[0] = 0;
+  }
+
+  iree_host_size_t buffer_length = 0;
+  if (iree_hal_buffer_view_shape_rank(buffer_view) > 0) {
+    // Shape: 1x2x3
+    iree_host_size_t shape_length = 0;
+    iree_status_t status = iree_hal_format_shape(
+        iree_hal_buffer_view_shape_dims(buffer_view),
+        iree_hal_buffer_view_shape_rank(buffer_view),
+        buffer ? buffer_capacity - buffer_length : 0,
+        buffer ? buffer + buffer_length : NULL, &shape_length);
+    buffer_length += shape_length;
+    if (iree_status_is_out_of_range(status)) {
+      status = iree_status_ignore(status);
+      buffer = NULL;
+    } else if (!iree_status_is_ok(status)) {
+      return status;
+    }
+
+    // Separator: <shape>x<format>
+    APPEND_CHAR('x');
+  }
+
+  // Element type: f32
+  iree_host_size_t element_type_length = 0;
+  iree_status_t status = iree_hal_format_element_type(
+      iree_hal_buffer_view_element_type(buffer_view),
+      buffer ? buffer_capacity - buffer_length : 0,
+      buffer ? buffer + buffer_length : NULL, &element_type_length);
+  buffer_length += element_type_length;
+  if (iree_status_is_out_of_range(status)) {
+    status = iree_status_ignore(status);
+    buffer = NULL;
+  } else if (!iree_status_is_ok(status)) {
+    return status;
+  }
+
+  // TODO(benvanik): allow printing the encoding.
+
+  // Separator: <meta>=<value>
+  APPEND_CHAR('=');
+
+  // Buffer contents: 0 1 2 3 ...
+  iree_hal_buffer_mapping_t buffer_mapping = {{0}};
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
+      iree_hal_buffer_view_buffer(buffer_view), IREE_HAL_MAPPING_MODE_SCOPED,
+      IREE_HAL_MEMORY_ACCESS_READ, 0, IREE_WHOLE_BUFFER, &buffer_mapping));
+  iree_host_size_t elements_length = 0;
+  status = iree_hal_format_buffer_elements(
+      iree_make_const_byte_span(buffer_mapping.contents.data,
+                                buffer_mapping.contents.data_length),
+      iree_hal_buffer_view_shape_dims(buffer_view),
+      iree_hal_buffer_view_shape_rank(buffer_view),
+      iree_hal_buffer_view_element_type(buffer_view), max_element_count,
+      buffer ? buffer_capacity - buffer_length : 0,
+      buffer ? buffer + buffer_length : NULL, &elements_length);
+  buffer_length += elements_length;
+  status =
+      iree_status_join(status, iree_hal_buffer_unmap_range(&buffer_mapping));
+  if (iree_status_is_out_of_range(status)) {
+    status = iree_status_ignore(status);
+    buffer = NULL;
+  } else if (!iree_status_is_ok(status)) {
+    return status;
+  }
+
+  if (out_buffer_length) {
+    *out_buffer_length = buffer_length;
+  }
+  return buffer ? iree_ok_status()
+                : iree_status_from_code(IREE_STATUS_OUT_OF_RANGE);
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_format(
+    const iree_hal_buffer_view_t* buffer_view,
+    iree_host_size_t max_element_count, iree_host_size_t buffer_capacity,
+    char* buffer, iree_host_size_t* out_buffer_length) {
+  IREE_ASSERT_ARGUMENT(buffer_view);
+  IREE_TRACE_ZONE_BEGIN(z0);
+  iree_status_t status = iree_hal_buffer_view_format_impl(
+      buffer_view, max_element_count, buffer_capacity, buffer,
+      out_buffer_length);
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+// TODO(benvanik): streaming all the way down (needs string_util updates).
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_fprint(
+    FILE* file, const iree_hal_buffer_view_t* buffer_view,
+    iree_host_size_t max_element_count, iree_allocator_t host_allocator) {
+  IREE_ASSERT_ARGUMENT(file);
+  IREE_ASSERT_ARGUMENT(buffer_view);
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  // Query the string length (in characters).
+  iree_host_size_t buffer_length = 0;
+  iree_status_t status = iree_hal_buffer_view_format(
+      buffer_view, max_element_count, 0, NULL, &buffer_length);
+  if (!iree_status_is_out_of_range(status)) {
+    IREE_TRACE_ZONE_END(z0);
+    return status;
+  }
+
+  // Allocate scratch space to format in to.
+  // We should be streaming.
+  iree_host_size_t buffer_capacity = buffer_length + 1;  // NUL
+  char* buffer = NULL;
+  status =
+      iree_allocator_malloc(host_allocator, buffer_capacity, (void**)&buffer);
+
+  // Format the buffer into the string storage.
+  if (iree_status_is_ok(status)) {
+    status =
+        iree_hal_buffer_view_format(buffer_view, max_element_count,
+                                    buffer_capacity, buffer, &buffer_length);
+  }
+
+  // Dump to the file.
+  if (iree_status_is_ok(status)) {
+    fprintf(file, "%.*s", (int)buffer_length, buffer);
+  }
+
+  iree_allocator_free(host_allocator, buffer);
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
diff --git a/iree/hal/buffer_view_util.h b/iree/hal/buffer_view_util.h
new file mode 100644
index 0000000..08bf361
--- /dev/null
+++ b/iree/hal/buffer_view_util.h
@@ -0,0 +1,184 @@
+// Copyright 2021 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
+
+#ifndef IREE_HAL_BUFFER_VIEW_UTIL_H_
+#define IREE_HAL_BUFFER_VIEW_UTIL_H_
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "iree/base/api.h"
+#include "iree/hal/buffer_view.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+//===----------------------------------------------------------------------===//
+// Buffer view math
+//===----------------------------------------------------------------------===//
+
+// Calculates the allocation size of a buffer view.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_size(
+    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
+    iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type,
+    iree_device_size_t* out_allocation_size);
+
+// Calculates a byte offset into a buffer at the given indices.
+// Only works with densely-packed representations.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_offset(
+    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
+    iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* indices,
+    size_t indices_count, iree_device_size_t* out_offset);
+
+// Calculates a byte range into a buffer of the given contiguous range.
+// Only works with densely-packed representations.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_compute_view_range(
+    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
+    iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, const iree_hal_dim_t* start_indices,
+    iree_host_size_t indices_count, const iree_hal_dim_t* lengths,
+    iree_host_size_t lengths_count, iree_device_size_t* out_start_offset,
+    iree_device_size_t* out_length);
+
+//===----------------------------------------------------------------------===//
+// Buffer view allocation and generation
+//===----------------------------------------------------------------------===//
+
+// Allocates a buffer from |allocator| and wraps it in a buffer view.
+//
+// This is equivalent to:
+//   1. iree_hal_buffer_compute_view_size
+//   2. iree_hal_allocator_allocate_buffer
+//   3. iree_hal_buffer_view_create
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_allocate_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_buffer_usage_t allowed_usage, iree_const_byte_span_t initial_data,
+    iree_hal_buffer_view_t** out_buffer_view);
+
+// Imports a host buffer using |allocator| and wraps it in a buffer view.
+//
+// This is equivalent to:
+//   1. iree_hal_allocator_wrap_buffer
+//   2. iree_hal_buffer_view_create
+//
+// NOTE: not all buffers can be imported and not all allocators support
+// importing. See iree_hal_allocator_wrap_buffer for more information.
+// Fails if the buffer cannot be imported.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_heap_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_memory_access_t allowed_access,
+    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
+    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view);
+
+// Tries to import a host buffer using |allocator| and wrap it in a buffer view.
+// If the buffer cannot be imported then a new buffer will be allocated and the
+// source data will be copied into it.
+//
+// This is equivalent to:
+//   if iree_hal_allocator_query_buffer_compatibility ok:
+//     1. iree_hal_allocator_wrap_buffer
+//     2. iree_hal_buffer_view_create
+//   else:
+//     1. iree_hal_allocator_allocate_buffer
+//     2. iree_hal_buffer_write_data
+//     3. iree_hal_buffer_view_create
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_wrap_or_clone_heap_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_memory_access_t allowed_access,
+    iree_hal_buffer_usage_t allowed_usage, iree_byte_span_t data,
+    iree_allocator_t data_allocator, iree_hal_buffer_view_t** out_buffer_view);
+
+typedef iree_status_t(IREE_API_PTR* iree_hal_buffer_view_generator_callback_t)(
+    iree_hal_buffer_mapping_t* mapping, void* user_data);
+
+// Generates a buffer view with its initial contents produced by a callback.
+// When host and device memory are shared this allows direct generation into the
+// target device buffer. If not shared this can avoid expensive transfer mapping
+// operations at the cost of a transient host memory allocation. The mapped host
+// pointer passed to the callback is only valid within the callback.
+//
+// Buffers allocated like this do not need the IREE_HAL_BUFFER_USAGE_MAPPING bit
+// set; it will be added automatically if the allocator needs it and otherwise
+// the memory can remain unmappable (and thus fully device isolated).
+//
+// As this _may_ require allocation of the entire buffer content in host memory
+// it is always preferable to stage and issue copy commands via the device
+// queue. Even better is to do all generation on-device via dispatches without
+// the need to ever transfer. Usage of this method should be limited to times
+// where device-side generation isn't possible or memory consumption is not a
+// concern.
+//
+// This is equivalent to:
+//   1. iree_hal_buffer_compute_view_size
+//   2. iree_hal_allocator_allocate_buffer
+//   3. iree_hal_buffer_map_range + callback + iree_hal_buffer_unmap_range
+//   4. iree_hal_buffer_view_create
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_generate_buffer(
+    iree_hal_allocator_t* allocator, const iree_hal_dim_t* shape,
+    iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
+    iree_hal_encoding_type_t encoding_type, iree_hal_memory_type_t memory_type,
+    iree_hal_buffer_usage_t allowed_usage,
+    iree_hal_buffer_view_generator_callback_t callback, void* user_data,
+    iree_hal_buffer_view_t** out_buffer_view);
+
+//===----------------------------------------------------------------------===//
+// Buffer view parsing and printing
+//===----------------------------------------------------------------------===//
+
+// Parses a serialized set of buffer elements in the canonical tensor format
+// (the same as produced by iree_hal_buffer_view_format). The underlying buffer
+// will be allocated with |buffer_allocator| as a host-local/device-visible
+// buffer.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_parse(
+    iree_string_view_t value, iree_hal_allocator_t* buffer_allocator,
+    iree_hal_buffer_view_t** out_buffer_view);
+
+// TODO(#5413): enum for printing mode (include shape, precision).
+
+// Converts buffer view elements into a fully-specified string-form format like
+// `2x4xi16=[[1 2][3 4]]`.
+//
+// |max_element_count| can be used to limit the total number of elements printed
+// when the count may be large. Elided elements will be replaced with `...`.
+//
+// |buffer_capacity| defines the size of |buffer| in bytes and
+// |out_buffer_length| will return the string length in characters. Returns
+// IREE_STATUS_OUT_OF_RANGE if the buffer capacity is insufficient to hold the
+// formatted elements and |out_buffer_length| will contain the required size.
+//
+// Follows the standard API string formatting rules. See iree/base/api.h.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_format(
+    const iree_hal_buffer_view_t* buffer_view,
+    iree_host_size_t max_element_count, iree_host_size_t buffer_capacity,
+    char* buffer, iree_host_size_t* out_buffer_length);
+
+// Prints buffer view elements into a fully-specified string-form format like
+// `2x4xi16=[[1 2][3 4]]`.
+//
+// |max_element_count| can be used to limit the total number of elements printed
+// when the count may be large. Elided elements will be replaced with `...`.
+//
+// |host_allocator| will be used for any transient allocations required while
+// printing.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_view_fprint(
+    FILE* file, const iree_hal_buffer_view_t* buffer_view,
+    iree_host_size_t max_element_count, iree_allocator_t host_allocator);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // IREE_HAL_BUFFER_VIEW_UTIL_H_
diff --git a/iree/hal/command_buffer.c b/iree/hal/command_buffer.c
index b34df20..e4c7fdf 100644
--- a/iree/hal/command_buffer.c
+++ b/iree/hal/command_buffer.c
@@ -454,3 +454,70 @@
   IREE_TRACE_ZONE_END(z0);
   return status;
 }
+
+//===----------------------------------------------------------------------===//
+// Utilities for command buffer creation
+//===----------------------------------------------------------------------===//
+
+IREE_API_EXPORT iree_status_t iree_hal_create_transfer_command_buffer(
+    iree_hal_device_t* device, iree_hal_command_buffer_mode_t mode,
+    iree_hal_queue_affinity_t queue_affinity, iree_host_size_t transfer_count,
+    const iree_hal_transfer_command_t* transfer_commands,
+    iree_hal_command_buffer_t** out_command_buffer) {
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  iree_hal_command_buffer_t* command_buffer = NULL;
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_hal_command_buffer_create(device, mode,
+                                         IREE_HAL_COMMAND_CATEGORY_TRANSFER,
+                                         queue_affinity, &command_buffer));
+
+  iree_status_t status = iree_hal_command_buffer_begin(command_buffer);
+  if (iree_status_is_ok(status)) {
+    for (iree_host_size_t i = 0; i < transfer_count; ++i) {
+      const iree_hal_transfer_command_t* transfer_command =
+          &transfer_commands[i];
+      switch (transfer_command->type) {
+        case IREE_HAL_TRANSFER_COMMAND_TYPE_FILL:
+          status = iree_hal_command_buffer_fill_buffer(
+              command_buffer, transfer_command->fill.target_buffer,
+              transfer_command->fill.target_offset,
+              transfer_command->fill.length, transfer_command->fill.pattern,
+              transfer_command->fill.pattern_length);
+          break;
+        case IREE_HAL_TRANSFER_COMMAND_TYPE_COPY:
+          status = iree_hal_command_buffer_copy_buffer(
+              command_buffer, transfer_command->copy.source_buffer,
+              transfer_command->copy.source_offset,
+              transfer_command->copy.target_buffer,
+              transfer_command->copy.target_offset,
+              transfer_command->copy.length);
+          break;
+        case IREE_HAL_TRANSFER_COMMAND_TYPE_UPDATE:
+          status = iree_hal_command_buffer_update_buffer(
+              command_buffer, transfer_command->update.source_buffer,
+              transfer_command->update.source_offset,
+              transfer_command->update.target_buffer,
+              transfer_command->update.target_offset,
+              transfer_command->update.length);
+          break;
+        default:
+          status = iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                                    "unknown transfer_commands[%zu] type %d", i,
+                                    (int)transfer_command->type);
+          break;
+      }
+      if (!iree_status_is_ok(status)) break;
+    }
+  }
+  status =
+      iree_status_join(status, iree_hal_command_buffer_end(command_buffer));
+
+  if (iree_status_is_ok(status)) {
+    *out_command_buffer = command_buffer;
+  } else {
+    iree_hal_command_buffer_release(command_buffer);
+  }
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
diff --git a/iree/hal/command_buffer.h b/iree/hal/command_buffer.h
index 41087a3..a219948 100644
--- a/iree/hal/command_buffer.h
+++ b/iree/hal/command_buffer.h
@@ -229,6 +229,12 @@
   // TODO(benvanik): valid push constant bit ranges.
 } iree_hal_command_buffer_validation_state_t;
 
+// Maximum size of any update in iree_hal_command_buffer_update_buffer.
+// 64KB is the limit on Vulkan and we uniformly use that today across all
+// targets as to not need too much command buffer memory.
+#define IREE_HAL_COMMAND_BUFFER_MAX_UPDATE_SIZE \
+  ((iree_device_size_t)(64 * 1024))
+
 //===----------------------------------------------------------------------===//
 // iree_hal_command_buffer_t
 //===----------------------------------------------------------------------===//
@@ -493,6 +499,68 @@
     iree_hal_buffer_t* workgroups_buffer, iree_device_size_t workgroups_offset);
 
 //===----------------------------------------------------------------------===//
+// Utilities for command buffer creation
+//===----------------------------------------------------------------------===//
+
+// Defines a transfer command operation.
+typedef enum iree_hal_transfer_command_type_t {
+  // iree_hal_command_buffer_fill_buffer
+  IREE_HAL_TRANSFER_COMMAND_TYPE_FILL = 0u,
+  // iree_hal_command_buffer_copy_buffer
+  IREE_HAL_TRANSFER_COMMAND_TYPE_COPY = 1u,
+  // iree_hal_command_buffer_update_buffer
+  IREE_HAL_TRANSFER_COMMAND_TYPE_UPDATE = 2u,
+} iree_hal_transfer_command_type_t;
+
+// Represents a single transfer command within a batch of commands.
+typedef struct iree_hal_transfer_command_t {
+  // The type of the command selecting which of the payload data is used.
+  iree_hal_transfer_command_type_t type;
+  union {
+    // IREE_HAL_TRANSFER_COMMAND_TYPE_FILL
+    struct {
+      iree_hal_buffer_t* target_buffer;
+      iree_device_size_t target_offset;
+      iree_device_size_t length;
+      const void* pattern;
+      iree_host_size_t pattern_length;
+    } fill;
+    // IREE_HAL_TRANSFER_COMMAND_TYPE_COPY
+    struct {
+      iree_hal_buffer_t* source_buffer;
+      iree_device_size_t source_offset;
+      iree_hal_buffer_t* target_buffer;
+      iree_device_size_t target_offset;
+      iree_device_size_t length;
+    } copy;
+    // IREE_HAL_TRANSFER_COMMAND_TYPE_UPDATE
+    struct {
+      const void* source_buffer;
+      iree_host_size_t source_offset;
+      iree_hal_buffer_t* target_buffer;
+      iree_device_size_t target_offset;
+      iree_device_size_t length;
+    } update;
+  };
+} iree_hal_transfer_command_t;
+
+// Builds a command buffer containing a recording of all |transfer_commands|.
+// All buffers must be compatible with |device| and ranges must not overlap
+// (same as with memcpy). All commands are executed concurrently with no
+// barriers. The provided commands and any referenced data needs only remain
+// live during recording, while all referenced buffers must be kept live by
+// the caller until the command buffer has completed execution.
+//
+// This is just a utility to make it easier to quickly construct batches of
+// transfer operations. If more control is required then record the command
+// buffer as normal.
+IREE_API_EXPORT iree_status_t iree_hal_create_transfer_command_buffer(
+    iree_hal_device_t* device, iree_hal_command_buffer_mode_t mode,
+    iree_hal_queue_affinity_t queue_affinity, iree_host_size_t transfer_count,
+    const iree_hal_transfer_command_t* transfer_commands,
+    iree_hal_command_buffer_t** out_command_buffer);
+
+//===----------------------------------------------------------------------===//
 // iree_hal_command_buffer_t validation wrapper
 //===----------------------------------------------------------------------===//
 
diff --git a/iree/hal/command_buffer_validation.c b/iree/hal/command_buffer_validation.c
index 56e6cc0..871045d 100644
--- a/iree/hal/command_buffer_validation.c
+++ b/iree/hal/command_buffer_validation.c
@@ -73,7 +73,8 @@
     return iree_make_status(
         IREE_STATUS_PERMISSION_DENIED,
         "requested buffer usage is not supported for the buffer on this queue; "
-        "buffer allows %.*s, operation requires %.*s",
+        "buffer allows %.*s, operation requires %.*s (allocator compatibility "
+        "mismatch)",
         (int)allowed_usage_str.size, allowed_usage_str.data,
         (int)intended_usage_str.size, intended_usage_str.data);
   }
diff --git a/iree/hal/cts/allocator_test.h b/iree/hal/cts/allocator_test.h
index 506d6ee..d4d4c50 100644
--- a/iree/hal/cts/allocator_test.h
+++ b/iree/hal/cts/allocator_test.h
@@ -85,7 +85,8 @@
 
   iree_hal_buffer_t* buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-      device_allocator_, memory_type, buffer_usage, kAllocationSize, &buffer));
+      device_allocator_, memory_type, buffer_usage, kAllocationSize,
+      iree_const_byte_span_empty(), &buffer));
 
   // At a mimimum, the requested memory type should be respected.
   // Additional bits may be optionally set depending on the allocator.
@@ -108,7 +109,7 @@
   iree_hal_buffer_t* buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
       device_allocator_, memory_type, buffer_usage, /*allocation_size=*/0,
-      &buffer));
+      iree_const_byte_span_empty(), &buffer));
 
   iree_hal_buffer_release(buffer);
 }
diff --git a/iree/hal/cts/buffer_mapping_test.h b/iree/hal/cts/buffer_mapping_test.h
index 106b24d..f8229c1 100644
--- a/iree/hal/cts/buffer_mapping_test.h
+++ b/iree/hal/cts/buffer_mapping_test.h
@@ -46,7 +46,8 @@
 
   iree_hal_buffer_t* buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-      device_allocator_, memory_type, buffer_usage, kAllocationSize, &buffer));
+      device_allocator_, memory_type, buffer_usage, kAllocationSize,
+      iree_const_byte_span_empty(), &buffer));
 
   EXPECT_TRUE(
       iree_all_bits_set(iree_hal_buffer_memory_type(buffer), memory_type));
@@ -63,7 +64,8 @@
 
   iree_hal_buffer_t* buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-      device_allocator_, memory_type, buffer_usage, kAllocationSize, &buffer));
+      device_allocator_, memory_type, buffer_usage, kAllocationSize,
+      iree_const_byte_span_empty(), &buffer));
 
   IREE_ASSERT_OK(iree_hal_buffer_zero(buffer, /*byte_offset=*/0,
                                       /*byte_length=*/kAllocationSize));
@@ -85,7 +87,8 @@
 
   iree_hal_buffer_t* buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-      device_allocator_, memory_type, buffer_usage, kAllocationSize, &buffer));
+      device_allocator_, memory_type, buffer_usage, kAllocationSize,
+      iree_const_byte_span_empty(), &buffer));
 
   IREE_ASSERT_OK(iree_hal_buffer_zero(buffer, /*byte_offset=*/0,
                                       /*byte_length=*/kAllocationSize));
@@ -113,7 +116,8 @@
 
   iree_hal_buffer_t* buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-      device_allocator_, memory_type, buffer_usage, kAllocationSize, &buffer));
+      device_allocator_, memory_type, buffer_usage, kAllocationSize,
+      iree_const_byte_span_empty(), &buffer));
 
   uint8_t fill_value = 0x07;
   IREE_ASSERT_OK(iree_hal_buffer_fill(buffer, /*byte_offset=*/0,
@@ -138,7 +142,8 @@
 
   iree_hal_buffer_t* buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-      device_allocator_, memory_type, buffer_usage, kAllocationSize, &buffer));
+      device_allocator_, memory_type, buffer_usage, kAllocationSize,
+      iree_const_byte_span_empty(), &buffer));
 
   uint8_t fill_value = 0x07;
   std::vector<uint8_t> reference_buffer(kAllocationSize);
@@ -162,10 +167,10 @@
   iree_hal_buffer_t* buffer_b;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
       device_allocator_, memory_type, buffer_usage, kAllocationSize,
-      &buffer_a));
+      iree_const_byte_span_empty(), &buffer_a));
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
       device_allocator_, memory_type, buffer_usage, kAllocationSize,
-      &buffer_b));
+      iree_const_byte_span_empty(), &buffer_b));
 
   uint8_t fill_value = 0x07;
   IREE_ASSERT_OK(iree_hal_buffer_fill(buffer_a, /*byte_offset=*/0,
diff --git a/iree/hal/cts/command_buffer_test.h b/iree/hal/cts/command_buffer_test.h
index 60df42d..4bd0d7b 100644
--- a/iree/hal/cts/command_buffer_test.h
+++ b/iree/hal/cts/command_buffer_test.h
@@ -44,7 +44,8 @@
     IREE_CHECK_OK(iree_hal_allocator_allocate_buffer(
         iree_hal_device_allocator(device_),
         IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
-        IREE_HAL_BUFFER_USAGE_ALL, buffer_size, &device_buffer));
+        IREE_HAL_BUFFER_USAGE_ALL, buffer_size, iree_const_byte_span_empty(),
+        &device_buffer));
 
     IREE_CHECK_OK(iree_hal_command_buffer_begin(command_buffer));
     // Start with a zero fill on the entire buffer...
@@ -141,26 +142,28 @@
       IREE_HAL_COMMAND_CATEGORY_TRANSFER, IREE_HAL_QUEUE_AFFINITY_ANY,
       &command_buffer));
 
+  uint8_t i8_val = 0x54;
+  std::vector<uint8_t> reference_buffer(kBufferSize);
+  std::memset(reference_buffer.data(), i8_val, kBufferSize);
+
   // Create and fill a host buffer.
   iree_hal_buffer_t* host_buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
       device_allocator_,
       IREE_HAL_MEMORY_TYPE_HOST_VISIBLE | IREE_HAL_MEMORY_TYPE_HOST_CACHED |
           IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
-      IREE_HAL_BUFFER_USAGE_ALL, kBufferSize, &host_buffer));
-  uint8_t i8_val = 0x54;
-  IREE_ASSERT_OK(iree_hal_buffer_fill(host_buffer, /*byte_offset=*/0,
-                                      /*byte_length=*/kBufferSize, &i8_val,
-                                      /*pattern_length=*/sizeof(i8_val)));
-  std::vector<uint8_t> reference_buffer(kBufferSize);
-  std::memset(reference_buffer.data(), i8_val, kBufferSize);
+      IREE_HAL_BUFFER_USAGE_ALL, kBufferSize,
+      iree_make_const_byte_span(reference_buffer.data(),
+                                reference_buffer.size()),
+      &host_buffer));
 
   // Create a device buffer.
   iree_hal_buffer_t* device_buffer;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
       device_allocator_,
       IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
-      IREE_HAL_BUFFER_USAGE_ALL, kBufferSize, &device_buffer));
+      IREE_HAL_BUFFER_USAGE_ALL, kBufferSize, iree_const_byte_span_empty(),
+      &device_buffer));
 
   // Copy the host buffer to the device buffer.
   IREE_ASSERT_OK(iree_hal_command_buffer_begin(command_buffer));
@@ -187,33 +190,34 @@
 }
 
 TEST_P(command_buffer_test, CopySubBuffer) {
-  iree_hal_command_buffer_t* command_buffer;
+  iree_hal_command_buffer_t* command_buffer = NULL;
   IREE_ASSERT_OK(iree_hal_command_buffer_create(
       device_, IREE_HAL_COMMAND_BUFFER_MODE_ONE_SHOT,
       IREE_HAL_COMMAND_CATEGORY_TRANSFER, IREE_HAL_QUEUE_AFFINITY_ANY,
       &command_buffer));
 
-  iree_hal_buffer_t* device_buffer;
+  iree_hal_buffer_t* device_buffer = NULL;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
       device_allocator_,
       IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
-      IREE_HAL_BUFFER_USAGE_ALL, kBufferSize, &device_buffer));
+      IREE_HAL_BUFFER_USAGE_ALL, kBufferSize, iree_const_byte_span_empty(),
+      &device_buffer));
+
+  uint8_t i8_val = 0x88;
+  std::vector<uint8_t> reference_buffer(kBufferSize);
+  std::memset(reference_buffer.data() + 8, i8_val, kBufferSize / 2 - 4);
 
   // Create another host buffer with a smaller size.
-  iree_hal_buffer_t* host_buffer;
+  std::vector<uint8_t> host_buffer_data(kBufferSize, i8_val);
+  iree_hal_buffer_t* host_buffer = NULL;
   IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
       device_allocator_,
       IREE_HAL_MEMORY_TYPE_HOST_VISIBLE | IREE_HAL_MEMORY_TYPE_HOST_CACHED |
           IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
-      IREE_HAL_BUFFER_USAGE_ALL, kBufferSize / 2, &host_buffer));
-
-  // Fill the host buffer.
-  uint8_t i8_val = 0x88;
-  IREE_ASSERT_OK(iree_hal_buffer_fill(host_buffer, /*byte_offset=*/0,
-                                      /*byte_length=*/kBufferSize / 2, &i8_val,
-                                      /*pattern_length=*/sizeof(i8_val)));
-  std::vector<uint8_t> reference_buffer(kBufferSize);
-  std::memset(reference_buffer.data() + 8, i8_val, kBufferSize / 2 - 4);
+      IREE_HAL_BUFFER_USAGE_ALL, host_buffer_data.size() / 2,
+      iree_make_const_byte_span(host_buffer_data.data(),
+                                host_buffer_data.size() / 2),
+      &host_buffer));
 
   // Copy the host buffer to the device buffer; zero fill the untouched bytes.
   uint8_t zero_val = 0x0;
diff --git a/iree/hal/cts/cts_test_base.h b/iree/hal/cts/cts_test_base.h
index 1269646..611926d 100644
--- a/iree/hal/cts/cts_test_base.h
+++ b/iree/hal/cts/cts_test_base.h
@@ -41,7 +41,7 @@
     iree_status_t status = TryGetDriver(driver_name, &driver);
     if (iree_status_is_unavailable(status)) {
       iree_status_free(status);
-      IREE_LOG(WARNING) << "Skipping test as << '" << driver_name
+      IREE_LOG(WARNING) << "Skipping test as '" << driver_name
                         << "' driver is unavailable";
       GTEST_SKIP();
       return;
diff --git a/iree/hal/cuda/BUILD b/iree/hal/cuda/BUILD
index c3d6f02..0522dba 100644
--- a/iree/hal/cuda/BUILD
+++ b/iree/hal/cuda/BUILD
@@ -65,6 +65,7 @@
         "//iree/base/internal:synchronization",
         "//iree/base/internal/flatcc:parsing",
         "//iree/hal",
+        "//iree/hal/utils:buffer_transfer",
         "//iree/hal/utils:deferred_command_buffer",
         "//iree/schemas:cuda_executable_def_c_fbs",
     ],
diff --git a/iree/hal/cuda/CMakeLists.txt b/iree/hal/cuda/CMakeLists.txt
index d3bd43b..9b91207 100644
--- a/iree/hal/cuda/CMakeLists.txt
+++ b/iree/hal/cuda/CMakeLists.txt
@@ -57,6 +57,7 @@
     iree::base::internal::synchronization
     iree::base::tracing
     iree::hal
+    iree::hal::utils::buffer_transfer
     iree::hal::utils::deferred_command_buffer
     iree::schemas::cuda_executable_def_c_fbs
   PUBLIC
diff --git a/iree/hal/cuda/cuda_allocator.c b/iree/hal/cuda/cuda_allocator.c
index ee60000..6ec25e7 100644
--- a/iree/hal/cuda/cuda_allocator.c
+++ b/iree/hal/cuda/cuda_allocator.c
@@ -16,10 +16,11 @@
 
 typedef struct iree_hal_cuda_allocator_t {
   iree_hal_resource_t resource;
+  iree_hal_device_t* base_device;
   iree_hal_cuda_context_wrapper_t* context;
   CUdevice device;
-  bool supports_concurrent_managed_access;
   CUstream stream;
+  bool supports_concurrent_managed_access;
 
   IREE_STATISTICS(iree_hal_allocator_statistics_t statistics;)
 } iree_hal_cuda_allocator_t;
@@ -33,8 +34,9 @@
 }
 
 iree_status_t iree_hal_cuda_allocator_create(
-    iree_hal_cuda_context_wrapper_t* context, CUdevice device, CUstream stream,
-    iree_hal_allocator_t** out_allocator) {
+    iree_hal_device_t* base_device, iree_hal_cuda_context_wrapper_t* context,
+    CUdevice device, CUstream stream, iree_hal_allocator_t** out_allocator) {
+  IREE_ASSERT_ARGUMENT(base_device);
   IREE_ASSERT_ARGUMENT(context);
   IREE_TRACE_ZONE_BEGIN(z0);
 
@@ -65,6 +67,7 @@
   if (iree_status_is_ok(status)) {
     iree_hal_resource_initialize(&iree_hal_cuda_allocator_vtable,
                                  &allocator->resource);
+    allocator->base_device = base_device;
     allocator->context = context;
     allocator->device = device;
     allocator->stream = stream;
@@ -156,7 +159,7 @@
 static iree_status_t iree_hal_cuda_allocator_allocate_buffer(
     iree_hal_allocator_t* base_allocator, iree_hal_memory_type_t memory_type,
     iree_hal_buffer_usage_t allowed_usage, iree_host_size_t allocation_size,
-    iree_hal_buffer_t** out_buffer) {
+    iree_const_byte_span_t initial_data, iree_hal_buffer_t** out_buffer) {
   iree_hal_cuda_allocator_t* allocator =
       iree_hal_cuda_allocator_cast(base_allocator);
   // Guard against the corner case where the requested buffer size is 0. The
@@ -215,19 +218,39 @@
           cuMemHostGetDevicePointer(&device_ptr, host_ptr, /*flags=*/0));
     }
   }
+
+  iree_hal_buffer_t* buffer = NULL;
   if (iree_status_is_ok(status)) {
     status = iree_hal_cuda_buffer_wrap(
         (iree_hal_allocator_t*)allocator, memory_type,
         IREE_HAL_MEMORY_ACCESS_ALL, allowed_usage, allocation_size,
         /*byte_offset=*/0,
-        /*byte_length=*/allocation_size, device_ptr, host_ptr, out_buffer);
+        /*byte_length=*/allocation_size, device_ptr, host_ptr, &buffer);
   }
+
+  // Copy the initial contents into the buffer. This may require staging.
+  if (iree_status_is_ok(status) &&
+      !iree_const_byte_span_is_empty(initial_data)) {
+    status = iree_hal_device_transfer_range(
+        allocator->base_device,
+        iree_hal_make_host_transfer_buffer_span((void*)initial_data.data,
+                                                initial_data.data_length),
+        0, iree_hal_make_device_transfer_buffer(buffer), 0,
+        initial_data.data_length, IREE_HAL_TRANSFER_BUFFER_FLAG_DEFAULT,
+        iree_infinite_timeout());
+  }
+
   if (iree_status_is_ok(status)) {
     IREE_STATISTICS(iree_hal_allocator_statistics_record_alloc(
         &allocator->statistics, memory_type, allocation_size));
+    *out_buffer = buffer;
   } else {
-    iree_hal_cuda_buffer_free(allocator->context, memory_type, device_ptr,
-                              host_ptr);
+    if (!buffer) {
+      iree_hal_cuda_buffer_free(allocator->context, memory_type, device_ptr,
+                                host_ptr);
+    } else {
+      iree_hal_buffer_release(buffer);
+    }
   }
   return status;
 }
diff --git a/iree/hal/cuda/cuda_allocator.h b/iree/hal/cuda/cuda_allocator.h
index 0fe2739..4f22579 100644
--- a/iree/hal/cuda/cuda_allocator.h
+++ b/iree/hal/cuda/cuda_allocator.h
@@ -18,8 +18,8 @@
 
 // Create a cuda allocator.
 iree_status_t iree_hal_cuda_allocator_create(
-    iree_hal_cuda_context_wrapper_t* context, CUdevice device, CUstream stream,
-    iree_hal_allocator_t** out_allocator);
+    iree_hal_device_t* base_device, iree_hal_cuda_context_wrapper_t* context,
+    CUdevice device, CUstream stream, iree_hal_allocator_t** out_allocator);
 
 #ifdef __cplusplus
 }  // extern "C"
diff --git a/iree/hal/cuda/cuda_buffer.c b/iree/hal/cuda/cuda_buffer.c
index db2a8aa..b9a56ef 100644
--- a/iree/hal/cuda/cuda_buffer.c
+++ b/iree/hal/cuda/cuda_buffer.c
@@ -68,14 +68,16 @@
     iree_hal_buffer_t* base_buffer, iree_hal_mapping_mode_t mapping_mode,
     iree_hal_memory_access_t memory_access,
     iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
-    void** out_data_ptr) {
+    iree_hal_buffer_mapping_t* mapping) {
   iree_hal_cuda_buffer_t* buffer = iree_hal_cuda_buffer_cast(base_buffer);
 
-  if (!iree_all_bits_set(buffer->base.memory_type,
-                         IREE_HAL_MEMORY_TYPE_HOST_VISIBLE)) {
-    return iree_make_status(IREE_STATUS_INTERNAL,
-                            "trying to map memory not host visible");
-  }
+  // TODO(benvanik): add upload/download for unmapped buffers.
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_memory_type(
+      iree_hal_buffer_memory_type(base_buffer),
+      IREE_HAL_MEMORY_TYPE_HOST_VISIBLE));
+  IREE_RETURN_IF_ERROR(
+      iree_hal_buffer_validate_usage(iree_hal_buffer_allowed_usage(base_buffer),
+                                     IREE_HAL_BUFFER_USAGE_MAPPING));
 
   uint8_t* data_ptr = (uint8_t*)(buffer->host_ptr) + local_byte_offset;
   // If we mapped for discard scribble over the bytes. This is not a mandated
@@ -87,14 +89,16 @@
     memset(data_ptr, 0xCD, local_byte_length);
   }
 #endif  // !NDEBUG
-  *out_data_ptr = data_ptr;
+
+  mapping->contents = iree_make_byte_span(data_ptr, local_byte_length);
   return iree_ok_status();
 }
 
-static void iree_hal_cuda_buffer_unmap_range(
+static iree_status_t iree_hal_cuda_buffer_unmap_range(
     iree_hal_buffer_t* base_buffer, iree_device_size_t local_byte_offset,
-    iree_device_size_t local_byte_length, void* data_ptr) {
-  // nothing to do.
+    iree_device_size_t local_byte_length, iree_hal_buffer_mapping_t* mapping) {
+  // Nothing to do (today).
+  return iree_ok_status();
 }
 
 static iree_status_t iree_hal_cuda_buffer_invalidate_range(
diff --git a/iree/hal/cuda/cuda_device.c b/iree/hal/cuda/cuda_device.c
index 7178368..eab84ee 100644
--- a/iree/hal/cuda/cuda_device.c
+++ b/iree/hal/cuda/cuda_device.c
@@ -23,6 +23,7 @@
 #include "iree/hal/cuda/nop_executable_cache.h"
 #include "iree/hal/cuda/status_util.h"
 #include "iree/hal/cuda/stream_command_buffer.h"
+#include "iree/hal/utils/buffer_transfer.h"
 #include "iree/hal/utils/deferred_command_buffer.h"
 
 //===----------------------------------------------------------------------===//
@@ -108,7 +109,8 @@
   device->context_wrapper.syms = syms;
 
   iree_status_t status = iree_hal_cuda_allocator_create(
-      &device->context_wrapper, cu_device, stream, &device->device_allocator);
+      (iree_hal_device_t*)device, &device->context_wrapper, cu_device, stream,
+      &device->device_allocator);
 
   device->command_buffer_mode = params->command_buffer_mode;
   if (iree_status_is_ok(status) &&
@@ -379,6 +381,7 @@
     .create_executable_cache = iree_hal_cuda_device_create_executable_cache,
     .create_executable_layout = iree_hal_cuda_device_create_executable_layout,
     .create_semaphore = iree_hal_cuda_device_create_semaphore,
+    .transfer_range = iree_hal_device_submit_transfer_range_and_wait,
     .queue_submit = iree_hal_cuda_device_queue_submit,
     .submit_and_wait = iree_hal_cuda_device_submit_and_wait,
     .wait_semaphores = iree_hal_cuda_device_wait_semaphores,
diff --git a/iree/hal/cuda/dynamic_symbols_test.cc b/iree/hal/cuda/dynamic_symbols_test.cc
index 47862d3..ab5136c 100644
--- a/iree/hal/cuda/dynamic_symbols_test.cc
+++ b/iree/hal/cuda/dynamic_symbols_test.cc
@@ -27,6 +27,8 @@
   iree_status_t status = iree_hal_cuda_dynamic_symbols_initialize(
       iree_allocator_system(), &symbols);
   if (!iree_status_is_ok(status)) {
+    iree_status_fprint(stderr, status);
+    iree_status_ignore(status);
     std::cerr << "Symbols cannot be loaded, skipping test.";
     GTEST_SKIP();
   }
diff --git a/iree/hal/device.c b/iree/hal/device.c
index 6bcad9e..648b3b9 100644
--- a/iree/hal/device.c
+++ b/iree/hal/device.c
@@ -7,6 +7,9 @@
 #include "iree/hal/device.h"
 
 #include "iree/base/tracing.h"
+#include "iree/hal/allocator.h"
+#include "iree/hal/buffer.h"
+#include "iree/hal/command_buffer.h"
 #include "iree/hal/detail.h"
 #include "iree/hal/resource.h"
 
@@ -58,6 +61,200 @@
   return _VTABLE_DISPATCH(device, query_i32)(device, category, key, out_value);
 }
 
+// Performs a synchronous host->device or device->host transfer.
+static iree_status_t iree_hal_device_transfer_buffer(
+    iree_hal_device_t* device, iree_hal_buffer_t* source_buffer,
+    iree_hal_memory_type_t memory_type, iree_hal_buffer_usage_t allowed_usage,
+    iree_hal_buffer_t** out_target_buffer) {
+  IREE_ASSERT_ARGUMENT(device);
+  IREE_ASSERT_ARGUMENT(source_buffer);
+  IREE_ASSERT_ARGUMENT(out_target_buffer);
+  *out_target_buffer = NULL;
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  if (iree_all_bits_set(iree_hal_buffer_memory_type(source_buffer),
+                        memory_type) &&
+      iree_all_bits_set(iree_hal_buffer_allowed_usage(source_buffer),
+                        allowed_usage)) {
+    // Source is already usable for the intended purposes - avoid the copy.
+    *out_target_buffer = source_buffer;
+    iree_hal_buffer_retain(source_buffer);
+    IREE_TRACE_ZONE_END(z0);
+    return iree_ok_status();
+  }
+
+  iree_device_size_t allocation_size =
+      iree_hal_buffer_allocation_size(source_buffer);
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, allocation_size);
+
+  // Allocate a new buffer of the desired memory type. It needs to be at least
+  // large enough to hold the requested data.
+  iree_hal_buffer_t* target_buffer = NULL;
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_hal_allocator_allocate_buffer(
+              iree_hal_device_allocator(device), memory_type,
+              allowed_usage | IREE_HAL_BUFFER_USAGE_TRANSFER, allocation_size,
+              iree_const_byte_span_empty(), &target_buffer));
+
+  // Perform the transfer and wait for it to complete.
+  const iree_hal_transfer_command_t transfer_command = {
+      .type = IREE_HAL_TRANSFER_COMMAND_TYPE_COPY,
+      .copy =
+          {
+              .source_buffer = source_buffer,
+              .source_offset = 0,
+              .target_buffer = target_buffer,
+              .target_offset = 0,
+              .length = IREE_WHOLE_BUFFER,
+          },
+  };
+  iree_status_t status = iree_hal_device_transfer_and_wait(
+      device, /*wait_semaphore=*/NULL,
+      /*wait_value=*/0ull, 1, &transfer_command, iree_infinite_timeout());
+
+  if (iree_status_is_ok(status)) {
+    *out_target_buffer = target_buffer;
+  } else {
+    iree_hal_buffer_release(target_buffer);
+  }
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_device_transfer_to_device(
+    iree_hal_device_t* device, iree_hal_buffer_t* source_buffer,
+    iree_hal_buffer_usage_t allowed_usage,
+    iree_hal_buffer_t** out_target_buffer) {
+  return iree_hal_device_transfer_buffer(device, source_buffer,
+                                         IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL,
+                                         allowed_usage, out_target_buffer);
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_device_transfer_to_host(
+    iree_hal_device_t* device, iree_hal_buffer_t* source_buffer,
+    iree_hal_buffer_t** out_target_buffer) {
+  iree_hal_buffer_usage_t allowed_usage =
+      IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING;
+  return iree_hal_device_transfer_buffer(device, source_buffer,
+                                         IREE_HAL_MEMORY_TYPE_HOST_LOCAL,
+                                         allowed_usage, out_target_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) {
+  if (data_length == 0) {
+    return iree_ok_status();  // No-op.
+  }
+
+  // host->host is not allowed. We may want to support this one day to allow for
+  // parallelized copies and such, however the validation code differs quite a
+  // bit and it'd be better to have this as part of a task system API.
+  bool is_source_host = source.device_buffer == NULL;
+  bool is_target_host = target.device_buffer == NULL;
+  if (is_source_host && is_target_host) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "cannot perform host->host transfers via this API, use memcpy/memmove");
+  }
+
+  // Check for overlap - like memcpy we require that the two ranges don't have
+  // any overlap as we may use memcpy. This only matters if the buffers are
+  // both device buffers - host and device should never alias: behavior is
+  // undefined if a user tries to pass a mapped device pointer as if it was a
+  // host pointer.
+  if (!is_source_host && !is_target_host &&
+      iree_hal_buffer_test_overlap(source.device_buffer, source_offset,
+                                   data_length, target.device_buffer,
+                                   target_offset, data_length) !=
+          IREE_HAL_BUFFER_OVERLAP_DISJOINT) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "source and target ranges must not overlap within the same buffer");
+  }
+
+  IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_TRACE_ZONE_APPEND_TEXT(
+      z0, is_source_host ? "h2d" : (is_target_host ? "d2h" : "d2d"));
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, data_length);
+
+  // Defer to the backing implementation.
+  iree_status_t status = _VTABLE_DISPATCH(device, transfer_range)(
+      device, source, source_offset, target, target_offset, data_length, flags,
+      timeout);
+
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+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_ASSERT_ARGUMENT(device);
+  IREE_ASSERT_ARGUMENT(!transfer_count || transfer_commands);
+  IREE_TRACE_ZONE_BEGIN(z0);
+
+  // We only want to allow inline execution if we have not been instructed to
+  // wait on a semaphore and it hasn't yet been signaled.
+  iree_hal_command_buffer_mode_t mode = IREE_HAL_COMMAND_BUFFER_MODE_ONE_SHOT;
+  if (wait_semaphore) {
+    uint64_t current_value = 0ull;
+    IREE_RETURN_AND_END_ZONE_IF_ERROR(
+        z0, iree_hal_semaphore_query(wait_semaphore, &current_value));
+    if (current_value >= wait_value) {
+      mode |= IREE_HAL_COMMAND_BUFFER_MODE_ALLOW_INLINE_EXECUTION;
+    }
+  } else {
+    mode |= IREE_HAL_COMMAND_BUFFER_MODE_ALLOW_INLINE_EXECUTION;
+  }
+
+  // Create a command buffer performing all of the transfer operations.
+  iree_hal_command_buffer_t* command_buffer = NULL;
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_hal_create_transfer_command_buffer(
+              device, mode, IREE_HAL_QUEUE_AFFINITY_ANY, transfer_count,
+              transfer_commands, &command_buffer));
+
+  // Perform a full submit-and-wait. On devices with multiple queues this can
+  // run out-of-order/overlapped with other work and return earlier than device
+  // idle.
+  iree_hal_semaphore_t* fence_semaphore = NULL;
+  iree_status_t status =
+      iree_hal_semaphore_create(device, 0ull, &fence_semaphore);
+  uint64_t signal_value = 1ull;
+  if (iree_status_is_ok(status)) {
+    iree_hal_submission_batch_t batch = {
+        .wait_semaphores =
+            {
+                .count = wait_semaphore != NULL ? 1 : 0,
+                .semaphores = &wait_semaphore,
+                .payload_values = &wait_value,
+            },
+        .command_buffer_count = 1,
+        .command_buffers = &command_buffer,
+        .signal_semaphores =
+            {
+                .count = 1,
+                .semaphores = &fence_semaphore,
+                .payload_values = &signal_value,
+            },
+    };
+    status = iree_hal_device_submit_and_wait(
+        device, IREE_HAL_COMMAND_CATEGORY_TRANSFER, IREE_HAL_QUEUE_AFFINITY_ANY,
+        1, &batch, fence_semaphore, signal_value, timeout);
+  }
+
+  iree_hal_command_buffer_release(command_buffer);
+  iree_hal_semaphore_release(fence_semaphore);
+
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
 // Validates that the submission is well-formed.
 static iree_status_t iree_hal_device_validate_submission(
     iree_host_size_t batch_count, const iree_hal_submission_batch_t* batches) {
diff --git a/iree/hal/device.h b/iree/hal/device.h
index 7ccf959..8dbf9b9 100644
--- a/iree/hal/device.h
+++ b/iree/hal/device.h
@@ -74,6 +74,41 @@
   iree_string_view_t name;
 } iree_hal_device_info_t;
 
+// A transfer source or destination.
+typedef struct iree_hal_transfer_buffer_t {
+  // A host-allocated void* buffer.
+  iree_byte_span_t host_buffer;
+  // A device-allocated buffer (may be of any memory type).
+  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) {
+  iree_hal_transfer_buffer_t transfer_buffer = {
+      host_buffer,
+      NULL,
+  };
+  return transfer_buffer;
+}
+
+static inline iree_hal_transfer_buffer_t
+iree_hal_make_host_transfer_buffer_span(void* ptr, iree_host_size_t length) {
+  iree_hal_transfer_buffer_t transfer_buffer = {
+      iree_make_byte_span(ptr, length),
+      NULL,
+  };
+  return transfer_buffer;
+}
+
+static inline iree_hal_transfer_buffer_t iree_hal_make_device_transfer_buffer(
+    iree_hal_buffer_t* device_buffer) {
+  iree_hal_transfer_buffer_t transfer_buffer = {
+      iree_byte_span_empty(),
+      device_buffer,
+  };
+  return transfer_buffer;
+}
+
 // A list of semaphores and their corresponding payloads.
 // When signaling each semaphore will be set to the new payload value provided.
 // When waiting each semaphore must reach or exceed the payload value.
@@ -173,6 +208,80 @@
     iree_hal_device_t* device, iree_string_view_t category,
     iree_string_view_t key, int32_t* out_value);
 
+// Synchronously transfers the given |source_buffer| to a device-local
+// buffer returned in |out_target_buffer|. Callers must release the target
+// buffer when no longer used. If the source buffer is already device-local it
+// will be returned without an allocation or copy occurring.
+//
+// This utility may incur signficant overhead and is present for simple tooling
+// and prototypes; when transfering multiple buffers users should always prefer
+// asynchronous command buffers submitted to device queues. Note too that the
+// entire buffer is transferred: if reading back smaller portions it is better
+// to perform these as ranged transfers to avoid the amount of data that needs
+// to be moved.
+IREE_API_EXPORT iree_status_t iree_hal_device_transfer_to_device(
+    iree_hal_device_t* device, iree_hal_buffer_t* source_buffer,
+    iree_hal_buffer_usage_t allowed_usage,
+    iree_hal_buffer_t** out_target_buffer);
+
+// Synchronously transfers the given |source_buffer| to a host-local
+// buffer returned in |out_target_buffer|. Callers must release the target
+// buffer when no longer used. If the source buffer is already host-local it
+// will be returned without an allocation or copy occurring.
+//
+// This utility may incur signficant overhead and is present for simple tooling
+// and prototypes; when transfering multiple buffers users should always prefer
+// asynchronous command buffers submitted to device queues.
+IREE_API_EXPORT iree_status_t iree_hal_device_transfer_to_host(
+    iree_hal_device_t* device, iree_hal_buffer_t* source_buffer,
+    iree_hal_buffer_t** out_target_buffer);
+
+// Synchronously copies data from |source| into |target|.
+//
+// Supports host->device, device->host, and device->device transfer,
+// including across devices. This method will never fail based on device
+// capabilities but may incur some extreme transient allocations and copies in
+// order to perform the transfer.
+//
+// The ordering of the transfer is undefined with respect to queue execution on
+// the source or target device; some may require full device flushes in order to
+// perform this operation while others may immediately perform it while there is
+// still work outstanding.
+//
+// It is strongly recommended that buffer operations are performed on transfer
+// queues; using this synchronous function may incur additional cache flushes
+// and synchronous blocking behavior and is not supported on all buffer types.
+// See iree_hal_command_buffer_copy_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);
+
+// Synchronously executes one or more transfer operations against a queue.
+// All buffers must be compatible with |device| and ranges must not overlap
+// (same as with memcpy).
+//
+// This is a blocking operation and may incur significant overheads as
+// internally it issues a command buffer with the transfer operations and waits
+// for it to complete. Users should do that themselves so that the work can be
+// issued concurrently and batched effectively. This is only useful as a
+// fallback for implementations that require it or tools where things like I/O
+// are transferred without worrying about performance. When submitting other
+// work it's preferable to use iree_hal_create_transfer_command_buffer and a
+// normal queue submission that allows for more fine-grained sequencing and
+// amortizes the submission cost by batching other work.
+//
+// The transfer will begin after the optional |wait_semaphore| reaches
+// |wait_value|. Behavior is undefined if no semaphore is provided and there are
+// in-flight operations concurrently using the buffer ranges.
+// Returns only after all transfers have completed and been flushed.
+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);
+
 // Submits one or more batches of work to a device queue.
 //
 // The queue is selected based on the flags set in |command_categories| and the
@@ -301,6 +410,12 @@
       iree_hal_device_t* device, uint64_t initial_value,
       iree_hal_semaphore_t** out_semaphore);
 
+  iree_status_t(IREE_API_PTR* 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);
+
   iree_status_t(IREE_API_PTR* 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,
diff --git a/iree/hal/local/BUILD b/iree/hal/local/BUILD
index 3b6cd65..3fc61bd 100644
--- a/iree/hal/local/BUILD
+++ b/iree/hal/local/BUILD
@@ -103,6 +103,7 @@
         "//iree/base/internal:arena",
         "//iree/base/internal:synchronization",
         "//iree/hal",
+        "//iree/hal/utils:buffer_transfer",
     ],
 )
 
@@ -168,6 +169,7 @@
         "//iree/base/internal:synchronization",
         "//iree/base/internal:wait_handle",
         "//iree/hal",
+        "//iree/hal/utils:buffer_transfer",
         "//iree/task",
     ],
 )
diff --git a/iree/hal/local/CMakeLists.txt b/iree/hal/local/CMakeLists.txt
index 42ad0c1..60c9588 100644
--- a/iree/hal/local/CMakeLists.txt
+++ b/iree/hal/local/CMakeLists.txt
@@ -99,6 +99,7 @@
     iree::base::internal::synchronization
     iree::base::tracing
     iree::hal
+    iree::hal::utils::buffer_transfer
   PUBLIC
 )
 
@@ -155,6 +156,7 @@
     iree::base::internal::wait_handle
     iree::base::tracing
     iree::hal
+    iree::hal::utils::buffer_transfer
     iree::task
   PUBLIC
 )
diff --git a/iree/hal/local/executable_library_benchmark.c b/iree/hal/local/executable_library_benchmark.c
index 2605928..1002f4a 100644
--- a/iree/hal/local/executable_library_benchmark.c
+++ b/iree/hal/local/executable_library_benchmark.c
@@ -274,9 +274,10 @@
     iree_hal_buffer_t* buffer = iree_hal_buffer_view_buffer(buffer_views[i]);
     iree_device_size_t buffer_length =
         iree_hal_buffer_view_byte_length(buffer_views[i]);
-    iree_hal_buffer_mapping_t buffer_mapping;
+    iree_hal_buffer_mapping_t buffer_mapping = {{0}};
     IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-        buffer, IREE_HAL_MEMORY_ACCESS_READ | IREE_HAL_MEMORY_ACCESS_WRITE, 0,
+        buffer, IREE_HAL_MAPPING_MODE_PERSISTENT,
+        IREE_HAL_MEMORY_ACCESS_READ | IREE_HAL_MEMORY_ACCESS_WRITE, 0,
         buffer_length, &buffer_mapping));
     binding_ptrs[i] = buffer_mapping.contents.data;
     binding_lengths[i] = (size_t)buffer_mapping.contents.data_length;
diff --git a/iree/hal/local/inline_command_buffer.c b/iree/hal/local/inline_command_buffer.c
index 5819de7..07d95c5 100644
--- a/iree/hal/local/inline_command_buffer.c
+++ b/iree/hal/local/inline_command_buffer.c
@@ -345,10 +345,11 @@
     iree_host_size_t binding_ordinal = binding_base + bindings[i].binding;
 
     // TODO(benvanik): track mapping so we can properly map/unmap/flush/etc.
-    iree_hal_buffer_mapping_t buffer_mapping;
+    iree_hal_buffer_mapping_t buffer_mapping = {{0}};
     IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-        bindings[i].buffer, IREE_HAL_MEMORY_ACCESS_ANY, bindings[i].offset,
-        bindings[i].length, &buffer_mapping));
+        bindings[i].buffer, IREE_HAL_MAPPING_MODE_PERSISTENT,
+        IREE_HAL_MEMORY_ACCESS_ANY, bindings[i].offset, bindings[i].length,
+        &buffer_mapping));
     command_buffer->state.full_bindings[binding_ordinal] =
         buffer_mapping.contents.data;
     command_buffer->state.full_binding_lengths[binding_ordinal] =
@@ -475,10 +476,11 @@
     iree_hal_buffer_t* workgroups_buffer,
     iree_device_size_t workgroups_offset) {
   // TODO(benvanik): track mapping so we can properly map/unmap/flush/etc.
-  iree_hal_buffer_mapping_t buffer_mapping;
+  iree_hal_buffer_mapping_t buffer_mapping = {{0}};
   IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-      workgroups_buffer, IREE_HAL_MEMORY_ACCESS_READ, workgroups_offset,
-      3 * sizeof(uint32_t), &buffer_mapping));
+      workgroups_buffer, IREE_HAL_MAPPING_MODE_PERSISTENT,
+      IREE_HAL_MEMORY_ACCESS_READ, workgroups_offset, 3 * sizeof(uint32_t),
+      &buffer_mapping));
   iree_hal_vec3_t workgroup_count =
       *(const iree_hal_vec3_t*)buffer_mapping.contents.data;
   return iree_hal_inline_command_buffer_dispatch(
diff --git a/iree/hal/local/sync_device.c b/iree/hal/local/sync_device.c
index 2e6e1f6..58b5029 100644
--- a/iree/hal/local/sync_device.c
+++ b/iree/hal/local/sync_device.c
@@ -18,6 +18,7 @@
 #include "iree/hal/local/local_executable_layout.h"
 #include "iree/hal/local/sync_event.h"
 #include "iree/hal/local/sync_semaphore.h"
+#include "iree/hal/utils/buffer_transfer.h"
 
 typedef struct iree_hal_sync_device_t {
   iree_hal_resource_t resource;
@@ -303,6 +304,7 @@
     .create_executable_cache = iree_hal_sync_device_create_executable_cache,
     .create_executable_layout = iree_hal_sync_device_create_executable_layout,
     .create_semaphore = iree_hal_sync_device_create_semaphore,
+    .transfer_range = iree_hal_device_transfer_mappable_range,
     .queue_submit = iree_hal_sync_device_queue_submit,
     .submit_and_wait = iree_hal_sync_device_submit_and_wait,
     .wait_semaphores = iree_hal_sync_device_wait_semaphores,
diff --git a/iree/hal/local/task_command_buffer.c b/iree/hal/local/task_command_buffer.c
index 051eade..85c6825 100644
--- a/iree/hal/local/task_command_buffer.c
+++ b/iree/hal/local/task_command_buffer.c
@@ -733,10 +733,11 @@
     iree_host_size_t binding_ordinal = binding_base + bindings[i].binding;
 
     // TODO(benvanik): track mapping so we can properly map/unmap/flush/etc.
-    iree_hal_buffer_mapping_t buffer_mapping;
+    iree_hal_buffer_mapping_t buffer_mapping = {{0}};
     IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-        bindings[i].buffer, IREE_HAL_MEMORY_ACCESS_ANY, bindings[i].offset,
-        bindings[i].length, &buffer_mapping));
+        bindings[i].buffer, IREE_HAL_MAPPING_MODE_PERSISTENT,
+        IREE_HAL_MEMORY_ACCESS_ANY, bindings[i].offset, bindings[i].length,
+        &buffer_mapping));
     command_buffer->state.bindings[binding_ordinal] =
         buffer_mapping.contents.data;
     command_buffer->state.binding_lengths[binding_ordinal] =
@@ -933,10 +934,11 @@
     iree_hal_buffer_t* workgroups_buffer,
     iree_device_size_t workgroups_offset) {
   // TODO(benvanik): track mapping so we can properly map/unmap/flush/etc.
-  iree_hal_buffer_mapping_t buffer_mapping;
+  iree_hal_buffer_mapping_t buffer_mapping = {{0}};
   IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-      workgroups_buffer, IREE_HAL_MEMORY_ACCESS_READ, workgroups_offset,
-      3 * sizeof(uint32_t), &buffer_mapping));
+      workgroups_buffer, IREE_HAL_MAPPING_MODE_PERSISTENT,
+      IREE_HAL_MEMORY_ACCESS_READ, workgroups_offset, 3 * sizeof(uint32_t),
+      &buffer_mapping));
 
   iree_hal_cmd_dispatch_t* cmd = NULL;
   IREE_RETURN_IF_ERROR(iree_hal_task_command_buffer_build_dispatch(
diff --git a/iree/hal/local/task_device.c b/iree/hal/local/task_device.c
index 1d1727a..4b0e1ed 100644
--- a/iree/hal/local/task_device.c
+++ b/iree/hal/local/task_device.c
@@ -21,6 +21,7 @@
 #include "iree/hal/local/task_event.h"
 #include "iree/hal/local/task_queue.h"
 #include "iree/hal/local/task_semaphore.h"
+#include "iree/hal/utils/buffer_transfer.h"
 
 #define IREE_HAL_LOCAL_TASK_EVENT_POOL_CAPACITY 32
 
@@ -368,6 +369,7 @@
     .create_executable_cache = iree_hal_task_device_create_executable_cache,
     .create_executable_layout = iree_hal_task_device_create_executable_layout,
     .create_semaphore = iree_hal_task_device_create_semaphore,
+    .transfer_range = iree_hal_device_transfer_mappable_range,
     .queue_submit = iree_hal_task_device_queue_submit,
     .submit_and_wait = iree_hal_task_device_submit_and_wait,
     .wait_semaphores = iree_hal_task_device_wait_semaphores,
diff --git a/iree/hal/utils/BUILD b/iree/hal/utils/BUILD
index 234d6d3..ce0e599 100644
--- a/iree/hal/utils/BUILD
+++ b/iree/hal/utils/BUILD
@@ -11,6 +11,18 @@
 )
 
 cc_library(
+    name = "buffer_transfer",
+    srcs = ["buffer_transfer.c"],
+    hdrs = ["buffer_transfer.h"],
+    visibility = ["//visibility:public"],
+    deps = [
+        "//iree/base",
+        "//iree/base:tracing",
+        "//iree/hal",
+    ],
+)
+
+cc_library(
     name = "deferred_command_buffer",
     srcs = ["deferred_command_buffer.c"],
     hdrs = ["deferred_command_buffer.h"],
diff --git a/iree/hal/utils/CMakeLists.txt b/iree/hal/utils/CMakeLists.txt
index 6709717..7c778ac 100644
--- a/iree/hal/utils/CMakeLists.txt
+++ b/iree/hal/utils/CMakeLists.txt
@@ -12,6 +12,20 @@
 
 iree_cc_library(
   NAME
+    buffer_transfer
+  HDRS
+    "buffer_transfer.h"
+  SRCS
+    "buffer_transfer.c"
+  DEPS
+    iree::base
+    iree::base::tracing
+    iree::hal
+  PUBLIC
+)
+
+iree_cc_library(
+  NAME
     deferred_command_buffer
   HDRS
     "deferred_command_buffer.h"
diff --git a/iree/hal/utils/buffer_transfer.c b/iree/hal/utils/buffer_transfer.c
new file mode 100644
index 0000000..258e36d
--- /dev/null
+++ b/iree/hal/utils/buffer_transfer.c
@@ -0,0 +1,351 @@
+// Copyright 2021 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/buffer_transfer.h"
+
+#include "iree/base/tracing.h"
+
+//===----------------------------------------------------------------------===//
+// iree_hal_device_transfer_range implementations
+//===----------------------------------------------------------------------===//
+
+IREE_API_EXPORT iree_status_t iree_hal_device_submit_transfer_range_and_wait(
+    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) {
+  // If the source and target are both mappable into host memory (or are host
+  // memory) then we can use the fast zero-alloc path. This may actually be
+  // slower than doing a device queue transfer depending on the size of the data
+  // and where the memory lives. For example, if we have two device buffers in
+  // device-local host-visible memory we'd be performing the transfer by pulling
+  // all the memory to the CPU and pushing it back again.
+  // TODO(benvanik): check for device-local -> device-local and avoid mapping.
+  bool is_source_mappable =
+      !source.device_buffer ||
+      iree_all_bits_set(iree_hal_buffer_allowed_usage(source.device_buffer),
+                        IREE_HAL_BUFFER_USAGE_MAPPING);
+  bool is_target_mappable =
+      !target.device_buffer ||
+      iree_all_bits_set(iree_hal_buffer_allowed_usage(target.device_buffer),
+                        IREE_HAL_BUFFER_USAGE_MAPPING);
+  if (is_source_mappable && is_target_mappable) {
+    return iree_hal_device_transfer_mappable_range(
+        device, source, source_offset, target, target_offset, data_length,
+        flags, timeout);
+  }
+
+  // If the source is a host buffer under 64KB then we can do a more efficient
+  // (though still relatively costly) update instead of needing a staging
+  // buffer.
+  if (!source.device_buffer && target.device_buffer &&
+      data_length <= IREE_HAL_COMMAND_BUFFER_MAX_UPDATE_SIZE) {
+    const iree_hal_transfer_command_t transfer_command = {
+        .type = IREE_HAL_TRANSFER_COMMAND_TYPE_UPDATE,
+        .update =
+            {
+                .source_buffer = source.host_buffer.data,
+                .source_offset = source_offset,
+                .target_buffer = target.device_buffer,
+                .target_offset = target_offset,
+                .length = data_length,
+            },
+    };
+    return iree_hal_device_transfer_and_wait(device, /*wait_semaphore=*/NULL,
+                                             /*wait_value=*/0ull, 1,
+                                             &transfer_command, timeout);
+  }
+
+  iree_status_t status = iree_ok_status();
+
+  // Allocate the staging buffer for upload to the device.
+  iree_hal_buffer_t* source_buffer = source.device_buffer;
+  if (!source_buffer) {
+    // Allocate staging memory with a copy of the host data. We only initialize
+    // the portion being transferred.
+    // TODO(benvanik): use wrap_buffer if supported to avoid the
+    // allocation/copy.
+    status = iree_hal_allocator_allocate_buffer(
+        iree_hal_device_allocator(device),
+        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING,
+        data_length,
+        iree_make_const_byte_span(source.host_buffer.data + source_offset,
+                                  data_length),
+        &source_buffer);
+    source_offset = 0;
+  }
+
+  // Allocate the staging buffer for download from the device.
+  iree_hal_buffer_t* target_buffer = target.device_buffer;
+  if (!target_buffer) {
+    // Allocate uninitialized staging memory for the transfer target.
+    // We only allocate enough for the portion we are transfering.
+    // TODO(benvanik): use wrap_buffer if supported to avoid the
+    // allocation/copy.
+    status = iree_hal_allocator_allocate_buffer(
+        iree_hal_device_allocator(device),
+        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING,
+        data_length, iree_const_byte_span_empty(), &target_buffer);
+    target_offset = 0;
+  }
+
+  // Issue synchronous device copy.
+  if (iree_status_is_ok(status)) {
+    const iree_hal_transfer_command_t transfer_command = {
+        .type = IREE_HAL_TRANSFER_COMMAND_TYPE_COPY,
+        .copy =
+            {
+                .source_buffer = source_buffer,
+                .source_offset = source_offset,
+                .target_buffer = target_buffer,
+                .target_offset = target_offset,
+                .length = data_length,
+            },
+    };
+    status = iree_hal_device_transfer_and_wait(device, /*wait_semaphore=*/NULL,
+                                               /*wait_value=*/0ull, 1,
+                                               &transfer_command, timeout);
+  }
+
+  // Read back the staging buffer into memory, if needed.
+  if (iree_status_is_ok(status) && !target.device_buffer) {
+    status = iree_hal_buffer_read_data(target_buffer, 0,
+                                       target.host_buffer.data, data_length);
+  }
+
+  // Discard staging buffers, if they were required.
+  if (!source.device_buffer) iree_hal_buffer_release(source_buffer);
+  if (!target.device_buffer) iree_hal_buffer_release(target_buffer);
+
+  return status;
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_device_transfer_mappable_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) {
+  iree_status_t status = iree_ok_status();
+
+  iree_hal_buffer_mapping_t source_mapping = {{0}};
+  if (iree_status_is_ok(status)) {
+    if (source.device_buffer) {
+      status = iree_hal_buffer_map_range(
+          source.device_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+          IREE_HAL_MEMORY_ACCESS_READ, source_offset, data_length,
+          &source_mapping);
+    } else {
+      source_mapping = (iree_hal_buffer_mapping_t){
+          .contents = source.host_buffer,
+      };
+    }
+  }
+
+  iree_hal_buffer_mapping_t target_mapping = {{0}};
+  if (iree_status_is_ok(status)) {
+    if (target.device_buffer) {
+      status = iree_hal_buffer_map_range(
+          target.device_buffer, IREE_HAL_MAPPING_MODE_SCOPED,
+          IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, target_offset, data_length,
+          &target_mapping);
+    } else {
+      target_mapping = (iree_hal_buffer_mapping_t){
+          .contents = target.host_buffer,
+      };
+    }
+  }
+
+  iree_device_size_t adjusted_data_length = 0;
+  if (iree_status_is_ok(status)) {
+    // Adjust the data length based on the min we have.
+    if (data_length == IREE_WHOLE_BUFFER) {
+      // Whole buffer copy requested - that could mean either, so take the min.
+      adjusted_data_length = iree_min(source_mapping.contents.data_length,
+                                      target_mapping.contents.data_length);
+    } else {
+      // Specific length requested - validate that we have matching lengths.
+      IREE_ASSERT_EQ(source_mapping.contents.data_length,
+                     target_mapping.contents.data_length);
+      adjusted_data_length = target_mapping.contents.data_length;
+    }
+
+    // Perform the copy, assuming there's anything to do.
+    if (adjusted_data_length != 0) {
+      memcpy(target_mapping.contents.data, source_mapping.contents.data,
+             adjusted_data_length);
+    }
+  }
+
+  if (source.device_buffer) {
+    status =
+        iree_status_join(status, iree_hal_buffer_unmap_range(&source_mapping));
+  }
+  if (target.device_buffer) {
+    if (adjusted_data_length > 0 &&
+        !iree_all_bits_set(iree_hal_buffer_memory_type(target.device_buffer),
+                           IREE_HAL_MEMORY_TYPE_HOST_COHERENT)) {
+      status = iree_status_join(
+          status, iree_hal_buffer_flush_range(&target_mapping, 0,
+                                              adjusted_data_length));
+    }
+    status =
+        iree_status_join(status, iree_hal_buffer_unmap_range(&target_mapping));
+  }
+  return status;
+}
+
+//===----------------------------------------------------------------------===//
+// iree_hal_buffer_map_range implementations
+//===----------------------------------------------------------------------===//
+
+typedef struct iree_hal_emulated_buffer_mapping_t {
+  iree_hal_buffer_t* host_local_buffer;
+  iree_hal_buffer_mapping_t host_local_mapping;
+} iree_hal_emulated_buffer_mapping_t;
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_emulated_map_range(
+    iree_hal_device_t* device, iree_hal_buffer_t* buffer,
+    iree_hal_mapping_mode_t mapping_mode,
+    iree_hal_memory_access_t memory_access,
+    iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
+    iree_hal_buffer_mapping_t* mapping) {
+  IREE_ASSERT_ARGUMENT(device);
+  IREE_ASSERT_ARGUMENT(buffer);
+  IREE_ASSERT_ARGUMENT(mapping);
+
+  iree_hal_allocator_t* device_allocator = iree_hal_device_allocator(device);
+  iree_allocator_t host_allocator = iree_hal_device_host_allocator(device);
+
+  // We can't perform persistent mapping with this as we need to manage the
+  // staging buffer lifetime.
+  if (IREE_UNLIKELY(mapping_mode == IREE_HAL_MAPPING_MODE_PERSISTENT)) {
+    return iree_make_status(
+        IREE_STATUS_INVALID_ARGUMENT,
+        "emulated buffer mapping only possible with scoped mappings");
+  }
+
+  // No implementation should be using this emulated method with memory that is
+  // allocated as mappable.
+  if (IREE_UNLIKELY(iree_all_bits_set(iree_hal_buffer_memory_type(buffer),
+                                      IREE_HAL_BUFFER_USAGE_MAPPING))) {
+    return iree_make_status(
+        IREE_STATUS_FAILED_PRECONDITION,
+        "emulated buffer mapping should not be used with mappable buffers");
+  }
+
+  IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, (uint64_t)local_byte_length);
+
+  // NOTE: this is assuming that the host is going to be doing a lot of work
+  // on the mapped memory and wants read/write caching and such. If the user
+  // wants write combining on device memory and other things they should ensure
+  // this emulated mapping path is not hit.
+
+  // Create a transient struct we use to track the emulated operation.
+  // We could pack this into the mapping but this composes better - it's small
+  // and pooled by the host allocator anyway.
+  iree_hal_emulated_buffer_mapping_t* emulation_state = NULL;
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_allocator_malloc(host_allocator, sizeof(*emulation_state),
+                                (void**)&emulation_state));
+
+  // Allocate the buffer we'll be using to stage our copy of the device memory.
+  // All devices should be able to satisfy this host-local + mapping request.
+  iree_status_t status = iree_hal_allocator_allocate_buffer(
+      device_allocator, IREE_HAL_MEMORY_TYPE_HOST_LOCAL,
+      IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING,
+      local_byte_length, iree_const_byte_span_empty(),
+      &emulation_state->host_local_buffer);
+
+  // We need to capture a copy of the device buffer to work with; unless the
+  // user was nice and said they don't care about the contents with the DISCARD
+  // bit. Ideally we'd also enable invalidate_range to specify subranges we want
+  // to map.
+  if (iree_status_is_ok(status) &&
+      !iree_all_bits_set(memory_access, IREE_HAL_MEMORY_ACCESS_DISCARD)) {
+    // Download (device->host) the data.
+    status = iree_hal_device_transfer_range(
+        device, iree_hal_make_device_transfer_buffer(mapping->buffer),
+        local_byte_offset,
+        iree_hal_make_device_transfer_buffer(
+            emulation_state->host_local_buffer),
+        0, local_byte_length, IREE_HAL_TRANSFER_BUFFER_FLAG_DEFAULT,
+        iree_infinite_timeout());
+  }
+
+  if (iree_status_is_ok(status)) {
+    // Map the scratch buffer: map-ception.
+    // Code-wise it looks like this may loop back onto this emulated path
+    // but no implementation should be using this emulation if they have host
+    // local IREE_HAL_BUFFER_USAGE_MAPPING memory - and we check that above.
+    status = iree_hal_buffer_map_range(emulation_state->host_local_buffer,
+                                       IREE_HAL_MAPPING_MODE_SCOPED,
+                                       memory_access, 0, local_byte_length,
+                                       &emulation_state->host_local_mapping);
+  }
+
+  // Retain the scratch buffer for the duration of the mapping.
+  if (iree_status_is_ok(status)) {
+    // Note that we are giving back the host-local mapped contents to the user -
+    // they don't need to know it's from our staging buffer.
+    mapping->contents = emulation_state->host_local_mapping.contents;
+    mapping->impl.reserved[0] = (uint64_t)((uintptr_t)emulation_state);
+  } else {
+    status = iree_status_join(
+        status,
+        iree_hal_buffer_unmap_range(&emulation_state->host_local_mapping));
+    iree_hal_buffer_release(emulation_state->host_local_buffer);
+    iree_allocator_free(host_allocator, emulation_state);
+  }
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
+
+IREE_API_EXPORT iree_status_t iree_hal_buffer_emulated_unmap_range(
+    iree_hal_device_t* device, iree_hal_buffer_t* buffer,
+    iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
+    iree_hal_buffer_mapping_t* mapping) {
+  IREE_ASSERT_ARGUMENT(device);
+  IREE_ASSERT_ARGUMENT(buffer);
+  IREE_ASSERT_ARGUMENT(mapping);
+  IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, (uint64_t)local_byte_length);
+  iree_hal_emulated_buffer_mapping_t* emulation_state =
+      (iree_hal_emulated_buffer_mapping_t*)((uintptr_t)
+                                                mapping->impl.reserved[0]);
+  IREE_ASSERT_NE(emulation_state, NULL);
+
+  // Unmap the scratch buffer first to make it available for copying (if
+  // needed).
+  iree_status_t status =
+      iree_hal_buffer_unmap_range(&emulation_state->host_local_mapping);
+
+  // If we were writing then we'll need to flush the range.
+  // Ideally we'd keep track of this on the mapping itself based on the user's
+  // calls to flush_range to limit how much we need to transfer.
+  if (iree_status_is_ok(status) &&
+      iree_all_bits_set(mapping->impl.allowed_access,
+                        IREE_HAL_MEMORY_ACCESS_WRITE)) {
+    // Upload (host->device) the data.
+    status = iree_hal_device_transfer_range(
+        device,
+        iree_hal_make_device_transfer_buffer(
+            emulation_state->host_local_buffer),
+        0, iree_hal_make_device_transfer_buffer(mapping->buffer),
+        local_byte_offset, local_byte_length,
+        IREE_HAL_TRANSFER_BUFFER_FLAG_DEFAULT, iree_infinite_timeout());
+  }
+
+  // Deallocate the scratch buffer and our emulation state.
+  iree_hal_buffer_release(emulation_state->host_local_buffer);
+  iree_allocator_t host_allocator = iree_hal_device_host_allocator(device);
+  iree_allocator_free(host_allocator, emulation_state);
+
+  IREE_TRACE_ZONE_END(z0);
+  return status;
+}
diff --git a/iree/hal/utils/buffer_transfer.h b/iree/hal/utils/buffer_transfer.h
new file mode 100644
index 0000000..2daac0a
--- /dev/null
+++ b/iree/hal/utils/buffer_transfer.h
@@ -0,0 +1,69 @@
+// Copyright 2021 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
+
+#ifndef IREE_HAL_UTILS_BUFFER_TRANSFER_H_
+#define IREE_HAL_UTILS_BUFFER_TRANSFER_H_
+
+#include "iree/base/api.h"
+#include "iree/hal/api.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif  // __cplusplus
+
+//===----------------------------------------------------------------------===//
+// iree_hal_device_transfer_range implementations
+//===----------------------------------------------------------------------===//
+
+// Performs a full transfer operation on a device transfer queue.
+// This creates a transfer command buffer, submits it against the device, and
+// waits for it to complete synchronously. Implementations that can do this
+// cheaper are encouraged to do so.
+//
+// Precondition: source and target do not overlap.
+IREE_API_EXPORT iree_status_t iree_hal_device_submit_transfer_range_and_wait(
+    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);
+
+// Generic implementation of iree_hal_device_transfer_range for when the buffers
+// are mappable. In certain implementations even if buffers are mappable it's
+// often cheaper to still use the full queue transfers: instead of wasting CPU
+// cycles copying the memory (and possible PCIe round-trips) letting the device
+// do it is effectively free.
+//
+// Precondition: source and target do not overlap.
+IREE_API_EXPORT iree_status_t iree_hal_device_transfer_mappable_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);
+
+//===----------------------------------------------------------------------===//
+// iree_hal_buffer_map_range implementations
+//===----------------------------------------------------------------------===//
+
+// Generic implementation of iree_hal_buffer_map_range and unmap_range for when
+// the buffer is not mappable and a full device transfer is required. This will
+// allocate additional host-local buffers and submit copy commands.
+// Implementations able to do this more efficiently should do so.
+IREE_API_EXPORT iree_status_t iree_hal_buffer_emulated_map_range(
+    iree_hal_device_t* device, iree_hal_buffer_t* buffer,
+    iree_hal_mapping_mode_t mapping_mode,
+    iree_hal_memory_access_t memory_access,
+    iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
+    iree_hal_buffer_mapping_t* mapping);
+IREE_API_EXPORT iree_status_t iree_hal_buffer_emulated_unmap_range(
+    iree_hal_device_t* device, iree_hal_buffer_t* buffer,
+    iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
+    iree_hal_buffer_mapping_t* mapping);
+
+#ifdef __cplusplus
+}  // extern "C"
+#endif  // __cplusplus
+
+#endif  // IREE_HAL_UTILS_BUFFER_TRANSFER_H_
diff --git a/iree/hal/vulkan/BUILD b/iree/hal/vulkan/BUILD
index d531e68..2184d1a 100644
--- a/iree/hal/vulkan/BUILD
+++ b/iree/hal/vulkan/BUILD
@@ -94,6 +94,7 @@
         "//iree/base/internal:synchronization",
         "//iree/base/internal/flatcc:parsing",
         "//iree/hal",
+        "//iree/hal/utils:buffer_transfer",
         "//iree/hal/vulkan/builtin",
         "//iree/hal/vulkan/util:arena",
         "//iree/hal/vulkan/util:intrusive_list",
diff --git a/iree/hal/vulkan/CMakeLists.txt b/iree/hal/vulkan/CMakeLists.txt
index caee945..9d27e03 100644
--- a/iree/hal/vulkan/CMakeLists.txt
+++ b/iree/hal/vulkan/CMakeLists.txt
@@ -84,6 +84,7 @@
     iree::base::logging
     iree::base::tracing
     iree::hal
+    iree::hal::utils::buffer_transfer
     iree::hal::vulkan::builtin
     iree::hal::vulkan::util::arena
     iree::hal::vulkan::util::intrusive_list
diff --git a/iree/hal/vulkan/tracing.cc b/iree/hal/vulkan/tracing.cc
index 36dd3d4..9e0e64f 100644
--- a/iree/hal/vulkan/tracing.cc
+++ b/iree/hal/vulkan/tracing.cc
@@ -17,7 +17,7 @@
 // Total number of queries the per-queue query pool will contain. This
 // translates to the maximum number of outstanding queries before collection is
 // required.
-#define IREE_HAL_VULKAN_TRACING_DEFAULT_QUERY_CAPACITY (64 * 1024)
+#define IREE_HAL_VULKAN_TRACING_DEFAULT_QUERY_CAPACITY (16 * 1024)
 
 // Total number of queries that can be read back from the API in a single
 // collection.
@@ -163,6 +163,7 @@
 static void iree_hal_vulkan_tracing_query_calibration_timestamps(
     iree_hal_vulkan_tracing_context_t* context, uint64_t* out_cpu_time,
     uint64_t* out_gpu_time) {
+  IREE_TRACE_ZONE_BEGIN(z0);
   *out_cpu_time = 0;
   *out_gpu_time = 0;
 
@@ -197,6 +198,8 @@
     default:
       break;
   }
+
+  IREE_TRACE_ZONE_END(z0);
 }
 
 // Populates |out_cpu_time| and |out_gpu_time| with calibrated timestamps.
@@ -210,6 +213,12 @@
   *out_cpu_time = 0;
   *out_gpu_time = 0;
 
+  IREE_TRACE_ZONE_BEGIN(z0);
+  IREE_TRACE_ZONE_APPEND_TEXT(z0,
+                              context->time_domain == VK_TIME_DOMAIN_DEVICE_EXT
+                                  ? "VK_TIME_DOMAIN_DEVICE_EXT"
+                                  : "VK_TIME_DOMAIN_CLOCK_MONOTONIC_EXT");
+
   // Attempt to get a timestamp from both the device and the host at roughly the
   // same time. There's a gap between when we get control returned to use after
   // submitting and waiting for idle and that will be the slop we have in the
@@ -234,6 +243,7 @@
 
     // Reset the query used.
     iree_hal_vulkan_tracing_reset_query_pool(context, 0, 1);
+    IREE_TRACE_ZONE_END(z0);
     return;
   }
 
@@ -258,12 +268,14 @@
   timestamp_infos[1].pNext = NULL;
   timestamp_infos[1].timeDomain = context->time_domain;
   uint64_t max_deviations[IREE_HAL_VULKAN_TRACING_MAX_DEVIATION_PROBE_COUNT];
+  IREE_TRACE_ZONE_BEGIN_NAMED(z1, "vkGetCalibratedTimestampsEXT");
   for (iree_host_size_t i = 0; i < IREE_ARRAYSIZE(max_deviations); ++i) {
     uint64_t timestamps[2] = {0, 0};
     syms->vkGetCalibratedTimestampsEXT(
         *context->logical_device, IREE_ARRAYSIZE(timestamps), timestamp_infos,
         timestamps, &max_deviations[i]);
   }
+  IREE_TRACE_ZONE_END(z1);
   uint64_t min_deviation = max_deviations[0];
   for (iree_host_size_t i = 1; i < IREE_ARRAYSIZE(max_deviations); ++i) {
     min_deviation = iree_min(min_deviation, max_deviations[i]);
@@ -273,6 +285,8 @@
   iree_hal_vulkan_tracing_query_calibration_timestamps(
       context, &context->previous_cpu_time, out_gpu_time);
   *out_cpu_time = tracy::Profiler::GetTime();
+
+  IREE_TRACE_ZONE_END(z0);
 }
 
 // Performs a periodic calibration (if supported) and sends the data to tracy.
@@ -282,6 +296,7 @@
 void iree_hal_vulkan_tracing_perform_calibration(
     iree_hal_vulkan_tracing_context_t* context) {
   if (context->time_domain == VK_TIME_DOMAIN_DEVICE_EXT) return;
+  IREE_TRACE_ZONE_BEGIN(z0);
 
   uint64_t cpu_time = 0;
   uint64_t gpu_time = 0;
@@ -300,28 +315,36 @@
     tracy::MemWrite(&item->gpuCalibration.context, context->id);
     tracy::Profiler::QueueSerialFinish();
   }
+
+  IREE_TRACE_ZONE_END(z0);
 }
 
 // Prepares the VkQueryPool backing storage for our query ringbuffer.
 static void iree_hal_vulkan_tracing_prepare_query_pool(
     iree_hal_vulkan_tracing_context_t* context) {
+  IREE_TRACE_ZONE_BEGIN(z0);
+
   // Create a query pool with the largest query capacity it can provide.
   VkQueryPoolCreateInfo pool_info;
   memset(&pool_info, 0, sizeof(pool_info));
   pool_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
   pool_info.queryCount = IREE_HAL_VULKAN_TRACING_DEFAULT_QUERY_CAPACITY;
   pool_info.queryType = VK_QUERY_TYPE_TIMESTAMP;
+  IREE_TRACE_ZONE_APPEND_VALUE(z0, pool_info.queryCount);
   while (context->logical_device->syms()->vkCreateQueryPool(
              *context->logical_device, &pool_info,
              context->logical_device->allocator(),
              &context->query_pool) != VK_SUCCESS) {
     pool_info.queryCount /= 2;
+    IREE_TRACE_ZONE_APPEND_VALUE(z0, pool_info.queryCount);
   }
   context->query_capacity = pool_info.queryCount;
 
   // Perform initial reset of the query pool. All queries must be reset upon
   // creation before first use.
   iree_hal_vulkan_tracing_reset_query_pool(context, 0, context->query_capacity);
+
+  IREE_TRACE_ZONE_END(z0);
 }
 
 // Prepares the Tracy-related GPU context that events are fed into. Each context
@@ -329,6 +352,8 @@
 static void iree_hal_vulkan_tracing_prepare_gpu_context(
     iree_hal_vulkan_tracing_context_t* context,
     VkPhysicalDevice physical_device, iree_string_view_t queue_name) {
+  IREE_TRACE_ZONE_BEGIN(z0);
+
   // Allocate the process-unique GPU context ID. There's a max of 255 available;
   // if we are recreating devices a lot we may exceed that. Don't do that, or
   // wrap around and get weird (but probably still usable) numbers.
@@ -386,6 +411,8 @@
     tracy::MemWrite(&item->gpuContextNameFat.size, queue_name.size);
     tracy::Profiler::QueueSerialFinish();
   }
+
+  IREE_TRACE_ZONE_END(z0);
 }
 
 // Returns the best possible platform-supported time domain, falling back to
diff --git a/iree/hal/vulkan/vma_allocator.cc b/iree/hal/vulkan/vma_allocator.cc
index 9e58b07..18e49fd 100644
--- a/iree/hal/vulkan/vma_allocator.cc
+++ b/iree/hal/vulkan/vma_allocator.cc
@@ -20,6 +20,7 @@
 
 typedef struct iree_hal_vulkan_vma_allocator_t {
   iree_hal_resource_t resource;
+  iree_hal_device_t* device;  // unretained to avoid cycles
   iree_allocator_t host_allocator;
   VmaAllocator vma;
 
@@ -82,11 +83,12 @@
 
 iree_status_t iree_hal_vulkan_vma_allocator_create(
     VkInstance instance, VkPhysicalDevice physical_device,
-    VkDeviceHandle* logical_device, VmaRecordSettings record_settings,
-    iree_hal_allocator_t** out_allocator) {
+    VkDeviceHandle* logical_device, iree_hal_device_t* device,
+    VmaRecordSettings record_settings, iree_hal_allocator_t** out_allocator) {
   IREE_ASSERT_ARGUMENT(instance);
   IREE_ASSERT_ARGUMENT(physical_device);
   IREE_ASSERT_ARGUMENT(logical_device);
+  IREE_ASSERT_ARGUMENT(device);
   IREE_ASSERT_ARGUMENT(out_allocator);
   IREE_TRACE_ZONE_BEGIN(z0);
 
@@ -98,6 +100,7 @@
   iree_hal_resource_initialize(&iree_hal_vulkan_vma_allocator_vtable,
                                &allocator->resource);
   allocator->host_allocator = host_allocator;
+  allocator->device = device;
 
   const auto& syms = logical_device->syms();
   VmaVulkanFunctions vulkan_fns;
@@ -220,11 +223,13 @@
   iree_hal_buffer_compatibility_t compatibility =
       IREE_HAL_BUFFER_COMPATIBILITY_ALLOCATABLE;
 
+  // All buffers can be used as transfer source/dest.
+  if (iree_all_bits_set(intended_usage, IREE_HAL_BUFFER_USAGE_TRANSFER)) {
+    compatibility |= IREE_HAL_BUFFER_COMPATIBILITY_QUEUE_TRANSFER;
+  }
+
   // Buffers can only be used on the queue if they are device visible.
   if (iree_all_bits_set(memory_type, IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE)) {
-    if (iree_all_bits_set(intended_usage, IREE_HAL_BUFFER_USAGE_TRANSFER)) {
-      compatibility |= IREE_HAL_BUFFER_COMPATIBILITY_QUEUE_TRANSFER;
-    }
     if (iree_all_bits_set(intended_usage, IREE_HAL_BUFFER_USAGE_DISPATCH)) {
       compatibility |= IREE_HAL_BUFFER_COMPATIBILITY_QUEUE_DISPATCH;
     }
@@ -244,7 +249,8 @@
     iree_hal_vulkan_vma_allocator_t* allocator,
     iree_hal_memory_type_t memory_type, iree_hal_buffer_usage_t allowed_usage,
     iree_hal_memory_access_t allowed_access, iree_host_size_t allocation_size,
-    VmaAllocationCreateFlags flags, iree_hal_buffer_t** out_buffer) {
+    iree_const_byte_span_t initial_data, VmaAllocationCreateFlags flags,
+    iree_hal_buffer_t** out_buffer) {
   // Guard against the corner case where the requested buffer size is 0. The
   // application is unlikely to do anything when requesting a 0-byte buffer; but
   // it can happen in real world use cases. So we should at least not crash.
@@ -316,6 +322,9 @@
     allocation_create_info.requiredFlags |= VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
   }
 
+  // TODO(benvanik): if on a unified memory system and initial data is present
+  // we could set the mapping bit and ensure a much more efficient upload.
+
   VkBuffer handle = VK_NULL_HANDLE;
   VmaAllocation allocation = VK_NULL_HANDLE;
   VmaAllocationInfo allocation_info;
@@ -324,18 +333,42 @@
                                      &allocation, &allocation_info),
                      "vmaCreateBuffer");
 
-  return iree_hal_vulkan_vma_buffer_wrap(
+  iree_hal_buffer_t* buffer = NULL;
+  iree_status_t status = iree_hal_vulkan_vma_buffer_wrap(
       (iree_hal_allocator_t*)allocator, memory_type, allowed_access,
       allowed_usage, allocation_size,
       /*byte_offset=*/0,
       /*byte_length=*/allocation_size, allocator->vma, handle, allocation,
-      allocation_info, out_buffer);
+      allocation_info, &buffer);
+  if (!iree_status_is_ok(status)) {
+    vmaDestroyBuffer(allocator->vma, handle, allocation);
+    return status;
+  }
+
+  // Copy the initial contents into the buffer. This may require staging.
+  if (iree_status_is_ok(status) &&
+      !iree_const_byte_span_is_empty(initial_data)) {
+    status = iree_hal_device_transfer_range(
+        allocator->device,
+        iree_hal_make_host_transfer_buffer_span((void*)initial_data.data,
+                                                initial_data.data_length),
+        0, iree_hal_make_device_transfer_buffer(buffer), 0,
+        initial_data.data_length, IREE_HAL_TRANSFER_BUFFER_FLAG_DEFAULT,
+        iree_infinite_timeout());
+  }
+
+  if (iree_status_is_ok(status)) {
+    *out_buffer = buffer;
+  } else {
+    iree_hal_buffer_release(buffer);
+  }
+  return status;
 }
 
 static iree_status_t iree_hal_vulkan_vma_allocator_allocate_buffer(
     iree_hal_allocator_t* base_allocator, iree_hal_memory_type_t memory_type,
     iree_hal_buffer_usage_t allowed_usage, iree_host_size_t allocation_size,
-    iree_hal_buffer_t** out_buffer) {
+    iree_const_byte_span_t initial_data, iree_hal_buffer_t** out_buffer) {
   iree_hal_vulkan_vma_allocator_t* allocator =
       iree_hal_vulkan_vma_allocator_cast(base_allocator);
 
@@ -346,6 +379,7 @@
 
   return iree_hal_vulkan_vma_allocator_allocate_internal(
       allocator, memory_type, allowed_usage, allowed_access, allocation_size,
+      initial_data,
       /*flags=*/0, out_buffer);
 }
 
diff --git a/iree/hal/vulkan/vma_allocator.h b/iree/hal/vulkan/vma_allocator.h
index 71c068b..bc6a17e 100644
--- a/iree/hal/vulkan/vma_allocator.h
+++ b/iree/hal/vulkan/vma_allocator.h
@@ -36,7 +36,8 @@
 iree_status_t iree_hal_vulkan_vma_allocator_create(
     VkInstance instance, VkPhysicalDevice physical_device,
     iree::hal::vulkan::VkDeviceHandle* logical_device,
-    VmaRecordSettings record_settings, iree_hal_allocator_t** out_allocator);
+    iree_hal_device_t* device, VmaRecordSettings record_settings,
+    iree_hal_allocator_t** out_allocator);
 
 #ifdef __cplusplus
 }  // extern "C"
diff --git a/iree/hal/vulkan/vma_buffer.cc b/iree/hal/vulkan/vma_buffer.cc
index b69cd67..7bf517b 100644
--- a/iree/hal/vulkan/vma_buffer.cc
+++ b/iree/hal/vulkan/vma_buffer.cc
@@ -103,15 +103,24 @@
     iree_hal_buffer_t* base_buffer, iree_hal_mapping_mode_t mapping_mode,
     iree_hal_memory_access_t memory_access,
     iree_device_size_t local_byte_offset, iree_device_size_t local_byte_length,
-    void** out_data_ptr) {
+    iree_hal_buffer_mapping_t* mapping) {
   iree_hal_vulkan_vma_buffer_t* buffer =
       iree_hal_vulkan_vma_buffer_cast(base_buffer);
 
+  // TODO(benvanik): add upload/download for unmapped buffers.
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_validate_memory_type(
+      iree_hal_buffer_memory_type(base_buffer),
+      IREE_HAL_MEMORY_TYPE_HOST_VISIBLE));
+  IREE_RETURN_IF_ERROR(
+      iree_hal_buffer_validate_usage(iree_hal_buffer_allowed_usage(base_buffer),
+                                     IREE_HAL_BUFFER_USAGE_MAPPING));
+
   uint8_t* data_ptr = nullptr;
   VK_RETURN_IF_ERROR(
       vmaMapMemory(buffer->vma, buffer->allocation, (void**)&data_ptr),
       "vmaMapMemory");
-  *out_data_ptr = data_ptr + local_byte_offset;
+  mapping->contents =
+      iree_make_byte_span(data_ptr + local_byte_offset, local_byte_length);
 
   // If we mapped for discard scribble over the bytes. This is not a mandated
   // behavior but it will make debugging issues easier. Alternatively for
@@ -119,19 +128,20 @@
   // would only work if the entire buffer was discarded.
 #ifndef NDEBUG
   if (iree_any_bit_set(memory_access, IREE_HAL_MEMORY_ACCESS_DISCARD)) {
-    memset(*out_data_ptr, 0xCD, local_byte_length);
+    memset(mapping->contents.data, 0xCD, local_byte_length);
   }
 #endif  // !NDEBUG
 
   return iree_ok_status();
 }
 
-static void iree_hal_vulkan_vma_buffer_unmap_range(
+static iree_status_t iree_hal_vulkan_vma_buffer_unmap_range(
     iree_hal_buffer_t* base_buffer, iree_device_size_t local_byte_offset,
-    iree_device_size_t local_byte_length, void* data_ptr) {
+    iree_device_size_t local_byte_length, iree_hal_buffer_mapping_t* mapping) {
   iree_hal_vulkan_vma_buffer_t* buffer =
       iree_hal_vulkan_vma_buffer_cast(base_buffer);
   vmaUnmapMemory(buffer->vma, buffer->allocation);
+  return iree_ok_status();
 }
 
 static iree_status_t iree_hal_vulkan_vma_buffer_invalidate_range(
diff --git a/iree/hal/vulkan/vulkan_device.cc b/iree/hal/vulkan/vulkan_device.cc
index c5ef02f..73e4a82 100644
--- a/iree/hal/vulkan/vulkan_device.cc
+++ b/iree/hal/vulkan/vulkan_device.cc
@@ -13,6 +13,7 @@
 
 #include "iree/base/internal/math.h"
 #include "iree/base/tracing.h"
+#include "iree/hal/utils/buffer_transfer.h"
 #include "iree/hal/vulkan/api.h"
 #include "iree/hal/vulkan/builtin_executables.h"
 #include "iree/hal/vulkan/command_queue.h"
@@ -582,8 +583,8 @@
   VmaRecordSettings vma_record_settings;
   memset(&vma_record_settings, 0, sizeof(vma_record_settings));
   iree_status_t status = iree_hal_vulkan_vma_allocator_create(
-      instance, physical_device, logical_device, vma_record_settings,
-      &device->device_allocator);
+      instance, physical_device, logical_device, (iree_hal_device_t*)device,
+      vma_record_settings, &device->device_allocator);
 
   // Create command pools for each queue family. If we don't have a transfer
   // queue then we'll ignore that one and just use the dispatch pool.
@@ -1139,6 +1140,7 @@
     /*.create_executable_layout=*/
     iree_hal_vulkan_device_create_executable_layout,
     /*.create_semaphore=*/iree_hal_vulkan_device_create_semaphore,
+    /*.transfer_range=*/iree_hal_device_submit_transfer_range_and_wait,
     /*.queue_submit=*/iree_hal_vulkan_device_queue_submit,
     /*.submit_and_wait=*/
     iree_hal_vulkan_device_submit_and_wait,
diff --git a/iree/modules/check/check_test.cc b/iree/modules/check/check_test.cc
index 5bcd6a9..142dd6f 100644
--- a/iree/modules/check/check_test.cc
+++ b/iree/modules/check/check_test.cc
@@ -80,18 +80,13 @@
       num_elements *= dim;
     }
     ASSERT_EQ(contents.size(), num_elements);
-    vm::ref<iree_hal_buffer_t> buffer;
-    IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-        allocator_,
-        static_cast<iree_hal_memory_type_t>(
-            IREE_HAL_MEMORY_TYPE_HOST_LOCAL |
-            IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE),
-        IREE_HAL_BUFFER_USAGE_ALL, contents.size() * sizeof(int32_t), &buffer));
-    IREE_ASSERT_OK(iree_hal_buffer_write_data(
-        buffer.get(), 0, contents.data(), contents.size() * sizeof(int32_t)));
-    IREE_ASSERT_OK(iree_hal_buffer_view_create(
-        buffer.get(), shape.data(), shape.size(), IREE_HAL_ELEMENT_TYPE_INT_32,
-        IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, iree_allocator_system(),
+    IREE_ASSERT_OK(iree_hal_buffer_view_allocate_buffer(
+        allocator_, shape.data(), shape.size(), IREE_HAL_ELEMENT_TYPE_INT_32,
+        IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_ALL,
+        iree_make_const_byte_span(contents.data(),
+                                  contents.size() * sizeof(int32_t)),
         &*out_buffer_view));
   }
 
@@ -103,20 +98,14 @@
       num_elements *= dim;
     }
     ASSERT_EQ(contents.size(), num_elements);
-    vm::ref<iree_hal_buffer_t> buffer;
-    IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-        allocator_,
-        static_cast<iree_hal_memory_type_t>(
-            IREE_HAL_MEMORY_TYPE_HOST_LOCAL |
-            IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE),
-        IREE_HAL_BUFFER_USAGE_ALL, contents.size() * sizeof(uint16_t),
-        &buffer));
-    IREE_ASSERT_OK(iree_hal_buffer_write_data(
-        buffer.get(), 0, contents.data(), contents.size() * sizeof(uint16_t)));
-    IREE_ASSERT_OK(iree_hal_buffer_view_create(
-        buffer.get(), shape.data(), shape.size(),
-        IREE_HAL_ELEMENT_TYPE_FLOAT_16, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
-        iree_allocator_system(), &*out_buffer_view));
+    IREE_ASSERT_OK(iree_hal_buffer_view_allocate_buffer(
+        allocator_, shape.data(), shape.size(), IREE_HAL_ELEMENT_TYPE_FLOAT_16,
+        IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_ALL,
+        iree_make_const_byte_span(contents.data(),
+                                  contents.size() * sizeof(uint16_t)),
+        &*out_buffer_view));
   }
 
   void CreateFloat32BufferView(iree::span<const float> contents,
@@ -127,19 +116,14 @@
       num_elements *= dim;
     }
     ASSERT_EQ(contents.size(), num_elements);
-    vm::ref<iree_hal_buffer_t> buffer;
-    IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-        allocator_,
-        static_cast<iree_hal_memory_type_t>(
-            IREE_HAL_MEMORY_TYPE_HOST_LOCAL |
-            IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE),
-        IREE_HAL_BUFFER_USAGE_ALL, contents.size() * sizeof(float), &buffer));
-    IREE_ASSERT_OK(iree_hal_buffer_write_data(buffer.get(), 0, contents.data(),
-                                              contents.size() * sizeof(float)));
-    IREE_ASSERT_OK(iree_hal_buffer_view_create(
-        buffer.get(), shape.data(), shape.size(),
-        IREE_HAL_ELEMENT_TYPE_FLOAT_32, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
-        iree_allocator_system(), &*out_buffer_view));
+    IREE_ASSERT_OK(iree_hal_buffer_view_allocate_buffer(
+        allocator_, shape.data(), shape.size(), IREE_HAL_ELEMENT_TYPE_FLOAT_32,
+        IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_ALL,
+        iree_make_const_byte_span(contents.data(),
+                                  contents.size() * sizeof(float)),
+        &*out_buffer_view));
   }
 
   void CreateFloat64BufferView(iree::span<const double> contents,
@@ -150,19 +134,14 @@
       num_elements *= dim;
     }
     ASSERT_EQ(contents.size(), num_elements);
-    vm::ref<iree_hal_buffer_t> buffer;
-    IREE_ASSERT_OK(iree_hal_allocator_allocate_buffer(
-        allocator_,
-        static_cast<iree_hal_memory_type_t>(
-            IREE_HAL_MEMORY_TYPE_HOST_LOCAL |
-            IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE),
-        IREE_HAL_BUFFER_USAGE_ALL, contents.size() * sizeof(double), &buffer));
-    IREE_ASSERT_OK(iree_hal_buffer_write_data(
-        buffer.get(), 0, contents.data(), contents.size() * sizeof(double)));
-    IREE_ASSERT_OK(iree_hal_buffer_view_create(
-        buffer.get(), shape.data(), shape.size(),
-        IREE_HAL_ELEMENT_TYPE_FLOAT_64, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
-        iree_allocator_system(), &*out_buffer_view));
+    IREE_ASSERT_OK(iree_hal_buffer_view_allocate_buffer(
+        allocator_, shape.data(), shape.size(), IREE_HAL_ELEMENT_TYPE_FLOAT_64,
+        IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_ALL,
+        iree_make_const_byte_span(contents.data(),
+                                  contents.size() * sizeof(double)),
+        &*out_buffer_view));
   }
 
   iree_status_t Invoke(const char* function_name) {
diff --git a/iree/modules/check/module.cc b/iree/modules/check/module.cc
index 061fa71..9996e94 100644
--- a/iree/modules/check/module.cc
+++ b/iree/modules/check/module.cc
@@ -184,13 +184,13 @@
         iree_hal_buffer_view_element_type(view);
     iree_hal_buffer_t* buf = iree_hal_buffer_view_buffer(view);
     iree_device_size_t size = iree_hal_buffer_view_byte_length(view);
-    iree_hal_buffer_mapping_t mapped_memory;
-    IREE_RETURN_IF_ERROR(
-        iree_hal_buffer_map_range(buf, IREE_HAL_MEMORY_ACCESS_READ,
-                                  /*byte_offset=*/0, size, &mapped_memory));
+    iree_hal_buffer_mapping_t mapped_memory = {{0}};
+    IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
+        buf, IREE_HAL_MAPPING_MODE_SCOPED, IREE_HAL_MEMORY_ACCESS_READ,
+        /*byte_offset=*/0, size, &mapped_memory));
     IREE_RETURN_IF_ERROR(
         ::iree::ExpectAllTrue(mapped_memory.contents, element_type));
-    iree_hal_buffer_unmap_range(&mapped_memory);
+    iree_status_ignore(iree_hal_buffer_unmap_range(&mapped_memory));
     return OkStatus();
   }
 
@@ -220,23 +220,26 @@
     iree_hal_element_type_t rhs_element_type =
         iree_hal_buffer_view_element_type(rhs);
 
+    // HACK: this is all broken and will leak. Let's kill this entire module
+    // please.
+
     iree_hal_buffer_t* lhs_buf = iree_hal_buffer_view_buffer(lhs);
-    iree_hal_buffer_mapping_t lhs_mapped_memory;
+    iree_hal_buffer_mapping_t lhs_mapped_memory = {{0}};
     IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-        lhs_buf, IREE_HAL_MEMORY_ACCESS_READ,
+        lhs_buf, IREE_HAL_MAPPING_MODE_SCOPED, IREE_HAL_MEMORY_ACCESS_READ,
         /*byte_offset=*/0, lhs_size, &lhs_mapped_memory));
     iree_hal_buffer_t* rhs_buf = iree_hal_buffer_view_buffer(rhs);
-    iree_hal_buffer_mapping_t rhs_mapped_memory;
+    iree_hal_buffer_mapping_t rhs_mapped_memory = {{0}};
     IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-        rhs_buf, IREE_HAL_MEMORY_ACCESS_READ,
+        rhs_buf, IREE_HAL_MAPPING_MODE_SCOPED, IREE_HAL_MEMORY_ACCESS_READ,
         /*byte_offset=*/0, rhs_size, &rhs_mapped_memory));
 
     bool element_types_eq = lhs_element_type == rhs_element_type;
     bool shape_eq = lhs_shape == rhs_shape;
     bool contents_eq =
         EqByteSpan(lhs_mapped_memory.contents, rhs_mapped_memory.contents);
-    iree_hal_buffer_unmap_range(&lhs_mapped_memory);
-    iree_hal_buffer_unmap_range(&rhs_mapped_memory);
+    iree_status_ignore(iree_hal_buffer_unmap_range(&lhs_mapped_memory));
+    iree_status_ignore(iree_hal_buffer_unmap_range(&rhs_mapped_memory));
 
     if (!element_types_eq || !shape_eq || !contents_eq) {
       std::ostringstream os;
@@ -297,14 +300,14 @@
         iree_hal_buffer_view_element_type(rhs);
 
     iree_hal_buffer_t* lhs_buf = iree_hal_buffer_view_buffer(lhs);
-    iree_hal_buffer_mapping_t lhs_mapped_memory;
+    iree_hal_buffer_mapping_t lhs_mapped_memory = {{0}};
     IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-        lhs_buf, IREE_HAL_MEMORY_ACCESS_READ,
+        lhs_buf, IREE_HAL_MAPPING_MODE_SCOPED, IREE_HAL_MEMORY_ACCESS_READ,
         /*byte_offset=*/0, lhs_size, &lhs_mapped_memory));
     iree_hal_buffer_t* rhs_buf = iree_hal_buffer_view_buffer(rhs);
-    iree_hal_buffer_mapping_t rhs_mapped_memory;
+    iree_hal_buffer_mapping_t rhs_mapped_memory = {{0}};
     IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-        rhs_buf, IREE_HAL_MEMORY_ACCESS_READ,
+        rhs_buf, IREE_HAL_MAPPING_MODE_SCOPED, IREE_HAL_MEMORY_ACCESS_READ,
         /*byte_offset=*/0, rhs_size, &rhs_mapped_memory));
 
     bool element_types_eq = lhs_element_type == rhs_element_type;
@@ -317,8 +320,8 @@
           AlmostEqByteSpan(lhs_mapped_memory.contents,
                            rhs_mapped_memory.contents, lhs_element_type));
     }
-    iree_hal_buffer_unmap_range(&lhs_mapped_memory);
-    iree_hal_buffer_unmap_range(&rhs_mapped_memory);
+    iree_status_ignore(iree_hal_buffer_unmap_range(&lhs_mapped_memory));
+    iree_status_ignore(iree_hal_buffer_unmap_range(&rhs_mapped_memory));
 
     if (!element_types_eq || !shape_eq || !contents_could_be_almost_eq) {
       std::ostringstream os;
diff --git a/iree/modules/hal/module.c b/iree/modules/hal/module.c
index 4a4d6f3..0a6f449 100644
--- a/iree/modules/hal/module.c
+++ b/iree/modules/hal/module.c
@@ -284,7 +284,8 @@
 
   iree_hal_buffer_t* buffer = NULL;
   IREE_RETURN_IF_ERROR(iree_hal_allocator_allocate_buffer(
-      allocator, memory_types, buffer_usage, allocation_size, &buffer));
+      allocator, memory_types, buffer_usage, allocation_size,
+      iree_const_byte_span_empty(), &buffer));
   rets->r0 = iree_hal_buffer_move_ref(buffer);
   return iree_ok_status();
 }
@@ -403,18 +404,14 @@
 
   iree_hal_buffer_t* buffer = NULL;
   IREE_RETURN_IF_ERROR(
-      iree_hal_allocator_allocate_buffer(allocator, memory_types, buffer_usage,
-                                         length, &buffer),
+      iree_hal_allocator_allocate_buffer(
+          allocator, memory_types, buffer_usage, length,
+          iree_make_const_byte_span(source->data.data + offset, length),
+          &buffer),
       "failed to allocate buffer of length %d", length);
 
-  iree_status_t status =
-      iree_hal_buffer_write_data(buffer, 0, source->data.data + offset, length);
-  if (iree_status_is_ok(status)) {
-    rets->r0 = iree_hal_buffer_move_ref(buffer);
-  } else {
-    iree_hal_buffer_release(buffer);
-  }
-  return status;
+  rets->r0 = iree_hal_buffer_move_ref(buffer);
+  return iree_ok_status();
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/iree/runtime/demo/hello_world_explained.c b/iree/runtime/demo/hello_world_explained.c
index cbdfb09..589a1d1 100644
--- a/iree/runtime/demo/hello_world_explained.c
+++ b/iree/runtime/demo/hello_world_explained.c
@@ -183,6 +183,8 @@
   // in other sessions depending on whether they share a compatible device.
   iree_hal_allocator_t* device_allocator =
       iree_runtime_session_device_allocator(session);
+  iree_allocator_t host_allocator =
+      iree_runtime_session_host_allocator(session);
   iree_status_t status = iree_ok_status();
   {
     // %arg0: tensor<4xf32>
@@ -212,7 +214,7 @@
     }
     if (iree_status_is_ok(status)) {
       IREE_IGNORE_ERROR(iree_hal_buffer_view_fprint(
-          stdout, arg0, /*max_element_count=*/4096));
+          stdout, arg0, /*max_element_count=*/4096, host_allocator));
       // Add to the call inputs list (which retains the buffer view).
       status = iree_runtime_call_inputs_push_back_buffer_view(&call, arg0);
     }
@@ -237,7 +239,7 @@
     }
     if (iree_status_is_ok(status)) {
       IREE_IGNORE_ERROR(iree_hal_buffer_view_fprint(
-          stdout, arg1, /*max_element_count=*/4096));
+          stdout, arg1, /*max_element_count=*/4096, host_allocator));
       status = iree_runtime_call_inputs_push_back_buffer_view(&call, arg1);
     }
     iree_hal_buffer_view_release(arg1);
@@ -259,8 +261,8 @@
   if (iree_status_is_ok(status)) {
     // This prints the buffer view out but an application could read its
     // contents, pass it to another call, etc.
-    status =
-        iree_hal_buffer_view_fprint(stdout, ret0, /*max_element_count=*/4096);
+    status = iree_hal_buffer_view_fprint(
+        stdout, ret0, /*max_element_count=*/4096, host_allocator);
   }
   iree_hal_buffer_view_release(ret0);
 
diff --git a/iree/runtime/demo/hello_world_terse.c b/iree/runtime/demo/hello_world_terse.c
index 3a10d32..b333e60 100644
--- a/iree/runtime/demo/hello_world_terse.c
+++ b/iree/runtime/demo/hello_world_terse.c
@@ -87,8 +87,9 @@
       IREE_HAL_MEMORY_ACCESS_READ, IREE_HAL_BUFFER_USAGE_ALL,
       iree_make_byte_span((void*)arg0_data, sizeof(arg0_data)),
       iree_allocator_null(), &arg0));
-  IREE_CHECK_OK(
-      iree_hal_buffer_view_fprint(stdout, arg0, /*max_element_count=*/4096));
+  IREE_CHECK_OK(iree_hal_buffer_view_fprint(
+      stdout, arg0, /*max_element_count=*/4096,
+      iree_runtime_session_host_allocator(session)));
   IREE_CHECK_OK(iree_runtime_call_inputs_push_back_buffer_view(&call, arg0));
   iree_hal_buffer_view_release(arg0);
 
@@ -106,8 +107,9 @@
       IREE_HAL_MEMORY_ACCESS_READ, IREE_HAL_BUFFER_USAGE_ALL,
       iree_make_byte_span((void*)arg1_data, sizeof(arg1_data)),
       iree_allocator_null(), &arg1));
-  IREE_CHECK_OK(
-      iree_hal_buffer_view_fprint(stdout, arg1, /*max_element_count=*/4096));
+  IREE_CHECK_OK(iree_hal_buffer_view_fprint(
+      stdout, arg1, /*max_element_count=*/4096,
+      iree_runtime_session_host_allocator(session)));
   IREE_CHECK_OK(iree_runtime_call_inputs_push_back_buffer_view(&call, arg1));
   iree_hal_buffer_view_release(arg1);
 
@@ -118,8 +120,9 @@
   // -> tensor<4xf32>
   iree_hal_buffer_view_t* ret0 = NULL;
   IREE_CHECK_OK(iree_runtime_call_outputs_pop_front_buffer_view(&call, &ret0));
-  IREE_CHECK_OK(
-      iree_hal_buffer_view_fprint(stdout, ret0, /*max_element_count=*/4096));
+  IREE_CHECK_OK(iree_hal_buffer_view_fprint(
+      stdout, ret0, /*max_element_count=*/4096,
+      iree_runtime_session_host_allocator(session)));
   iree_hal_buffer_view_release(ret0);
 
   iree_runtime_call_deinitialize(&call);
diff --git a/iree/samples/dynamic_shapes/main.c b/iree/samples/dynamic_shapes/main.c
index f82737c..3808a33 100644
--- a/iree/samples/dynamic_shapes/main.c
+++ b/iree/samples/dynamic_shapes/main.c
@@ -21,7 +21,7 @@
   //   * debugging some apparent memory corruption with the stack-local value
   iree_status_t status = iree_ok_status();
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_clone_heap_buffer(
+    status = iree_hal_buffer_view_allocate_buffer(
         iree_runtime_session_device_allocator(session), arg0_shape,
         IREE_ARRAYSIZE(arg0_shape), IREE_HAL_ELEMENT_TYPE_SINT_32,
         IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
@@ -43,16 +43,10 @@
     status =
         iree_runtime_call_outputs_pop_front_buffer_view(&call, &buffer_view);
   }
-  iree_hal_buffer_mapping_t buffer_mapping;
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_map_range(iree_hal_buffer_view_buffer(buffer_view),
-                                       IREE_HAL_MEMORY_ACCESS_READ, 0,
-                                       IREE_WHOLE_BUFFER, &buffer_mapping);
+    status = iree_hal_buffer_read_data(iree_hal_buffer_view_buffer(buffer_view),
+                                       0, out_result, sizeof(*out_result));
   }
-  if (iree_status_is_ok(status)) {
-    *out_result = *buffer_mapping.contents.data;
-  }
-  iree_hal_buffer_unmap_range(&buffer_mapping);
   iree_hal_buffer_view_release(buffer_view);
 
   iree_runtime_call_deinitialize(&call);
@@ -73,7 +67,7 @@
   //   * debugging some apparent memory corruption with the stack-local value
   iree_status_t status = iree_ok_status();
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_clone_heap_buffer(
+    status = iree_hal_buffer_view_allocate_buffer(
         iree_runtime_session_device_allocator(session), arg0_shape,
         IREE_ARRAYSIZE(arg0_shape), IREE_HAL_ELEMENT_TYPE_SINT_32,
         IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
@@ -113,7 +107,7 @@
   //   * debugging some apparent memory corruption with the stack-local value
   iree_status_t status = iree_ok_status();
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_clone_heap_buffer(
+    status = iree_hal_buffer_view_allocate_buffer(
         iree_runtime_session_device_allocator(session), arg0_shape,
         IREE_ARRAYSIZE(arg0_shape), IREE_HAL_ELEMENT_TYPE_SINT_32,
         IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
@@ -204,8 +198,10 @@
     status = reduce_sum_2d(session, input, 6, &result_buffer_view);
     if (iree_status_is_ok(status)) {
       fprintf(stdout, "reduce_sum_2d([[1, 2, 3], [10, 20, 30]]): ");
-      status = iree_hal_buffer_view_fprint(stdout, result_buffer_view,
-                                           /*max_element_count=*/4096);
+      status = iree_hal_buffer_view_fprint(
+          stdout, result_buffer_view,
+          /*max_element_count=*/4096,
+          iree_runtime_session_host_allocator(session));
       fprintf(stdout, "\n");
     }
     iree_hal_buffer_view_release(result_buffer_view);
@@ -219,8 +215,10 @@
     if (iree_status_is_ok(status)) {
       fprintf(stdout,
               "reduce_sum_2d([[1, 2, 3], [10, 20, 30], [100, 200, 300]]): ");
-      status = iree_hal_buffer_view_fprint(stdout, result_buffer_view,
-                                           /*max_element_count=*/4096);
+      status = iree_hal_buffer_view_fprint(
+          stdout, result_buffer_view,
+          /*max_element_count=*/4096,
+          iree_runtime_session_host_allocator(session));
       fprintf(stdout, "\n");
     }
     iree_hal_buffer_view_release(result_buffer_view);
@@ -233,8 +231,10 @@
     status = add_one(session, input, 3, &result_buffer_view);
     if (iree_status_is_ok(status)) {
       fprintf(stdout, "add_one([1, 10, 100]): ");
-      status = iree_hal_buffer_view_fprint(stdout, result_buffer_view,
-                                           /*max_element_count=*/64);
+      status = iree_hal_buffer_view_fprint(
+          stdout, result_buffer_view,
+          /*max_element_count=*/64,
+          iree_runtime_session_host_allocator(session));
       fprintf(stdout, "\n");
     }
     iree_hal_buffer_view_release(result_buffer_view);
diff --git a/iree/samples/simple_embedding/simple_embedding.c b/iree/samples/simple_embedding/simple_embedding.c
index db9f2b6..94f2dee 100644
--- a/iree/samples/simple_embedding/simple_embedding.c
+++ b/iree/samples/simple_embedding/simple_embedding.c
@@ -7,6 +7,10 @@
 // A example of setting up the HAL module to run simple pointwise array
 // multiplication with the device implemented by different backends via
 // create_sample_driver().
+//
+// NOTE: this file does not properly handle error cases and will leak on
+// failure. Applications that are just going to exit()/abort() on failure can
+// probably get away with the same thing but really should prefer not to.
 
 #include <stdio.h>
 
@@ -66,42 +70,29 @@
   IREE_RETURN_IF_ERROR(iree_vm_context_resolve_function(
       context, iree_make_cstring_view(kMainFunctionName), &main_function));
 
-  // Allocate buffers that can be mapped on the CPU and that can also be used
-  // on the device. Not all devices support this, but the ones we have now do.
-  const int kElementCount = 4;
-  iree_hal_buffer_t* arg0_buffer = NULL;
-  iree_hal_buffer_t* arg1_buffer = NULL;
-  iree_hal_memory_type_t input_memory_type =
-      IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE;
-  IREE_RETURN_IF_ERROR(iree_hal_allocator_allocate_buffer(
-      iree_hal_device_allocator(device), input_memory_type,
-      IREE_HAL_BUFFER_USAGE_ALL, sizeof(float) * kElementCount, &arg0_buffer));
-  IREE_RETURN_IF_ERROR(iree_hal_allocator_allocate_buffer(
-      iree_hal_device_allocator(device), input_memory_type,
-      IREE_HAL_BUFFER_USAGE_ALL, sizeof(float) * kElementCount, &arg1_buffer));
+  // Initial buffer contents for 4 * 2 = 8.
+  const float kFloat4[] = {4.0f, 4.0f, 4.0f, 4.0f};
+  const float kFloat2[] = {2.0f, 2.0f, 2.0f, 2.0f};
 
-  // Populate initial values for 4 * 2 = 8.
-  const float kFloat4 = 4.0f;
-  const float kFloat2 = 2.0f;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_fill(arg0_buffer, 0, IREE_WHOLE_BUFFER,
-                                            &kFloat4, sizeof(float)));
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_fill(arg1_buffer, 0, IREE_WHOLE_BUFFER,
-                                            &kFloat2, sizeof(float)));
-
-  // Wrap buffers in shaped buffer views.
-  iree_hal_dim_t shape[1] = {kElementCount};
+  // Allocate buffers in device-local memory so that if the device has an
+  // independent address space they live on the fast side of the fence.
+  iree_hal_dim_t shape[1] = {IREE_ARRAYSIZE(kFloat4)};
   iree_hal_buffer_view_t* arg0_buffer_view = NULL;
   iree_hal_buffer_view_t* arg1_buffer_view = NULL;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_view_create(
-      arg0_buffer, shape, IREE_ARRAYSIZE(shape), IREE_HAL_ELEMENT_TYPE_FLOAT_32,
-      IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, iree_allocator_system(),
-      &arg0_buffer_view));
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_view_create(
-      arg1_buffer, shape, IREE_ARRAYSIZE(shape), IREE_HAL_ELEMENT_TYPE_FLOAT_32,
-      IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, iree_allocator_system(),
-      &arg1_buffer_view));
-  iree_hal_buffer_release(arg0_buffer);
-  iree_hal_buffer_release(arg1_buffer);
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_view_allocate_buffer(
+      iree_hal_device_allocator(device), shape, IREE_ARRAYSIZE(shape),
+      IREE_HAL_ELEMENT_TYPE_FLOAT_32, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+      IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
+      IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER |
+          IREE_HAL_BUFFER_USAGE_MAPPING,
+      iree_make_const_byte_span(kFloat4, sizeof(kFloat4)), &arg0_buffer_view));
+  IREE_RETURN_IF_ERROR(iree_hal_buffer_view_allocate_buffer(
+      iree_hal_device_allocator(device), shape, IREE_ARRAYSIZE(shape),
+      IREE_HAL_ELEMENT_TYPE_FLOAT_32, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
+      IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
+      IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER |
+          IREE_HAL_BUFFER_USAGE_MAPPING,
+      iree_make_const_byte_span(kFloat2, sizeof(kFloat2)), &arg1_buffer_view));
 
   // Setup call inputs with our buffers.
   iree_vm_list_t* inputs = NULL;
@@ -142,16 +133,15 @@
   }
 
   // Read back the results and ensure we got the right values.
-  iree_hal_buffer_mapping_t mapped_memory;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-      iree_hal_buffer_view_buffer(ret_buffer_view), IREE_HAL_MEMORY_ACCESS_READ,
-      0, IREE_WHOLE_BUFFER, &mapped_memory));
-  for (int i = 0; i < mapped_memory.contents.data_length / sizeof(float); ++i) {
-    if (((const float*)mapped_memory.contents.data)[i] != 8.0f) {
+  float results[] = {0.0f, 0.0f, 0.0f, 0.0f};
+  IREE_RETURN_IF_ERROR(
+      iree_hal_buffer_read_data(iree_hal_buffer_view_buffer(ret_buffer_view), 0,
+                                results, sizeof(results)));
+  for (iree_host_size_t i = 0; i < IREE_ARRAYSIZE(results); ++i) {
+    if (results[i] != 8.0f) {
       return iree_make_status(IREE_STATUS_UNKNOWN, "result mismatches");
     }
   }
-  iree_hal_buffer_unmap_range(&mapped_memory);
 
   iree_vm_list_release(inputs);
   iree_vm_list_release(outputs);
diff --git a/iree/samples/static_library/static_library_demo.c b/iree/samples/static_library/static_library_demo.c
index 637d01b..79c802e 100644
--- a/iree/samples/static_library/static_library_demo.c
+++ b/iree/samples/static_library/static_library_demo.c
@@ -127,7 +127,7 @@
   iree_hal_memory_type_t input_memory_type =
       IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE;
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_clone_heap_buffer(
+    status = iree_hal_buffer_view_allocate_buffer(
         iree_hal_device_allocator(device), shape, IREE_ARRAYSIZE(shape),
         IREE_HAL_ELEMENT_TYPE_FLOAT_32, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
         input_memory_type, IREE_HAL_BUFFER_USAGE_ALL,
@@ -136,7 +136,7 @@
         &arg0_buffer_view);
   }
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_clone_heap_buffer(
+    status = iree_hal_buffer_view_allocate_buffer(
         iree_hal_device_allocator(device), shape, IREE_ARRAYSIZE(shape),
         IREE_HAL_ELEMENT_TYPE_FLOAT_32, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
         input_memory_type, IREE_HAL_BUFFER_USAGE_ALL,
@@ -171,31 +171,22 @@
   }
 
   // Read back the results and ensure we got the right values.
-  iree_hal_buffer_mapping_t mapped_memory;
-  memset(&mapped_memory, 0, sizeof(mapped_memory));
+  float results[] = {0.0f, 0.0f, 0.0f, 0.0f};
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_map_range(
-        iree_hal_buffer_view_buffer(ret_buffer_view),
-        IREE_HAL_MEMORY_ACCESS_READ, 0, IREE_WHOLE_BUFFER, &mapped_memory);
+    status =
+        iree_hal_buffer_read_data(iree_hal_buffer_view_buffer(ret_buffer_view),
+                                  0, results, sizeof(results));
   }
   if (iree_status_is_ok(status)) {
-    if (mapped_memory.contents.data_length / sizeof(float) != kElementCount) {
-      status = iree_make_status(IREE_STATUS_UNKNOWN,
-                                "result does not match element count ");
-    }
-  }
-  if (iree_status_is_ok(status)) {
-    const float* data = (const float*)mapped_memory.contents.data;
-    for (iree_host_size_t i = 0;
-         i < mapped_memory.contents.data_length / sizeof(float); ++i) {
-      if (data[i] != 8.0f) {
+    for (iree_host_size_t i = 0; i < IREE_ARRAYSIZE(results); ++i) {
+      if (results[i] != 8.0f) {
         status = iree_make_status(IREE_STATUS_UNKNOWN, "result mismatches");
+        break;
       }
     }
   }
 
   // Cleanup call and buffers.
-  iree_hal_buffer_unmap_range(&mapped_memory);
   iree_hal_buffer_view_release(ret_buffer_view);
   iree_runtime_call_deinitialize(&call);
 
diff --git a/iree/samples/variables_and_state/main.c b/iree/samples/variables_and_state/main.c
index 302ff4e..aa08ad0 100644
--- a/iree/samples/variables_and_state/main.c
+++ b/iree/samples/variables_and_state/main.c
@@ -23,16 +23,10 @@
     status =
         iree_runtime_call_outputs_pop_front_buffer_view(&call, &buffer_view);
   }
-  iree_hal_buffer_mapping_t buffer_mapping;
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_map_range(iree_hal_buffer_view_buffer(buffer_view),
-                                       IREE_HAL_MEMORY_ACCESS_READ, 0,
-                                       IREE_WHOLE_BUFFER, &buffer_mapping);
+    status = iree_hal_buffer_read_data(iree_hal_buffer_view_buffer(buffer_view),
+                                       0, out_value, sizeof(*out_value));
   }
-  if (iree_status_is_ok(status)) {
-    *out_value = *buffer_mapping.contents.data;
-  }
-  iree_hal_buffer_unmap_range(&buffer_mapping);
   iree_hal_buffer_view_release(buffer_view);
 
   iree_runtime_call_deinitialize(&call);
@@ -52,7 +46,7 @@
   //   * debugging some apparent memory corruption with the stack-local value
   iree_status_t status = iree_ok_status();
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_clone_heap_buffer(
+    status = iree_hal_buffer_view_allocate_buffer(
         iree_runtime_session_device_allocator(session), /*shape=*/NULL,
         /*shape_rank=*/0, IREE_HAL_ELEMENT_TYPE_SINT_32,
         IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
@@ -84,7 +78,7 @@
   //   * debugging some apparent memory corruption with the stack-local value
   iree_status_t status = iree_ok_status();
   if (iree_status_is_ok(status)) {
-    status = iree_hal_buffer_view_clone_heap_buffer(
+    status = iree_hal_buffer_view_allocate_buffer(
         iree_runtime_session_device_allocator(session), /*shape=*/NULL,
         /*shape_rank=*/0, IREE_HAL_ELEMENT_TYPE_SINT_32,
         IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
diff --git a/iree/samples/vision/iree-run-mnist-module.c b/iree/samples/vision/iree-run-mnist-module.c
index bfd9d7c..d525bdd 100644
--- a/iree/samples/vision/iree-run-mnist-module.c
+++ b/iree/samples/vision/iree-run-mnist-module.c
@@ -73,23 +73,22 @@
 
   // Read back the results. The output of the mnist model is a 1x10 prediction
   // confidence values for each digit in [0, 9].
-  iree_hal_buffer_mapping_t mapped_memory;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-      iree_hal_buffer_view_buffer(ret_buffer_view), IREE_HAL_MEMORY_ACCESS_READ,
-      0, IREE_WHOLE_BUFFER, &mapped_memory));
+  float predictions[1 * 10] = {0.0f};
+  IREE_RETURN_IF_ERROR(
+      iree_hal_buffer_read_data(iree_hal_buffer_view_buffer(ret_buffer_view), 0,
+                                predictions, sizeof(predictions)));
+  iree_hal_buffer_view_release(ret_buffer_view);
+
+  // Get the highest index from the output.
   float result_val = FLT_MIN;
   int result_idx = 0;
-  const float* data_ptr = (const float*)mapped_memory.contents.data;
-  for (int i = 0; i < mapped_memory.contents.data_length / sizeof(float); ++i) {
-    if (data_ptr[i] > result_val) {
-      result_val = data_ptr[i];
+  for (iree_host_size_t i = 0; i < IREE_ARRAYSIZE(predictions); ++i) {
+    if (predictions[i] > result_val) {
+      result_val = predictions[i];
       result_idx = i;
     }
   }
-  iree_hal_buffer_unmap_range(&mapped_memory);
-  // Get the highest index from the output.
   fprintf(stdout, "Detected number: %d\n", result_idx);
-  iree_hal_buffer_view_release(ret_buffer_view);
 
   iree_runtime_call_deinitialize(&call);
   iree_runtime_session_release(session);
diff --git a/iree/samples/vulkan/vulkan_inference_gui.cc b/iree/samples/vulkan/vulkan_inference_gui.cc
index 4939b93..9d895a1 100644
--- a/iree/samples/vulkan/vulkan_inference_gui.cc
+++ b/iree/samples/vulkan/vulkan_inference_gui.cc
@@ -71,8 +71,10 @@
   }
 
   // Setup window
+  // clang-format off
   SDL_WindowFlags window_flags = (SDL_WindowFlags)(
       SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
+  // clang-format on
   SDL_Window* window = SDL_CreateWindow(
       "IREE Samples - Vulkan Inference GUI", SDL_WINDOWPOS_CENTERED,
       SDL_WINDOWPOS_CENTERED, 1280, 720, window_flags);
@@ -363,8 +365,6 @@
         constexpr int32_t kElementCount = 4;
         iree_hal_allocator_t* allocator =
             iree_hal_device_allocator(iree_vk_device);
-        iree_hal_buffer_t* input0_buffer = nullptr;
-        iree_hal_buffer_t* input1_buffer = nullptr;
         iree_hal_memory_type_t input_memory_type =
             static_cast<iree_hal_memory_type_t>(
                 IREE_HAL_MEMORY_TYPE_HOST_LOCAL |
@@ -372,33 +372,25 @@
         iree_hal_buffer_usage_t input_buffer_usage =
             static_cast<iree_hal_buffer_usage_t>(
                 IREE_HAL_BUFFER_USAGE_ALL | IREE_HAL_BUFFER_USAGE_CONSTANT);
-        IREE_CHECK_OK(iree_hal_allocator_allocate_buffer(
-            allocator, input_memory_type, input_buffer_usage,
-            sizeof(float) * kElementCount, &input0_buffer));
-        IREE_CHECK_OK(iree_hal_allocator_allocate_buffer(
-            allocator, input_memory_type, input_buffer_usage,
-            sizeof(float) * kElementCount, &input1_buffer));
-        IREE_CHECK_OK(iree_hal_buffer_write_data(input0_buffer, 0, &input_x,
-                                                 sizeof(input_x)));
-        IREE_CHECK_OK(iree_hal_buffer_write_data(input1_buffer, 0, &input_y,
-                                                 sizeof(input_y)));
         // Wrap input buffers in buffer views.
         iree_hal_buffer_view_t* input0_buffer_view = nullptr;
         iree_hal_buffer_view_t* input1_buffer_view = nullptr;
-        IREE_CHECK_OK(iree_hal_buffer_view_create(
-            input0_buffer,
+        IREE_CHECK_OK(iree_hal_buffer_view_allocate_buffer(
+            allocator,
             /*shape=*/&kElementCount, /*shape_rank=*/1,
             IREE_HAL_ELEMENT_TYPE_FLOAT_32,
-            IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, iree_allocator_system(),
-            &input0_buffer_view));
-        IREE_CHECK_OK(iree_hal_buffer_view_create(
-            input1_buffer,
+            IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, input_memory_type,
+            input_buffer_usage,
+            iree_make_const_byte_span(&input_x, sizeof(input_x)),
+            iree_allocator_system(), &input0_buffer_view));
+        IREE_CHECK_OK(iree_hal_buffer_view_allocate_buffer(
+            allocator,
             /*shape=*/&kElementCount, /*shape_rank=*/1,
             IREE_HAL_ELEMENT_TYPE_FLOAT_32,
-            IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, iree_allocator_system(),
-            &input1_buffer_view));
-        iree_hal_buffer_release(input0_buffer);
-        iree_hal_buffer_release(input1_buffer);
+            IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR, input_memory_type,
+            input_buffer_usage,
+            iree_make_const_byte_span(&input_y, sizeof(input_y)),
+            iree_allocator_system(), &input1_buffer_view));
         // Marshal inputs through a VM variant list.
         // [wait_semaphore|wait_value|arg0|arg1|signal_semaphore|signal_value]
         vm::ref<iree_vm_list_t> inputs;
diff --git a/iree/tools/iree-e2e-matmul-test.c b/iree/tools/iree-e2e-matmul-test.c
index 4d622a0..8646827 100644
--- a/iree/tools/iree-e2e-matmul-test.c
+++ b/iree/tools/iree-e2e-matmul-test.c
@@ -66,9 +66,10 @@
     return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
                             "buffer_view is not dense row major");
   }
-  iree_hal_buffer_mapping_t mapping;
+  iree_hal_buffer_mapping_t mapping = {{0}};
   IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-      iree_hal_buffer_view_buffer(buffer_view), IREE_HAL_MEMORY_ACCESS_READ, 0,
+      iree_hal_buffer_view_buffer(buffer_view),
+      IREE_HAL_MAPPING_MODE_PERSISTENT, IREE_HAL_MEMORY_ACCESS_READ, 0,
       IREE_WHOLE_BUFFER, &mapping));
   *data = mapping.contents.data;
   return iree_ok_status();
@@ -478,21 +479,23 @@
       iree_hal_buffer_view_element_type(src),
       iree_hal_buffer_view_encoding_type(src),
       IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
-      IREE_HAL_BUFFER_USAGE_ALL, dst);
+      IREE_HAL_BUFFER_USAGE_ALL, iree_const_byte_span_empty(), dst);
 }
 
 // Performs a deep copy of |src| into |dst|. Takes care of allocating |dst|.
 static iree_status_t copy_buffer(iree_hal_allocator_t* hal_allocator,
                                  iree_hal_buffer_view_t* src,
                                  iree_hal_buffer_view_t** dst) {
-  iree_hal_buffer_mapping_t src_mapping;
+  // TODO(benvanik): change this to use iree_hal_buffer_copy_data. Or something.
+  // I can't understand what all this code is doing.
+  iree_hal_buffer_mapping_t src_mapping = {{0}};
   IREE_RETURN_IF_ERROR(iree_hal_buffer_map_range(
-      iree_hal_buffer_view_buffer(src), IREE_HAL_MEMORY_ACCESS_READ, 0,
-      IREE_WHOLE_BUFFER, &src_mapping));
+      iree_hal_buffer_view_buffer(src), IREE_HAL_MAPPING_MODE_PERSISTENT,
+      IREE_HAL_MEMORY_ACCESS_READ, 0, IREE_WHOLE_BUFFER, &src_mapping));
   iree_const_byte_span_t src_span;
   src_span.data = src_mapping.contents.data;
   src_span.data_length = src_mapping.contents.data_length;
-  return iree_hal_buffer_view_clone_heap_buffer(
+  return iree_hal_buffer_view_allocate_buffer(
       hal_allocator, iree_hal_buffer_view_shape_dims(src),
       iree_hal_buffer_view_shape_rank(src),
       iree_hal_buffer_view_element_type(src),
@@ -551,7 +554,8 @@
   // linalg.matmul. We need to preserve the original test inputs to run the
   // reference matmul on and to use in test failure logs.
   iree_vm_list_t* copy_of_input_list = NULL;
-  copy_list_of_buffer_views(device_allocator, input_list, &copy_of_input_list);
+  IREE_CHECK_OK(copy_list_of_buffer_views(device_allocator, input_list,
+                                          &copy_of_input_list));
 
   // Invoke the function to produce the actual result.
   iree_vm_list_t* output_list = NULL;
diff --git a/iree/tools/utils/image_util.c b/iree/tools/utils/image_util.c
index 61e1486..94aaab0 100644
--- a/iree/tools/utils/image_util.c
+++ b/iree/tools/utils/image_util.c
@@ -148,11 +148,27 @@
   return result;
 }
 
+typedef struct iree_tools_utils_buffer_view_load_params_t {
+  const uint8_t* pixel_data;
+  iree_host_size_t pixel_data_length;
+  const float* input_range;
+  iree_host_size_t input_range_length;
+} iree_tools_utils_buffer_view_load_params_t;
+static iree_status_t iree_tools_utils_buffer_view_load_image_rescaled(
+    iree_hal_buffer_mapping_t* mapping, void* user_data) {
+  iree_tools_utils_buffer_view_load_params_t* params =
+      (iree_tools_utils_buffer_view_load_params_t*)user_data;
+  return iree_tools_utils_pixel_rescaled_to_buffer(
+      params->pixel_data, params->pixel_data_length, params->input_range,
+      params->input_range_length, (float*)mapping->contents.data);
+}
+
 iree_status_t iree_tools_utils_buffer_view_from_image_rescaled(
     const iree_string_view_t filename, const iree_hal_dim_t* shape,
     iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
     iree_hal_allocator_t* allocator, const float* input_range,
-    iree_host_size_t range_length, iree_hal_buffer_view_t** out_buffer_view) {
+    iree_host_size_t input_range_length,
+    iree_hal_buffer_view_t** out_buffer_view) {
   IREE_TRACE_ZONE_BEGIN(z0);
   *out_buffer_view = NULL;
   if (element_type != IREE_HAL_ELEMENT_TYPE_FLOAT_32) {
@@ -161,42 +177,36 @@
                             "element type should be f32");
   }
 
-  iree_status_t result;
+  // Classic row-major image layout.
+  iree_hal_encoding_type_t encoding_type =
+      IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR;
+
+  // Load pixel data from the file into a new host memory allocation (the only
+  // interface stb_image provides). A real application would want to use the
+  // generation callback to directly decode the image into the target mapped
+  // device buffer.
   uint8_t* pixel_data = NULL;
-  iree_hal_buffer_t* buffer = NULL;
-  iree_host_size_t buffer_length;
-  iree_host_size_t element_byte =
-      iree_hal_element_dense_byte_count(element_type);
-  iree_hal_buffer_mapping_t mapped_memory;
-  result = iree_tools_utils_load_pixel_data(
-      filename, shape, shape_rank, element_type, &pixel_data, &buffer_length);
-  if (iree_status_is_ok(result)) {
-    result = iree_hal_allocator_allocate_buffer(
-        allocator,
-        IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE,
-        IREE_HAL_BUFFER_USAGE_ALL, element_byte * buffer_length, &buffer);
-  }
-  if (iree_status_is_ok(result)) {
-    result = iree_hal_buffer_map_range(
-        buffer, IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, 0,
-        element_byte * buffer_length, &mapped_memory);
-  }
-  if (iree_status_is_ok(result)) {
-    // Need to normalize to the expected input range.
-    result = iree_tools_utils_pixel_rescaled_to_buffer(
-        pixel_data, buffer_length, input_range, range_length,
-        (float*)mapped_memory.contents.data);
-    iree_hal_buffer_unmap_range(&mapped_memory);
-  }
-  if (iree_status_is_ok(result)) {
-    iree_hal_encoding_type_t encoding_type =
-        IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR;
-    result = iree_hal_buffer_view_create(
-        buffer, shape, shape_rank, element_type, encoding_type,
-        iree_hal_allocator_host_allocator(allocator), out_buffer_view);
-  }
-  iree_hal_buffer_release(buffer);
+  iree_host_size_t buffer_length = 0;
+  IREE_RETURN_AND_END_ZONE_IF_ERROR(
+      z0, iree_tools_utils_load_pixel_data(filename, shape, shape_rank,
+                                           element_type, &pixel_data,
+                                           &buffer_length));
+
+  iree_tools_utils_buffer_view_load_params_t params = {
+      .pixel_data = pixel_data,
+      .pixel_data_length = buffer_length,
+      .input_range = input_range,
+      .input_range_length = input_range_length,
+  };
+  iree_status_t status = iree_hal_buffer_view_generate_buffer(
+      allocator, shape, shape_rank, element_type, encoding_type,
+      IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
+      IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER |
+          IREE_HAL_BUFFER_USAGE_MAPPING,
+      iree_tools_utils_buffer_view_load_image_rescaled, &params,
+      out_buffer_view);
+
   stbi_image_free(pixel_data);
   IREE_TRACE_ZONE_END(z0);
-  return result;
+  return status;
 }
diff --git a/iree/tools/utils/image_util.h b/iree/tools/utils/image_util.h
index 6c420d2..fa2e709 100644
--- a/iree/tools/utils/image_util.h
+++ b/iree/tools/utils/image_util.h
@@ -54,9 +54,10 @@
     const iree_string_view_t filename, const iree_hal_dim_t* shape,
     iree_host_size_t shape_rank, iree_hal_element_type_t element_type,
     iree_hal_allocator_t* allocator, const float* input_range,
-    iree_host_size_t range_length, iree_hal_buffer_view_t** out_buffer_view);
+    iree_host_size_t input_range_length,
+    iree_hal_buffer_view_t** out_buffer_view);
 
-// Normalize uint8_t |pixel data| of the size |buffer_length| to float buffer
+// Normalize uint8_t |pixel_data| of the size |buffer_length| to float buffer
 // |out_buffer| with the range |input_range|.
 //
 // float32_x = (uint8_x - 127.5) / 127.5 * input_scale + input_offset, where
@@ -65,8 +66,9 @@
 //
 // |out_buffer| needs to be allocated before the call.
 iree_status_t iree_tools_utils_pixel_rescaled_to_buffer(
-    const uint8_t* pixel_data, iree_host_size_t buffer_length,
-    const float* input_range, iree_host_size_t range_length, float* out_buffer);
+    const uint8_t* pixel_data, iree_host_size_t pixel_count,
+    const float* input_range, iree_host_size_t input_range_length,
+    float* out_buffer);
 
 #if __cplusplus
 }
diff --git a/iree/tools/utils/trace_replay.c b/iree/tools/utils/trace_replay.c
index c84c9d4..fc1a185 100644
--- a/iree/tools/utils/trace_replay.c
+++ b/iree/tools/utils/trace_replay.c
@@ -468,7 +468,7 @@
 static iree_status_t iree_trace_replay_parse_hal_buffer(
     iree_trace_replay_t* replay, yaml_document_t* document,
     yaml_node_t* contents_node, iree_hal_element_type_t element_type,
-    iree_hal_buffer_t* buffer) {
+    iree_hal_buffer_mapping_t* mapping) {
   if (!contents_node) {
     // Empty contents = zero fill.
     return iree_ok_status();
@@ -480,23 +480,16 @@
   iree_string_view_t value =
       iree_string_view_trim(iree_yaml_node_as_string(contents_node));
 
-  iree_hal_buffer_mapping_t mapping;
-  IREE_RETURN_IF_ERROR(
-      iree_hal_buffer_map_range(buffer, IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, 0,
-                                IREE_WHOLE_BUFFER, &mapping));
-  iree_status_t status = iree_ok_status();
   if (strcmp(contents_node->tag, "tag:yaml.org,2002:binary") == 0) {
-    status = iree_yaml_base64_decode(value, mapping.contents);
+    return iree_yaml_base64_decode(value, mapping->contents);
   } else if (strcmp(contents_node->tag, "tag:yaml.org,2002:str") == 0) {
-    status =
-        iree_hal_parse_buffer_elements(value, element_type, mapping.contents);
+    return iree_hal_parse_buffer_elements(value, element_type,
+                                          mapping->contents);
   } else {
-    status = iree_make_status(
-        IREE_STATUS_UNIMPLEMENTED, "(%zu): unimplemented buffer encoding '%s'",
-        contents_node->start_mark.line, contents_node->tag);
+    return iree_make_status(IREE_STATUS_UNIMPLEMENTED,
+                            "(%zu): unimplemented buffer encoding '%s'",
+                            contents_node->start_mark.line, contents_node->tag);
   }
-  iree_hal_buffer_unmap_range(&mapping);
-  return status;
 }
 
 // Writes an element of the given |element_type| with the given integral |value|
@@ -596,60 +589,76 @@
 }
 
 // Generates the destination |buffer| using the generator specified by
-// |contents_generator_node|.
+// |generator_node|.
 static iree_status_t iree_trace_replay_generate_hal_buffer(
     iree_trace_replay_t* replay, yaml_document_t* document,
-    yaml_node_t* contents_generator_node, iree_hal_element_type_t element_type,
-    iree_hal_buffer_t* buffer, iree_hal_dim_t* shape,
-    iree_host_size_t shape_size) {
-  if (!contents_generator_node) {
-    return iree_ok_status();
-  } else if (contents_generator_node->type != YAML_SCALAR_NODE) {
+    yaml_node_t* generator_node, iree_hal_element_type_t element_type,
+    const iree_hal_dim_t* shape, iree_host_size_t shape_rank,
+    iree_hal_buffer_mapping_t* mapping) {
+  if (generator_node->type != YAML_SCALAR_NODE) {
     return iree_make_status(
         IREE_STATUS_INVALID_ARGUMENT,
         "(%zu): expected scalar node for buffer contents_generator",
-        contents_generator_node->start_mark.line);
+        generator_node->start_mark.line);
   }
-
-  iree_hal_buffer_mapping_t mapping;
-  IREE_RETURN_IF_ERROR(
-      iree_hal_buffer_map_range(buffer, IREE_HAL_MEMORY_ACCESS_DISCARD_WRITE, 0,
-                                IREE_WHOLE_BUFFER, &mapping));
-  iree_status_t status = iree_ok_status();
-  if (strcmp(contents_generator_node->tag, "!tag:iree:identity_matrix") == 0) {
-    if (shape_size == 2) {
-      iree_hal_dim_t inner_size = shape[shape_size - 1];
-      iree_trace_replay_generate_identity_matrix(element_type, mapping.contents,
-                                                 inner_size);
+  if (strcmp(generator_node->tag, "!tag:iree:identity_matrix") == 0) {
+    if (shape_rank == 2) {
+      iree_hal_dim_t inner_size = shape[shape_rank - 1];
+      iree_trace_replay_generate_identity_matrix(element_type,
+                                                 mapping->contents, inner_size);
     } else {
-      status = iree_make_status(
+      return iree_make_status(
           IREE_STATUS_INVALID_ARGUMENT,
           "the identity_matrix generator is only for 2D shapes (matrices)");
     }
-  } else if (strcmp(contents_generator_node->tag,
+  } else if (strcmp(generator_node->tag,
                     "!tag:iree:fully_specified_pseudorandom") == 0) {
     // To enable pseudorandom tests that are both reproducible and invariant
     // under reordering and filtering testcases, the seed is explicitly
     // passed as argument in the contents_generator tag.
-    iree_string_view_t seed_str = iree_string_view_trim(
-        iree_yaml_node_as_string(contents_generator_node));
-    uint32_t seed;
+    iree_string_view_t seed_str =
+        iree_string_view_trim(iree_yaml_node_as_string(generator_node));
+    uint32_t seed = 0;
     if (iree_string_view_atoi_uint32(seed_str, &seed)) {
       iree_trace_replay_generate_fully_specified_pseudorandom_buffer(
-          element_type, mapping.contents, seed);
+          element_type, mapping->contents, seed);
     } else {
-      status = iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
-                                "could not parse the seed argument ('%s') of "
-                                "the fully_specified_pseudorandom tag",
-                                seed_str.data);
+      return iree_make_status(IREE_STATUS_INVALID_ARGUMENT,
+                              "could not parse the seed argument ('%s') of "
+                              "the fully_specified_pseudorandom tag",
+                              seed_str.data);
     }
   } else {
-    status = iree_make_status(
+    return iree_make_status(
         IREE_STATUS_UNIMPLEMENTED, "(%zu): unimplemented buffer generator '%s'",
-        contents_generator_node->start_mark.line, contents_generator_node->tag);
+        generator_node->start_mark.line, generator_node->tag);
   }
-  iree_hal_buffer_unmap_range(&mapping);
-  return status;
+  return iree_ok_status();
+}
+
+typedef struct iree_trace_replay_generation_params_t {
+  iree_trace_replay_t* replay;
+  yaml_document_t* document;
+  yaml_node_t* contents_node;
+  yaml_node_t* generator_node;
+  iree_hal_element_type_t element_type;
+  const iree_hal_dim_t* shape;
+  iree_host_size_t shape_rank;
+} iree_trace_replay_generation_params_t;
+
+static iree_status_t iree_trace_replay_generate_hal_buffer_callback(
+    iree_hal_buffer_mapping_t* mapping, void* user_data) {
+  iree_trace_replay_generation_params_t* params =
+      (iree_trace_replay_generation_params_t*)user_data;
+  if (params->contents_node) {
+    return iree_trace_replay_parse_hal_buffer(params->replay, params->document,
+                                              params->contents_node,
+                                              params->element_type, mapping);
+  } else {
+    return iree_trace_replay_generate_hal_buffer(
+        params->replay, params->document, params->generator_node,
+        params->element_type, params->shape, params->shape_rank, mapping);
+  }
 }
 
 // Parses a !hal.buffer_view and appends it to |target_list|.
@@ -694,51 +703,47 @@
       document, value_node, iree_make_cstring_view("contents"),
       &contents_node));
 
-  yaml_node_t* contents_generator_node = NULL;
+  yaml_node_t* generator_node = NULL;
   IREE_RETURN_IF_ERROR(iree_yaml_mapping_try_find(
       document, value_node, iree_make_cstring_view("contents_generator"),
-      &contents_generator_node));
+      &generator_node));
 
-  if (contents_node && contents_generator_node) {
+  iree_hal_buffer_view_t* buffer_view = NULL;
+  if (contents_node && generator_node) {
     return iree_make_status(
         IREE_STATUS_INVALID_ARGUMENT,
         "(%zu): cannot have both contents and contents_generator",
-        contents_generator_node->start_mark.line);
+        generator_node->start_mark.line);
+  } else if (contents_node || generator_node) {
+    iree_trace_replay_generation_params_t params = {
+        .replay = replay,
+        .document = document,
+        .contents_node = contents_node,
+        .generator_node = generator_node,
+        .element_type = element_type,
+        .shape = shape,
+        .shape_rank = shape_rank,
+    };
+    IREE_RETURN_IF_ERROR(iree_hal_buffer_view_generate_buffer(
+        iree_hal_device_allocator(replay->device), shape, shape_rank,
+        element_type, encoding_type,
+        IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER |
+            IREE_HAL_BUFFER_USAGE_MAPPING,
+        iree_trace_replay_generate_hal_buffer_callback, &params, &buffer_view));
+  } else {
+    IREE_RETURN_IF_ERROR(iree_hal_buffer_view_allocate_buffer(
+        iree_hal_device_allocator(replay->device), shape, shape_rank,
+        element_type, encoding_type,
+        IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
+        IREE_HAL_BUFFER_USAGE_DISPATCH | IREE_HAL_BUFFER_USAGE_TRANSFER |
+            IREE_HAL_BUFFER_USAGE_MAPPING,
+        iree_const_byte_span_empty(), &buffer_view));
   }
 
-  iree_device_size_t allocation_size = 0;
-  IREE_RETURN_IF_ERROR(iree_hal_buffer_compute_view_size(
-      shape, shape_rank, element_type, IREE_HAL_ENCODING_TYPE_DENSE_ROW_MAJOR,
-      &allocation_size));
-
-  iree_hal_buffer_t* buffer = NULL;
-  IREE_RETURN_IF_ERROR(iree_hal_allocator_allocate_buffer(
-      iree_hal_device_allocator(replay->device),
-      IREE_HAL_MEMORY_TYPE_DEVICE_LOCAL | IREE_HAL_MEMORY_TYPE_HOST_VISIBLE,
-      IREE_HAL_BUFFER_USAGE_ALL, allocation_size, &buffer));
-  iree_status_t status = iree_trace_replay_generate_hal_buffer(
-      replay, document, contents_generator_node, element_type, buffer, shape,
-      shape_rank);
-  if (!iree_status_is_ok(status)) {
-    iree_hal_buffer_release(buffer);
-    return status;
-  }
-  status = iree_trace_replay_parse_hal_buffer(replay, document, contents_node,
-                                              element_type, buffer);
-  if (!iree_status_is_ok(status)) {
-    iree_hal_buffer_release(buffer);
-    return status;
-  }
-
-  iree_hal_buffer_view_t* buffer_view = NULL;
-  status = iree_hal_buffer_view_create(buffer, shape, shape_rank, element_type,
-                                       encoding_type, replay->host_allocator,
-                                       &buffer_view);
-  iree_hal_buffer_release(buffer);
-  IREE_RETURN_IF_ERROR(status);
-
   iree_vm_ref_t buffer_view_ref = iree_hal_buffer_view_move_ref(buffer_view);
-  status = iree_vm_list_push_ref_move(target_list, &buffer_view_ref);
+  iree_status_t status =
+      iree_vm_list_push_ref_move(target_list, &buffer_view_ref);
   iree_vm_ref_release(&buffer_view_ref);
   return status;
 }
@@ -819,7 +824,8 @@
   return iree_ok_status();
 }
 
-static iree_status_t iree_trace_replay_print_item(iree_vm_variant_t* value);
+static iree_status_t iree_trace_replay_print_item(
+    iree_vm_variant_t* value, iree_allocator_t host_allocator);
 
 static iree_status_t iree_trace_replay_print_scalar(iree_vm_variant_t* value) {
   switch (value->type.value_type) {
@@ -848,35 +854,34 @@
   return iree_ok_status();
 }
 
-static iree_status_t iree_trace_replay_print_vm_list(iree_vm_list_t* list) {
+static iree_status_t iree_trace_replay_print_vm_list(
+    iree_vm_list_t* list, iree_allocator_t host_allocator) {
   for (iree_host_size_t i = 0; i < iree_vm_list_size(list); ++i) {
     iree_vm_variant_t variant = iree_vm_variant_empty();
     IREE_RETURN_IF_ERROR(iree_vm_list_get_variant(list, i, &variant),
                          "variant %zu not present", i);
-    IREE_RETURN_IF_ERROR(iree_trace_replay_print_item(&variant));
+    IREE_RETURN_IF_ERROR(
+        iree_trace_replay_print_item(&variant, host_allocator));
     fprintf(stdout, "\n");
   }
   return iree_ok_status();
 }
 
-static iree_status_t iree_trace_replay_print_hal_buffer_view(
-    iree_hal_buffer_view_t* buffer_view) {
-  return iree_hal_buffer_view_fprint(stdout, buffer_view,
-                                     /*max_element_count=*/1024);
-}
-
-static iree_status_t iree_trace_replay_print_item(iree_vm_variant_t* value) {
+static iree_status_t iree_trace_replay_print_item(
+    iree_vm_variant_t* value, iree_allocator_t host_allocator) {
   if (iree_vm_variant_is_value(*value)) {
     IREE_RETURN_IF_ERROR(iree_trace_replay_print_scalar(value));
   } else if (iree_vm_variant_is_ref(*value)) {
     if (iree_hal_buffer_view_isa(value->ref)) {
       iree_hal_buffer_view_t* buffer_view =
           iree_hal_buffer_view_deref(value->ref);
-      IREE_RETURN_IF_ERROR(
-          iree_trace_replay_print_hal_buffer_view(buffer_view));
+      IREE_RETURN_IF_ERROR(iree_hal_buffer_view_fprint(
+          stdout, buffer_view,
+          /*max_element_count=*/1024, host_allocator));
     } else if (iree_vm_list_isa(value->ref)) {
       iree_vm_list_t* list = iree_vm_list_deref(value->ref);
-      IREE_RETURN_IF_ERROR(iree_trace_replay_print_vm_list(list));
+      IREE_RETURN_IF_ERROR(
+          iree_trace_replay_print_vm_list(list, host_allocator));
     } else {
       // TODO(benvanik): a way for ref types to describe themselves.
       fprintf(stdout, "(no printer)");
@@ -985,7 +990,8 @@
 
   // Print the outputs.
   if (iree_status_is_ok(status)) {
-    status = iree_trace_replay_print_vm_list(output_list);
+    status =
+        iree_trace_replay_print_vm_list(output_list, replay->host_allocator);
   }
   iree_vm_list_release(output_list);