| // Copyright 2022 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 <stdint.h> | |
| #include <stdio.h> | |
| #include <string.h> | |
| // Must be first. | |
| #include "experimental/webgpu/platform/webgpu.h" | |
| // NOTE: include order matters. | |
| #include "experimental/webgpu/buffer.h" | |
| #include "experimental/webgpu/webgpu_device.h" | |
| #include "iree/base/api.h" | |
| #include "iree/base/internal/wait_handle.h" | |
| #include "iree/base/loop_emscripten.h" | |
| #include "iree/hal/api.h" | |
| #include "iree/modules/hal/module.h" | |
| #include "iree/runtime/api.h" | |
| #include "iree/vm/bytecode/module.h" | |
| //===----------------------------------------------------------------------===// | |
| // Public API | |
| //===----------------------------------------------------------------------===// | |
| // Opaque state for the sample, shared between multiple loaded programs. | |
| typedef struct iree_sample_state_t iree_sample_state_t; | |
| // Initializes the sample and returns its state. | |
| iree_sample_state_t* setup_sample(); | |
| // Shuts down the sample and frees its state. | |
| // Requires that all programs first be unloaded with |unload_program|. | |
| void cleanup_sample(iree_sample_state_t* sample_state); | |
| // Opaque state for an individual loaded program. | |
| typedef struct iree_program_state_t iree_program_state_t; | |
| // Loads a program into the sample from the provided data. | |
| // Note: this takes ownership of |vmfb_data|. | |
| iree_program_state_t* load_program(iree_sample_state_t* sample_state, | |
| uint8_t* vmfb_data, size_t length); | |
| // Inspects metadata about a loaded program, printing to stdout. | |
| void inspect_program(iree_program_state_t* program_state); | |
| // Unloads a program and frees its state. | |
| void unload_program(iree_program_state_t* program_state); | |
| // Callback function passed in from JS. | |
| // On call failure, is called with the empty string. | |
| // On call success, is called with a JSON object: | |
| // { | |
| // "invoke_time_ms": [number], | |
| // "readback_time_ms": [number], | |
| // "outputs": [semicolon delimited list of formatted outputs] | |
| // } | |
| // TODO(scotttodd): on error, call with some other JSON / iree_status_fprint | |
| // TODO(scotttodd): on success, return structured data instead of formatted text | |
| typedef void(IREE_API_PTR* iree_call_function_callback_fn_t)(char* output); | |
| // Calls a function asynchronously. | |
| // | |
| // Returns true if call setup was successful, or false if setup failed. | |
| // | |
| // * |function_name| is the function name, like 'abs' (not fully qualified). | |
| // * |inputs| is a semicolon delimited list of VM scalars and buffers, as | |
| // described in iree/tooling/vm_util and used in IREE's CLI tools. | |
| // For example, the CLI `--function_input=f32=1 --function_input=f32=2` | |
| // should be passed here as `f32=1;f32=2`. | |
| // * |completion_callback_fn| will be called after the call completes. | |
| const bool call_function( | |
| iree_program_state_t* program_state, const char* function_name, | |
| const char* inputs, | |
| iree_call_function_callback_fn_t completion_callback_fn); | |
| //===----------------------------------------------------------------------===// | |
| // Implementation | |
| //===----------------------------------------------------------------------===// | |
| typedef struct iree_sample_state_t { | |
| iree_runtime_instance_t* instance; | |
| iree_hal_device_t* device; | |
| iree_loop_emscripten_t* loop; | |
| } iree_sample_state_t; | |
| typedef struct iree_program_state_t { | |
| iree_sample_state_t* sample_state; | |
| iree_runtime_session_t* session; | |
| iree_vm_module_t* module; | |
| } iree_program_state_t; | |
| // Function calls run asynchronously, both for the computations themselves | |
| // and for mapping the outputs back from GPU/device memory to CPU/host memory. | |
| // | |
| // Here is the high level flow: | |
| // | |
| // call_function | |
| // parse_inputs_into_call | |
| // iree_vm_async_invoke | |
| // [async] invoke_callback | |
| // process_call_outputs | |
| // <batch transfer from output buffers to mappable device buffers> | |
| // map_call_output[0...n] | |
| // wgpuBufferMapAsync | |
| // [async] buffer_map_async_callback | |
| // <set event> | |
| // [async] iree_loop_wait_all | |
| // map_all_callback | |
| // print_outputs_from_call | |
| // issue top level callback | |
| // | |
| // State tracking uses the iree_call_function_state_t and iree_output_state_t | |
| // structs defined below, keeping both live until the final map_all_callback. | |
| // If any of the asynchronous calls fail, all other calls must be treated as | |
| // failed and all state must be freed. | |
| // State for a single output within an asynchronous function call. | |
| // | |
| // If the output is a buffer, optional fields are used to track asynchronous | |
| // mapping from device memory to host memory. | |
| typedef struct iree_output_state_t { | |
| // Event that will be signaled when the output is ready to access on the host. | |
| // * For non-buffer outputs, this will start signaled. | |
| // * For buffer outputs, this will be signaled when _either_ | |
| // |mapped_host_buffer| points to the output data _or_ mapping failed. | |
| iree_event_t ready_event; | |
| // The original buffer_view from the call output, if the output is a buffer. | |
| // Not guaranteed to reference mappable memory. | |
| iree_hal_buffer_view_t* buffer_view; | |
| // A mappable device buffer (backed by a WGPUBuffer). | |
| // The original buffer will be copied into this buffer by a transfer command. | |
| iree_hal_buffer_t* mappable_device_buffer; | |
| // A mapped host buffer (backed by a iree_hal_heap_buffer_t). | |
| // After asynchronous mapping of |mappable_device_buffer|, the output data | |
| // will be wrapped into this buffer. | |
| // Note: to recover the original shape, use iree_hal_buffer_view_create_like. | |
| iree_hal_buffer_t* mapped_host_buffer; | |
| } iree_output_state_t; | |
| // Aggregate state for an asynchronous function call. | |
| typedef struct iree_call_function_state_t { | |
| iree_runtime_call_t call; | |
| iree_loop_emscripten_t* loop; | |
| iree_call_function_callback_fn_t callback_fn; | |
| // Opaque state used by iree_vm_async_invoke. | |
| iree_vm_async_invoke_state_t* invoke_state; | |
| // Timing/statistics metadata (~millisecond precision on the web). | |
| // https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#reduced_time_precision | |
| iree_time_t invoke_start_time; | |
| iree_time_t invoke_end_time; | |
| iree_time_t readback_start_time; | |
| iree_time_t readback_end_time; | |
| // Sticky status for the first async error. | |
| // If this is not ok, treat all output buffer mappings as having errored and | |
| // issue the callback with a failure message. | |
| iree_status_t async_status; | |
| // Output processing state. | |
| iree_host_size_t outputs_size; | |
| iree_output_state_t* output_states; | |
| } iree_call_function_state_t; | |
| static void iree_call_function_state_destroy( | |
| iree_call_function_state_t* call_state) { | |
| // Output processing state. | |
| for (iree_host_size_t i = 0; i < call_state->outputs_size; ++i) { | |
| if (call_state->output_states[i].buffer_view) { | |
| iree_hal_buffer_release(call_state->output_states[i].mapped_host_buffer); | |
| iree_hal_buffer_release( | |
| call_state->output_states[i].mappable_device_buffer); | |
| iree_hal_buffer_view_release(call_state->output_states[i].buffer_view); | |
| } | |
| iree_event_deinitialize(&call_state->output_states[i].ready_event); | |
| } | |
| iree_allocator_free(iree_allocator_system(), call_state->output_states); | |
| iree_status_free(call_state->async_status); | |
| // Invoke state. | |
| iree_allocator_free(iree_allocator_system(), call_state->invoke_state); | |
| iree_runtime_call_deinitialize(&call_state->call); | |
| iree_allocator_free(iree_allocator_system(), call_state); | |
| } | |
| extern iree_status_t create_device(iree_allocator_t host_allocator, | |
| iree_hal_device_t** out_device); | |
| iree_sample_state_t* setup_sample() { | |
| iree_sample_state_t* sample_state = NULL; | |
| iree_status_t status = iree_allocator_malloc( | |
| iree_allocator_system(), sizeof(*sample_state), (void**)&sample_state); | |
| iree_runtime_instance_options_t instance_options; | |
| iree_runtime_instance_options_initialize(&instance_options); | |
| // Note: no call to iree_runtime_instance_options_use_all_available_drivers(). | |
| if (iree_status_is_ok(status)) { | |
| status = iree_runtime_instance_create( | |
| &instance_options, iree_allocator_system(), &sample_state->instance); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| status = create_device(iree_allocator_system(), &sample_state->device); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| status = iree_loop_emscripten_allocate(iree_allocator_system(), | |
| &sample_state->loop); | |
| } | |
| if (!iree_status_is_ok(status)) { | |
| iree_status_fprint(stderr, status); | |
| iree_status_free(status); | |
| cleanup_sample(sample_state); | |
| return NULL; | |
| } | |
| return sample_state; | |
| } | |
| void cleanup_sample(iree_sample_state_t* sample_state) { | |
| iree_loop_emscripten_free(sample_state->loop); | |
| iree_hal_device_release(sample_state->device); | |
| iree_runtime_instance_release(sample_state->instance); | |
| free(sample_state); | |
| } | |
| iree_program_state_t* load_program(iree_sample_state_t* sample_state, | |
| uint8_t* vmfb_data, size_t length) { | |
| iree_program_state_t* program_state = NULL; | |
| iree_status_t status = iree_allocator_malloc( | |
| iree_allocator_system(), sizeof(*program_state), (void**)&program_state); | |
| program_state->sample_state = sample_state; | |
| iree_runtime_session_options_t session_options; | |
| iree_runtime_session_options_initialize(&session_options); | |
| if (iree_status_is_ok(status)) { | |
| status = iree_runtime_session_create_with_device( | |
| sample_state->instance, &session_options, sample_state->device, | |
| iree_runtime_instance_host_allocator(sample_state->instance), | |
| &program_state->session); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| // Take ownership of the FlatBuffer data so JavaScript doesn't need to | |
| // explicitly call `Module._free()`. | |
| status = iree_vm_bytecode_module_create( | |
| iree_runtime_instance_vm_instance(sample_state->instance), | |
| iree_make_const_byte_span(vmfb_data, length), | |
| /*flatbuffer_allocator=*/iree_allocator_system(), | |
| iree_allocator_system(), &program_state->module); | |
| } else { | |
| // Must clean up the FlatBuffer data directly. | |
| iree_allocator_free(iree_allocator_system(), (void*)vmfb_data); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| status = iree_runtime_session_append_module(program_state->session, | |
| program_state->module); | |
| } | |
| if (!iree_status_is_ok(status)) { | |
| iree_status_fprint(stderr, status); | |
| iree_status_free(status); | |
| unload_program(program_state); | |
| return NULL; | |
| } | |
| return program_state; | |
| } | |
| void inspect_program(iree_program_state_t* program_state) { | |
| fprintf(stdout, "=== program properties ===\n"); | |
| iree_vm_module_t* module = program_state->module; | |
| iree_string_view_t module_name = iree_vm_module_name(module); | |
| fprintf(stdout, " module name: '%.*s'\n", (int)module_name.size, | |
| module_name.data); | |
| iree_vm_module_signature_t module_signature = | |
| iree_vm_module_signature(module); | |
| fprintf(stdout, " module signature:\n"); | |
| fprintf(stdout, " %" PRIhsz " imported functions\n", | |
| module_signature.import_function_count); | |
| fprintf(stdout, " %" PRIhsz " exported functions\n", | |
| module_signature.export_function_count); | |
| fprintf(stdout, " %" PRIhsz " internal functions\n", | |
| module_signature.internal_function_count); | |
| fprintf(stdout, " exported functions:\n"); | |
| for (iree_host_size_t i = 0; i < module_signature.export_function_count; | |
| ++i) { | |
| iree_vm_function_t function; | |
| iree_status_t status = iree_vm_module_lookup_function_by_ordinal( | |
| module, IREE_VM_FUNCTION_LINKAGE_EXPORT, i, &function); | |
| if (!iree_status_is_ok(status)) { | |
| iree_status_fprint(stderr, status); | |
| iree_status_free(status); | |
| continue; | |
| } | |
| iree_string_view_t function_name = iree_vm_function_name(&function); | |
| iree_vm_function_signature_t function_signature = | |
| iree_vm_function_signature(&function); | |
| iree_string_view_t calling_convention = | |
| function_signature.calling_convention; | |
| fprintf(stdout, " function name: '%.*s', calling convention: %.*s'\n", | |
| (int)function_name.size, function_name.data, | |
| (int)calling_convention.size, calling_convention.data); | |
| } | |
| } | |
| void unload_program(iree_program_state_t* program_state) { | |
| iree_vm_module_release(program_state->module); | |
| iree_runtime_session_release(program_state->session); | |
| free(program_state); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Input parsing | |
| //===----------------------------------------------------------------------===// | |
| static iree_status_t parse_input_into_call( | |
| iree_runtime_call_t* call, iree_hal_device_t* device, | |
| iree_hal_allocator_t* device_allocator, iree_string_view_t input) { | |
| bool has_equal = | |
| iree_string_view_find_char(input, '=', 0) != IREE_STRING_VIEW_NPOS; | |
| bool has_x = | |
| iree_string_view_find_char(input, 'x', 0) != IREE_STRING_VIEW_NPOS; | |
| if (has_equal || has_x) { | |
| // Buffer view (either just a shape or a shape=value) or buffer. | |
| bool is_storage_reference = | |
| iree_string_view_consume_prefix(&input, iree_make_cstring_view("&")); | |
| iree_hal_buffer_view_t* buffer_view = NULL; | |
| IREE_RETURN_IF_ERROR(iree_hal_buffer_view_parse( | |
| input, device, device_allocator, &buffer_view), | |
| "parsing value '%.*s'", (int)input.size, input.data); | |
| if (is_storage_reference) { | |
| // Storage buffer reference; just take the storage for the buffer view - | |
| // it'll still have whatever contents were specified (or 0) but we'll | |
| // discard the metadata. | |
| iree_vm_ref_t buffer_ref = | |
| iree_hal_buffer_retain_ref(iree_hal_buffer_view_buffer(buffer_view)); | |
| iree_hal_buffer_view_release(buffer_view); | |
| return iree_vm_list_push_ref_move(call->inputs, &buffer_ref); | |
| } else { | |
| iree_vm_ref_t buffer_view_ref = | |
| iree_hal_buffer_view_move_ref(buffer_view); | |
| return iree_vm_list_push_ref_move(call->inputs, &buffer_view_ref); | |
| } | |
| } else { | |
| // Scalar. | |
| bool has_dot = | |
| iree_string_view_find_char(input, '.', 0) != IREE_STRING_VIEW_NPOS; | |
| iree_vm_value_t val; | |
| if (has_dot) { | |
| // Float. | |
| val = iree_vm_value_make_f32(0.0f); | |
| if (!iree_string_view_atof(input, &val.f32)) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "parsing value '%.*s' as f32", (int)input.size, | |
| input.data); | |
| } | |
| } else { | |
| // Integer. | |
| val = iree_vm_value_make_i32(0); | |
| if (!iree_string_view_atoi_int32(input, &val.i32)) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "parsing value '%.*s' as i32", (int)input.size, | |
| input.data); | |
| } | |
| } | |
| return iree_vm_list_push_value(call->inputs, &val); | |
| } | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "Unhandled function input (unreachable?)"); | |
| } | |
| static iree_status_t parse_inputs_into_call( | |
| iree_runtime_call_t* call, iree_hal_device_t* device, | |
| iree_hal_allocator_t* device_allocator, iree_string_view_t inputs) { | |
| if (inputs.size == 0) return iree_ok_status(); | |
| // Inputs are provided in a semicolon-delimited list. | |
| // Split inputs from the list until no semicolons are left. | |
| iree_string_view_t remaining_inputs = inputs; | |
| intptr_t split_index = 0; | |
| do { | |
| iree_string_view_t next_input; | |
| split_index = iree_string_view_split(remaining_inputs, ';', &next_input, | |
| &remaining_inputs); | |
| IREE_RETURN_IF_ERROR( | |
| parse_input_into_call(call, device, device_allocator, next_input)); | |
| } while (split_index != -1); | |
| return iree_ok_status(); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Output readback and formatting | |
| //===----------------------------------------------------------------------===// | |
| typedef struct iree_buffer_map_userdata_t { | |
| iree_call_function_state_t* call_state; | |
| iree_host_size_t buffer_index; | |
| } iree_buffer_map_userdata_t; | |
| static void iree_webgpu_mapped_buffer_release(void* user_data, | |
| iree_hal_buffer_t* buffer) { | |
| WGPUBuffer buffer_handle = (WGPUBuffer)user_data; | |
| wgpuBufferUnmap(buffer_handle); | |
| } | |
| static void buffer_map_async_callback(WGPUBufferMapAsyncStatus map_status, | |
| void* userdata_ptr) { | |
| iree_status_t status = iree_ok_status(); | |
| iree_buffer_map_userdata_t* userdata = | |
| (iree_buffer_map_userdata_t*)userdata_ptr; | |
| iree_host_size_t buffer_index = userdata->buffer_index; | |
| iree_hal_buffer_view_t* output_buffer_view = | |
| userdata->call_state->output_states[buffer_index].buffer_view; | |
| iree_hal_buffer_t* mappable_device_buffer = | |
| userdata->call_state->output_states[buffer_index].mappable_device_buffer; | |
| iree_hal_buffer_t** mapped_host_buffer_ptr = | |
| &userdata->call_state->output_states[buffer_index].mapped_host_buffer; | |
| switch (map_status) { | |
| case WGPUBufferMapAsyncStatus_Success: | |
| break; | |
| case WGPUBufferMapAsyncStatus_DeviceLost: | |
| fprintf(stderr, " buffer_map_async_callback status: DeviceLost\n"); | |
| break; | |
| default: | |
| fprintf(stderr, " buffer_map_async_callback status: Error %d\n", | |
| map_status); | |
| break; | |
| } | |
| if (map_status != WGPUBufferMapAsyncStatus_Success) { | |
| status = iree_make_status(IREE_STATUS_UNKNOWN, | |
| "wgpuBufferMapAsync failed for buffer %" PRIhsz, | |
| buffer_index); | |
| } | |
| iree_device_size_t data_offset = 0; | |
| iree_device_size_t data_length = | |
| iree_hal_buffer_view_byte_length(output_buffer_view); | |
| WGPUBuffer buffer_handle = | |
| iree_hal_webgpu_buffer_handle(mappable_device_buffer); | |
| // For this sample we want to print arbitrary buffers, which is easiest | |
| // using the |iree_hal_buffer_view_format| function. Internally, that | |
| // function requires synchronous buffer mapping, so we'll first wrap the | |
| // already (async) mapped GPU memory into a heap buffer. In a less general | |
| // application (or one not requiring pretty logging like this), we could | |
| // skip a few buffer copies and other data transformations here. | |
| const void* data_ptr = NULL; | |
| if (iree_status_is_ok(status)) { | |
| data_ptr = | |
| wgpuBufferGetConstMappedRange(buffer_handle, data_offset, data_length); | |
| if (data_ptr == NULL) { | |
| status = iree_make_status( | |
| IREE_STATUS_UNKNOWN, | |
| "wgpuBufferGetConstMappedRange failed for buffer %" PRIhsz, | |
| buffer_index); | |
| } | |
| } | |
| if (iree_status_is_ok(status)) { | |
| // The buffer we get from WebGPU may not be aligned to 64. | |
| iree_hal_memory_access_t memory_access = | |
| IREE_HAL_MEMORY_ACCESS_READ | IREE_HAL_MEMORY_ACCESS_UNALIGNED; | |
| status = iree_hal_heap_buffer_wrap( | |
| mappable_device_buffer->device_allocator, | |
| IREE_HAL_MEMORY_TYPE_HOST_LOCAL, memory_access, | |
| IREE_HAL_BUFFER_USAGE_MAPPING, data_length, | |
| iree_make_byte_span((void*)data_ptr, data_length), | |
| (iree_hal_buffer_release_callback_t){ | |
| .fn = iree_webgpu_mapped_buffer_release, | |
| .user_data = buffer_handle, | |
| }, | |
| mapped_host_buffer_ptr); | |
| } | |
| if (!iree_status_is_ok(status)) { | |
| // Set the sticky async error if not already set. | |
| userdata->call_state->async_status = | |
| iree_status_join(userdata->call_state->async_status, status); | |
| } | |
| iree_event_set( | |
| &userdata->call_state->output_states[buffer_index].ready_event); | |
| iree_allocator_free(iree_allocator_system(), userdata); | |
| } | |
| static iree_status_t print_outputs_from_call( | |
| iree_call_function_state_t* call_state, | |
| iree_string_builder_t* outputs_builder) { | |
| iree_vm_list_t* variants_list = iree_runtime_call_outputs(&call_state->call); | |
| for (iree_host_size_t i = 0; i < iree_vm_list_size(variants_list); ++i) { | |
| iree_vm_variant_t variant = iree_vm_variant_empty(); | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_get_variant_assign(variants_list, i, &variant), | |
| "variant %" PRIhsz " not present", i); | |
| if (iree_vm_variant_is_value(variant)) { | |
| switch (iree_vm_type_def_as_value(variant.type)) { | |
| case IREE_VM_VALUE_TYPE_I8: { | |
| IREE_RETURN_IF_ERROR(iree_string_builder_append_format( | |
| outputs_builder, "i8=%" PRIi8, variant.i8)); | |
| break; | |
| } | |
| case IREE_VM_VALUE_TYPE_I16: { | |
| IREE_RETURN_IF_ERROR(iree_string_builder_append_format( | |
| outputs_builder, "i16=%" PRIi16, variant.i16)); | |
| break; | |
| } | |
| case IREE_VM_VALUE_TYPE_I32: { | |
| IREE_RETURN_IF_ERROR(iree_string_builder_append_format( | |
| outputs_builder, "i32=%" PRIi32, variant.i32)); | |
| break; | |
| } | |
| case IREE_VM_VALUE_TYPE_I64: { | |
| IREE_RETURN_IF_ERROR(iree_string_builder_append_format( | |
| outputs_builder, "i64=%" PRIi64, variant.i64)); | |
| break; | |
| } | |
| case IREE_VM_VALUE_TYPE_F32: { | |
| IREE_RETURN_IF_ERROR(iree_string_builder_append_format( | |
| outputs_builder, "f32=%f", variant.f32)); | |
| break; | |
| } | |
| case IREE_VM_VALUE_TYPE_F64: { | |
| IREE_RETURN_IF_ERROR(iree_string_builder_append_format( | |
| outputs_builder, "f64=%lf", variant.f64)); | |
| break; | |
| } | |
| default: { | |
| IREE_RETURN_IF_ERROR( | |
| iree_string_builder_append_cstring(outputs_builder, "?")); | |
| break; | |
| } | |
| } | |
| } else if (iree_vm_variant_is_ref(variant)) { | |
| // Interpret the mapped buffer in the same format as the output view. | |
| iree_hal_buffer_view_t* heap_buffer_view = NULL; | |
| IREE_RETURN_IF_ERROR(iree_hal_buffer_view_create_like( | |
| call_state->output_states[i].mapped_host_buffer, | |
| call_state->output_states[i].buffer_view, iree_allocator_system(), | |
| &heap_buffer_view)); | |
| IREE_RETURN_IF_ERROR(iree_hal_buffer_view_append_to_builder( | |
| heap_buffer_view, SIZE_MAX, outputs_builder)); | |
| iree_hal_buffer_view_release(heap_buffer_view); | |
| } else { | |
| IREE_RETURN_IF_ERROR( | |
| iree_string_builder_append_cstring(outputs_builder, "(null)")); | |
| } | |
| if (i < iree_vm_list_size(variants_list) - 1) { | |
| IREE_RETURN_IF_ERROR( | |
| iree_string_builder_append_cstring(outputs_builder, ";")); | |
| } | |
| } | |
| iree_vm_list_resize(variants_list, 0); | |
| return iree_ok_status(); | |
| } | |
| static iree_status_t map_all_callback(void* user_data, iree_loop_t loop, | |
| iree_status_t status) { | |
| iree_call_function_state_t* call_state = | |
| (iree_call_function_state_t*)user_data; | |
| call_state->readback_end_time = iree_time_now(); | |
| status = iree_status_join(call_state->async_status, status); | |
| iree_string_builder_t output_string_builder; | |
| iree_string_builder_initialize(iree_allocator_system(), | |
| &output_string_builder); | |
| // Output a JSON object as a string: | |
| // { | |
| // "invoke_time_ms": [number], | |
| // "readback_time_ms": [number], | |
| // "outputs": [semicolon delimited list of formatted outputs] | |
| // } | |
| if (iree_status_is_ok(status)) { | |
| iree_time_t invoke_time_ms = | |
| (call_state->invoke_end_time - call_state->invoke_start_time) / 1000000; | |
| iree_time_t readback_time_ms = | |
| (call_state->readback_end_time - call_state->readback_start_time) / | |
| 1000000; | |
| status = iree_string_builder_append_format( | |
| &output_string_builder, | |
| "{ \"invoke_time_ms\": %" PRId64 ", \"readback_time_ms\": %" PRId64 | |
| ", \"outputs\": \"", | |
| invoke_time_ms, readback_time_ms); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| status = print_outputs_from_call(call_state, &output_string_builder); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| status = iree_string_builder_append_cstring(&output_string_builder, "\"}"); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| // Note: this leaks the buffer. It's up to the caller to free it after use. | |
| char* outputs_string = | |
| strdup(iree_string_builder_buffer(&output_string_builder)); | |
| iree_string_builder_deinitialize(&output_string_builder); | |
| call_state->callback_fn(outputs_string); | |
| } else { | |
| fprintf(stderr, "map_all_callback error:\n"); | |
| // TODO(scotttodd): return a JSON object with the error message | |
| // * free |status| and return a status to the loop with no storage | |
| // * the returned string is always freed, so then we'd have no leaks | |
| iree_status_fprint(stderr, status); | |
| // Note: loop_emscripten.js must free 'status'! | |
| call_state->callback_fn(NULL); | |
| } | |
| iree_string_builder_deinitialize(&output_string_builder); | |
| iree_call_function_state_destroy(call_state); | |
| return status; | |
| } | |
| static iree_status_t allocate_mappable_device_buffer( | |
| iree_hal_device_t* device, iree_hal_buffer_view_t* buffer_view, | |
| iree_hal_buffer_t** out_buffer) { | |
| *out_buffer = NULL; | |
| iree_device_size_t data_length = | |
| iree_hal_buffer_view_byte_length(buffer_view); | |
| // Note: iree_hal_webgpu_simple_allocator_allocate_buffer only supports | |
| // CopySrc today, so we'll create the buffer directly with | |
| // wgpuDeviceCreateBuffer and then wrap it using iree_hal_webgpu_buffer_wrap. | |
| WGPUBufferDescriptor descriptor = { | |
| .nextInChain = NULL, | |
| .label = "IREE_mapping", | |
| .usage = WGPUBufferUsage_MapRead | WGPUBufferUsage_CopyDst, | |
| .size = data_length, | |
| .mappedAtCreation = false, | |
| }; | |
| WGPUBuffer device_buffer_handle = NULL; | |
| // Note: wgpuBufferDestroy is called after iree_hal_webgpu_buffer_wrap -> | |
| // iree_hal_buffer_release -> iree_hal_webgpu_buffer_destroy | |
| device_buffer_handle = wgpuDeviceCreateBuffer( | |
| iree_hal_webgpu_device_handle(device), &descriptor); | |
| if (!device_buffer_handle) { | |
| return iree_make_status(IREE_STATUS_RESOURCE_EXHAUSTED, | |
| "unable to allocate buffer of size %" PRIdsz, | |
| data_length); | |
| } | |
| const iree_hal_buffer_params_t target_params = { | |
| .usage = IREE_HAL_BUFFER_USAGE_TRANSFER | IREE_HAL_BUFFER_USAGE_MAPPING, | |
| .type = | |
| IREE_HAL_MEMORY_TYPE_HOST_LOCAL | IREE_HAL_MEMORY_TYPE_DEVICE_VISIBLE, | |
| .access = IREE_HAL_MEMORY_ACCESS_ALL, | |
| }; | |
| return iree_hal_webgpu_buffer_wrap( | |
| device, iree_hal_device_allocator(device), target_params.type, | |
| target_params.access, target_params.usage, data_length, | |
| /*byte_offset=*/0, | |
| /*byte_length=*/data_length, device_buffer_handle, | |
| iree_allocator_system(), out_buffer); | |
| } | |
| // Processes outputs from a completed function invocation. | |
| // Some output data types may require asynchronous mapping. | |
| static iree_status_t process_call_outputs( | |
| iree_call_function_state_t* call_state) { | |
| call_state->readback_start_time = iree_time_now(); | |
| iree_vm_list_t* outputs_list = iree_runtime_call_outputs(&call_state->call); | |
| iree_host_size_t outputs_size = iree_vm_list_size(outputs_list); | |
| iree_hal_device_t* device = | |
| iree_runtime_session_device(call_state->call.session); | |
| IREE_RETURN_IF_ERROR(iree_allocator_malloc( | |
| iree_allocator_system(), sizeof(iree_output_state_t) * outputs_size, | |
| (void**)&call_state->output_states)); | |
| call_state->outputs_size = outputs_size; | |
| // TODO(scotttodd): allocate on the heap and track? | |
| // * iree_loop_wait_all claims that wait_sources must live until the | |
| // callback is issued | |
| // * loop_emscripten uses what it needs (Promise handles/objects) | |
| // immediately, before objects go out of scope | |
| iree_wait_source_t* wait_sources = (iree_wait_source_t*)iree_alloca( | |
| sizeof(iree_wait_source_t) * outputs_size); | |
| // Loop through the outputs once to find buffers that need readback. | |
| iree_host_size_t buffer_count = 0; | |
| for (iree_host_size_t i = 0; i < outputs_size; ++i) { | |
| iree_vm_variant_t variant = iree_vm_variant_empty(); | |
| IREE_RETURN_IF_ERROR( | |
| iree_vm_list_get_variant_assign(outputs_list, i, &variant), | |
| "variant %" PRIhsz " not present", i); | |
| if (iree_vm_variant_is_ref(variant)) { | |
| if (!iree_hal_buffer_view_isa(variant.ref)) { | |
| return iree_make_status(IREE_STATUS_UNIMPLEMENTED, | |
| "only buffer_view variants are supported"); | |
| } | |
| // Output is a buffer_view ref, add to mapping batch (async). | |
| iree_event_initialize(false, &call_state->output_states[i].ready_event); | |
| buffer_count++; | |
| iree_hal_buffer_view_t* buffer_view = | |
| iree_hal_buffer_view_deref(variant.ref); | |
| call_state->output_states[i].buffer_view = buffer_view; | |
| iree_hal_buffer_view_retain(buffer_view); | |
| // TODO(scotttodd): signal event if failed | |
| IREE_RETURN_IF_ERROR(allocate_mappable_device_buffer( | |
| device, buffer_view, | |
| &call_state->output_states[i].mappable_device_buffer)); | |
| } else { | |
| // Not a buffer, data is available immediately - start signaled. | |
| iree_event_initialize(true, &call_state->output_states[i].ready_event); | |
| } | |
| wait_sources[i] = | |
| iree_event_await(&call_state->output_states[i].ready_event); | |
| } | |
| // Loop through the outputs again to build a batched transfer command buffer. | |
| iree_hal_transfer_command_t* transfer_commands = | |
| (iree_hal_transfer_command_t*)iree_alloca( | |
| sizeof(iree_hal_transfer_command_t) * buffer_count); | |
| for (iree_host_size_t i = 0, buffer_index = 0; i < outputs_size; ++i) { | |
| iree_hal_buffer_view_t* buffer_view = | |
| call_state->output_states[i].buffer_view; | |
| if (!buffer_view) continue; | |
| iree_hal_buffer_t* source_buffer = iree_hal_buffer_view_buffer(buffer_view); | |
| iree_hal_buffer_t* source_buffer_allocated = | |
| iree_hal_buffer_allocated_buffer(source_buffer); | |
| iree_device_size_t source_offset = | |
| iree_hal_buffer_byte_offset(source_buffer); | |
| iree_hal_buffer_t* target_buffer = | |
| call_state->output_states[i].mappable_device_buffer; | |
| iree_device_size_t target_offset = 0; | |
| iree_device_size_t data_length = | |
| iree_hal_buffer_view_byte_length(buffer_view); | |
| transfer_commands[buffer_index++] = (iree_hal_transfer_command_t){ | |
| .type = IREE_HAL_TRANSFER_COMMAND_TYPE_COPY, | |
| .copy = | |
| { | |
| .source_buffer = source_buffer_allocated, | |
| .source_offset = source_offset, | |
| .target_buffer = target_buffer, | |
| .target_offset = target_offset, | |
| .length = data_length, | |
| }, | |
| }; | |
| } | |
| // Construct and issue the transfer command buffer, then wait on it. | |
| iree_status_t status = iree_ok_status(); | |
| iree_hal_command_buffer_t* transfer_command_buffer = NULL; | |
| if (iree_status_is_ok(status)) { | |
| status = iree_hal_create_transfer_command_buffer( | |
| device, IREE_HAL_COMMAND_BUFFER_MODE_ONE_SHOT, | |
| IREE_HAL_QUEUE_AFFINITY_ANY, buffer_count, transfer_commands, | |
| &transfer_command_buffer); | |
| } | |
| iree_hal_semaphore_t* signal_semaphore = NULL; | |
| if (iree_status_is_ok(status)) { | |
| status = iree_hal_semaphore_create(device, 0ull, &signal_semaphore); | |
| } | |
| uint64_t signal_value = 1ull; | |
| if (iree_status_is_ok(status)) { | |
| iree_hal_semaphore_list_t signal_semaphores = { | |
| .count = 1, | |
| .semaphores = &signal_semaphore, | |
| .payload_values = &signal_value, | |
| }; | |
| status = iree_hal_device_queue_execute( | |
| device, IREE_HAL_QUEUE_AFFINITY_ANY, iree_hal_semaphore_list_empty(), | |
| signal_semaphores, 1, &transfer_command_buffer); | |
| } | |
| // TODO(scotttodd): Make this async - pass a wait source to iree_loop_wait_one | |
| // 1. create iree_hal_fence_t, iree_hal_fence_insert(fance, semaphore) | |
| // 2. iree_hal_fence_await -> iree_wait_source_t | |
| // 3. iree_loop_wait_one(loop, wait_source, ...) | |
| // (requires moving off of nop_semaphore and wait source import) | |
| if (iree_status_is_ok(status)) { | |
| status = iree_hal_semaphore_wait(signal_semaphore, signal_value, | |
| iree_infinite_timeout()); | |
| } | |
| iree_hal_command_buffer_release(transfer_command_buffer); | |
| iree_hal_semaphore_release(signal_semaphore); | |
| // Loop through one last time to map the buffers asynchronously. | |
| for (iree_host_size_t i = 0; i < outputs_size; ++i) { | |
| if (!iree_status_is_ok(status)) break; | |
| if (!call_state->output_states[i].mappable_device_buffer) continue; | |
| iree_buffer_map_userdata_t* map_userdata = NULL; | |
| IREE_RETURN_IF_ERROR(iree_allocator_malloc( | |
| iree_allocator_system(), sizeof(*map_userdata), (void**)&map_userdata)); | |
| map_userdata->call_state = call_state; | |
| map_userdata->buffer_index = i; | |
| iree_device_size_t data_length = iree_hal_buffer_view_byte_length( | |
| call_state->output_states[i].buffer_view); | |
| WGPUBuffer device_buffer = iree_hal_webgpu_buffer_handle( | |
| call_state->output_states[i].mappable_device_buffer); | |
| wgpuBufferMapAsync(device_buffer, WGPUMapMode_Read, | |
| /*offset=*/0, | |
| /*size=*/data_length, buffer_map_async_callback, | |
| /*userdata=*/map_userdata); | |
| } | |
| // Finally, wait on all wait sources. | |
| // | |
| // If there are any buffer outputs that need asynchronous mapping, those | |
| // wait sources will be signaled when the mapping completes. | |
| // | |
| // Note: call_state (and everything within it) is kept alive until the | |
| // callback resolves. | |
| if (iree_status_is_ok(status)) { | |
| status = iree_loop_wait_all(iree_loop_emscripten(call_state->loop), | |
| outputs_size, wait_sources, | |
| iree_make_timeout_ms(5000), map_all_callback, | |
| /*user_data=*/call_state); | |
| } | |
| return status; | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Function calling / invocations | |
| //===----------------------------------------------------------------------===// | |
| // Handles the completion callback from `iree_vm_async_invoke()`. | |
| iree_status_t invoke_callback(void* user_data, iree_loop_t loop, | |
| iree_status_t status, iree_vm_list_t* outputs) { | |
| iree_call_function_state_t* call_state = | |
| (iree_call_function_state_t*)user_data; | |
| call_state->invoke_end_time = iree_time_now(); | |
| if (!iree_status_is_ok(status)) { | |
| fprintf(stderr, "iree_vm_async_invoke_callback_fn_t error:\n"); | |
| iree_status_fprint(stderr, status); | |
| iree_call_function_state_destroy(call_state); | |
| return status; // Note: loop_emscripten.js must free this! | |
| } | |
| status = process_call_outputs(call_state); | |
| if (!iree_status_is_ok(status)) { | |
| fprintf(stderr, "process_call_outputs error:\n"); | |
| iree_status_fprint(stderr, status); | |
| iree_call_function_state_destroy(call_state); | |
| // Note: loop_emscripten.js must free 'status'! | |
| } | |
| return status; | |
| } | |
| const bool call_function( | |
| iree_program_state_t* program_state, const char* function_name, | |
| const char* inputs, | |
| iree_call_function_callback_fn_t completion_callback_fn) { | |
| iree_status_t status = iree_ok_status(); | |
| iree_call_function_state_t* call_state = NULL; | |
| if (iree_status_is_ok(status)) { | |
| status = iree_allocator_malloc(iree_allocator_system(), sizeof(*call_state), | |
| (void**)&call_state); | |
| } | |
| call_state->loop = program_state->sample_state->loop; | |
| call_state->callback_fn = completion_callback_fn; | |
| // Fully qualify the function name. This sample only supports loading one | |
| // module (i.e. 'program') per session, so we can do this. | |
| iree_string_builder_t name_builder; | |
| iree_string_builder_initialize(iree_allocator_system(), &name_builder); | |
| if (iree_status_is_ok(status)) { | |
| iree_string_view_t module_name = iree_vm_module_name(program_state->module); | |
| status = iree_string_builder_append_format(&name_builder, "%.*s.%s", | |
| (int)module_name.size, | |
| module_name.data, function_name); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| status = iree_runtime_call_initialize_by_name( | |
| program_state->session, iree_string_builder_view(&name_builder), | |
| &call_state->call); | |
| } | |
| iree_string_builder_deinitialize(&name_builder); | |
| if (iree_status_is_ok(status)) { | |
| status = parse_inputs_into_call( | |
| &call_state->call, iree_runtime_session_device(program_state->session), | |
| iree_runtime_session_device_allocator(program_state->session), | |
| iree_make_cstring_view(inputs)); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| status = iree_allocator_malloc(iree_allocator_system(), | |
| sizeof(*(call_state->invoke_state)), | |
| (void**)&(call_state->invoke_state)); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| iree_loop_t loop = iree_loop_emscripten(program_state->sample_state->loop); | |
| iree_vm_context_t* vm_context = | |
| iree_runtime_session_context(call_state->call.session); | |
| iree_vm_function_t vm_function = call_state->call.function; | |
| iree_vm_list_t* inputs = call_state->call.inputs; | |
| iree_vm_list_t* outputs = call_state->call.outputs; | |
| call_state->invoke_start_time = iree_time_now(); | |
| status = iree_vm_async_invoke( | |
| loop, call_state->invoke_state, vm_context, vm_function, | |
| IREE_VM_INVOCATION_FLAG_NONE, /*policy=*/NULL, inputs, outputs, | |
| iree_allocator_system(), invoke_callback, /*user_data=*/call_state); | |
| } | |
| if (!iree_status_is_ok(status)) { | |
| iree_status_fprint(stderr, status); | |
| iree_status_free(status); | |
| iree_call_function_state_destroy(call_state); | |
| return false; | |
| } | |
| return true; | |
| } |