| // 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 "experimental/webgpu/executable.h" | |
| #include <stddef.h> | |
| #include "iree/base/api.h" | |
| #include "iree/base/internal/inline_array.h" | |
| // flatcc schemas: | |
| #include "iree/base/internal/flatcc/parsing.h" | |
| #include "iree/schemas/wgsl_executable_def_reader.h" | |
| #include "iree/schemas/wgsl_executable_def_verifier.h" | |
| typedef struct iree_hal_webgpu_executable_t { | |
| iree_hal_resource_t resource; | |
| iree_allocator_t host_allocator; | |
| iree_host_size_t entry_point_count; | |
| iree_hal_webgpu_entry_point_t entry_points[]; | |
| } iree_hal_webgpu_executable_t; | |
| extern const iree_hal_executable_vtable_t iree_hal_webgpu_executable_vtable; | |
| static iree_hal_webgpu_executable_t* iree_hal_webgpu_executable_cast( | |
| iree_hal_executable_t* base_value) { | |
| IREE_HAL_ASSERT_TYPE(base_value, &iree_hal_webgpu_executable_vtable); | |
| return (iree_hal_webgpu_executable_t*)base_value; | |
| } | |
| // Verifies the structure of the flatbuffer. | |
| static iree_status_t iree_hal_webgpu_executable_flatbuffer_verify( | |
| iree_const_byte_span_t flatbuffer_data, | |
| iree_host_size_t expected_entry_point_count) { | |
| if (!flatbuffer_data.data || flatbuffer_data.data_length < 16) { | |
| return iree_make_status( | |
| IREE_STATUS_INVALID_ARGUMENT, | |
| "flatbuffer data is not present or less than 16 bytes (%" PRIhsz | |
| " total)", | |
| flatbuffer_data.data_length); | |
| } | |
| // Run flatcc generated verification. This ensures all pointers are in-bounds | |
| // and that we can safely walk the file, but not that the actual contents of | |
| // the flatbuffer meet our expectations. | |
| int verify_ret = iree_hal_wgsl_ExecutableDef_verify_as_root( | |
| flatbuffer_data.data, flatbuffer_data.data_length); | |
| if (verify_ret != flatcc_verify_ok) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "flatbuffer verification failed: %s", | |
| flatcc_verify_error_string(verify_ret)); | |
| } | |
| iree_hal_wgsl_ExecutableDef_table_t executable_def = | |
| iree_hal_wgsl_ExecutableDef_as_root(flatbuffer_data.data); | |
| iree_hal_wgsl_ShaderModuleDef_vec_t shader_modules_vec = | |
| iree_hal_wgsl_ExecutableDef_shader_modules_get(executable_def); | |
| size_t shader_module_count = | |
| iree_hal_wgsl_ShaderModuleDef_vec_len(shader_modules_vec); | |
| for (size_t i = 0; i < shader_module_count; ++i) { | |
| iree_hal_wgsl_ShaderModuleDef_table_t shader_module_def = | |
| iree_hal_wgsl_ShaderModuleDef_vec_at(shader_modules_vec, i); | |
| if (flatbuffers_string_len( | |
| iree_hal_wgsl_ShaderModuleDef_code_get(shader_module_def)) == 0) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "shader module %zu WGSL code is missing/empty", | |
| i); | |
| } | |
| } | |
| flatbuffers_uint32_vec_t entry_points_vec = | |
| iree_hal_wgsl_ExecutableDef_entry_points_get(executable_def); | |
| size_t entry_point_count = flatbuffers_uint32_vec_len(entry_points_vec); | |
| if (entry_point_count != expected_entry_point_count) { | |
| return iree_make_status(IREE_STATUS_FAILED_PRECONDITION, | |
| "executable provides %zu entry points but caller " | |
| "provided %" PRIhsz "; must match", | |
| entry_point_count, expected_entry_point_count); | |
| } | |
| for (size_t i = 0; i < entry_point_count; ++i) { | |
| uint32_t module_ordinal = flatbuffers_uint32_vec_at(entry_points_vec, i); | |
| if (module_ordinal >= shader_module_count) { | |
| return iree_make_status(IREE_STATUS_INVALID_ARGUMENT, | |
| "executable entry point %zu" | |
| " references an invalid shader module %d", | |
| i, module_ordinal); | |
| } | |
| } | |
| return iree_ok_status(); | |
| } | |
| static iree_status_t iree_hal_webgpu_create_wgsl_shader_module( | |
| WGPUDevice device, iree_hal_wgsl_ShaderModuleDef_table_t shader_module_def, | |
| WGPUShaderModule* out_shader_module) { | |
| IREE_ASSERT_ARGUMENT(shader_module_def); | |
| IREE_ASSERT_ARGUMENT(out_shader_module); | |
| *out_shader_module = NULL; | |
| IREE_TRACE_ZONE_BEGIN(z0); | |
| const char* code = iree_hal_wgsl_ShaderModuleDef_code_get(shader_module_def); | |
| const WGPUShaderModuleWGSLDescriptor descriptor = { | |
| .chain = | |
| { | |
| .next = NULL, | |
| .sType = WGPUSType_ShaderModuleWGSLDescriptor, | |
| }, | |
| .code = code, | |
| }; | |
| const WGPUShaderModuleDescriptor module_descriptor = { | |
| .nextInChain = &descriptor.chain, | |
| .label = NULL, | |
| }; | |
| *out_shader_module = wgpuDeviceCreateShaderModule(device, &module_descriptor); | |
| iree_status_t status = iree_ok_status(); | |
| if (!*out_shader_module) { | |
| // TODO(benvanik): see if we can get more detailed error info. | |
| status = iree_make_status(IREE_STATUS_INTERNAL, | |
| "wgpuDeviceCreateShaderModule failed"); | |
| } | |
| IREE_TRACE_ZONE_END(z0); | |
| return status; | |
| } | |
| // Enough room for `d` + max uint32 characters + NUL. | |
| #define IREE_HAL_WEBGPU_MAX_ENTRY_NAME_LENGTH (1 + /*uint32*/ 10 + /*NUL*/ 1) | |
| // Makes a canonical entry point name based on its entry ordinal. | |
| // |buffer| must have at least | |
| // Example: ordinal 3 => 'd3' | |
| static void iree_hal_webgpu_make_entry_name(uint32_t entry_ordinal, | |
| char* buffer) { | |
| // Inlined base 10 unsigned itoa-like. | |
| // Generates the string in reverse and then flips it around. | |
| // It's not worth pulling in snprintf for this. | |
| buffer[0] = 'd'; | |
| ++buffer; | |
| uint32_t n = entry_ordinal; | |
| int length = 0; | |
| do { | |
| buffer[length++] = '0' + (n % 10); | |
| } while ((n /= 10) > 0); | |
| buffer[length] = '\0'; | |
| for (int i = 0, j = length - 1; i < j; ++i, --j) { | |
| char c = buffer[i]; | |
| buffer[i] = buffer[j]; | |
| buffer[j] = c; | |
| } | |
| } | |
| // TODO(benvanik): switch to async compilation using | |
| // wgpuDeviceCreateComputePipelineAsync. We pack all pipelines into a single | |
| // executable (usually) and can batch compilation of all of them and only | |
| // join at the end. Technically we could extend the join point until first use | |
| // but it's harder to reason about lifetime that way. Today we just compile | |
| // them all synchronously. | |
| static iree_status_t iree_hal_webgpu_create_pipeline( | |
| WGPUDevice device, WGPUShaderModule shader_module, uint32_t entry_ordinal, | |
| iree_hal_pipeline_layout_t* pipeline_layout, | |
| iree_hal_webgpu_entry_point_t* out_entry_point) { | |
| IREE_ASSERT_ARGUMENT(shader_module); | |
| IREE_ASSERT_ARGUMENT(pipeline_layout); | |
| IREE_ASSERT_ARGUMENT(out_entry_point); | |
| IREE_TRACE_ZONE_BEGIN(z0); | |
| char entry_name[IREE_HAL_WEBGPU_MAX_ENTRY_NAME_LENGTH] = {0}; | |
| iree_hal_webgpu_make_entry_name(entry_ordinal, entry_name); | |
| const WGPUComputePipelineDescriptor pipeline_descriptor = { | |
| .nextInChain = NULL, | |
| .label = WGPU_DEBUG_LABEL(entry_name), | |
| .layout = iree_hal_webgpu_pipeline_layout_handle(pipeline_layout), | |
| .compute = | |
| { | |
| .nextInChain = NULL, | |
| .module = shader_module, | |
| .entryPoint = entry_name, | |
| }, | |
| }; | |
| WGPUComputePipeline pipeline = | |
| wgpuDeviceCreateComputePipeline(device, &pipeline_descriptor); | |
| iree_status_t status = iree_ok_status(); | |
| if (!pipeline) { | |
| status = iree_make_status(IREE_STATUS_INTERNAL, | |
| "wgpuDeviceCreateComputePipeline " | |
| "failed for entry point '%s'", | |
| entry_name); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| out_entry_point->pipeline = pipeline; | |
| out_entry_point->layout = pipeline_layout; | |
| iree_hal_pipeline_layout_retain(pipeline_layout); | |
| } | |
| IREE_TRACE_ZONE_END(z0); | |
| return status; | |
| } | |
| iree_status_t iree_hal_webgpu_executable_create( | |
| WGPUDevice device, const iree_hal_executable_params_t* executable_params, | |
| iree_allocator_t host_allocator, iree_hal_executable_t** out_executable) { | |
| IREE_ASSERT_ARGUMENT(device); | |
| IREE_ASSERT_ARGUMENT(executable_params); | |
| IREE_ASSERT_ARGUMENT(out_executable); | |
| *out_executable = NULL; | |
| IREE_TRACE_ZONE_BEGIN(z0); | |
| // Verify support up-front - the code below assumes | |
| if (!iree_string_view_equal(executable_params->executable_format, | |
| iree_make_cstring_view("webgpu-wgsl-fb"))) { | |
| IREE_TRACE_ZONE_END(z0); | |
| return iree_make_status( | |
| IREE_STATUS_UNIMPLEMENTED, | |
| "executable format '%.*s' not available in this build", | |
| (int)executable_params->executable_format.size, | |
| executable_params->executable_format.data); | |
| } | |
| // Verify and fetch the executable flatbuffer wrapper. | |
| IREE_RETURN_AND_END_ZONE_IF_ERROR( | |
| z0, iree_hal_webgpu_executable_flatbuffer_verify( | |
| executable_params->executable_data, | |
| executable_params->pipeline_layout_count)); | |
| iree_hal_wgsl_ExecutableDef_table_t executable_def = | |
| iree_hal_wgsl_ExecutableDef_as_root( | |
| executable_params->executable_data.data); | |
| // Create shader modules. This will be cheap on some implementations like | |
| // Metal that need pipeline information in order to be JIT'ed from WGSL while | |
| // on others it can be more expensive. | |
| iree_hal_wgsl_ShaderModuleDef_vec_t shader_modules_vec = | |
| iree_hal_wgsl_ExecutableDef_shader_modules_get(executable_def); | |
| size_t shader_module_count = | |
| iree_hal_wgsl_ShaderModuleDef_vec_len(shader_modules_vec); | |
| iree_inline_array(WGPUShaderModule, shader_modules, shader_module_count, | |
| host_allocator); | |
| memset(iree_inline_array_data(shader_modules), 0, | |
| sizeof(WGPUShaderModule) * shader_module_count); | |
| iree_status_t status = iree_ok_status(); | |
| for (size_t i = 0; i < shader_module_count; ++i) { | |
| status = iree_hal_webgpu_create_wgsl_shader_module( | |
| device, iree_hal_wgsl_ShaderModuleDef_vec_at(shader_modules_vec, i), | |
| iree_inline_array_at(shader_modules, i)); | |
| if (!iree_status_is_ok(status)) break; | |
| } | |
| // Allocate the executable with storage for the pipeline handles. | |
| iree_hal_webgpu_executable_t* executable = NULL; | |
| if (iree_status_is_ok(status)) { | |
| iree_host_size_t total_size = | |
| sizeof(*executable) + executable_params->pipeline_layout_count * | |
| sizeof(iree_hal_webgpu_entry_point_t); | |
| status = | |
| iree_allocator_malloc(host_allocator, total_size, (void**)&executable); | |
| } | |
| if (iree_status_is_ok(status)) { | |
| iree_hal_resource_initialize(&iree_hal_webgpu_executable_vtable, | |
| &executable->resource); | |
| executable->host_allocator = host_allocator; | |
| executable->entry_point_count = executable_params->pipeline_layout_count; | |
| // Create one pipeline per entry point. | |
| flatbuffers_uint32_vec_t entry_points_vec = | |
| iree_hal_wgsl_ExecutableDef_entry_points_get(executable_def); | |
| for (iree_host_size_t i = 0; i < executable->entry_point_count; i++) { | |
| uint32_t module_ordinal = flatbuffers_uint32_vec_at(entry_points_vec, i); | |
| status = iree_hal_webgpu_create_pipeline( | |
| device, *iree_inline_array_at(shader_modules, module_ordinal), i, | |
| executable_params->pipeline_layouts[i], &executable->entry_points[i]); | |
| if (!iree_status_is_ok(status)) break; | |
| } | |
| } | |
| for (size_t i = 0; i < shader_module_count; ++i) { | |
| iree_wgpuShaderModuleDrop(*iree_inline_array_at(shader_modules, i)); | |
| } | |
| iree_inline_array_deinitialize(shader_modules); | |
| if (iree_status_is_ok(status)) { | |
| *out_executable = (iree_hal_executable_t*)executable; | |
| } else { | |
| iree_hal_executable_destroy((iree_hal_executable_t*)executable); | |
| } | |
| IREE_TRACE_ZONE_END(z0); | |
| return status; | |
| } | |
| static void iree_hal_webgpu_executable_destroy( | |
| iree_hal_executable_t* base_executable) { | |
| iree_hal_webgpu_executable_t* executable = | |
| iree_hal_webgpu_executable_cast(base_executable); | |
| iree_allocator_t host_allocator = executable->host_allocator; | |
| IREE_TRACE_ZONE_BEGIN(z0); | |
| for (iree_host_size_t i = 0; i < executable->entry_point_count; i++) { | |
| iree_hal_webgpu_entry_point_t* entry_point = &executable->entry_points[i]; | |
| iree_hal_pipeline_layout_release(entry_point->layout); | |
| iree_wgpuComputePipelineDrop(entry_point->pipeline); | |
| } | |
| iree_allocator_free(host_allocator, executable); | |
| IREE_TRACE_ZONE_END(z0); | |
| } | |
| const iree_hal_webgpu_entry_point_t* | |
| iree_hal_webgpu_executable_lookup_entry_point( | |
| iree_hal_executable_t* base_executable, uint32_t ordinal) { | |
| iree_hal_webgpu_executable_t* executable = | |
| iree_hal_webgpu_executable_cast(base_executable); | |
| IREE_ASSERT_LT(ordinal, executable->entry_point_count); | |
| return &executable->entry_points[ordinal]; | |
| } | |
| const iree_hal_executable_vtable_t iree_hal_webgpu_executable_vtable = { | |
| .destroy = iree_hal_webgpu_executable_destroy, | |
| }; |