#include "vulkan_backend.hpp" #include "kernels.hpp" #include #include #include #include #include #include int main() { const auto info = ppocr::detail::QueryVulkanBackendInfo(); if (!info.vulkan_compute_available) { std::cerr << "Vulkan compute unavailable\n"; return 2; } // GPU-only graph execution will retain live values in a tensor arena rather // than using a per-operator host round trip. Verify the allocator's // last-use reuse contract independently of any model-specific graph. { ppocr::detail::VulkanTensorArena arena; if (!arena.available()) { std::cerr << "Vulkan tensor arena unavailable\n"; return 1; } auto first = arena.Acquire(4096, "smoke-first"); if (!first.resident || !arena.mapped_data(first) || arena.live_bytes() != 4096 * sizeof(float)) { std::cerr << "Vulkan tensor arena initial allocation failed\n"; return 1; } const auto capacity = arena.capacity_bytes(); arena.Release(first); auto reused = arena.Acquire(2048, "smoke-reused"); if (!reused.resident || reused.capacity_elements < 4096 || arena.capacity_bytes() != capacity || arena.live_bytes() != 2048 * sizeof(float)) { std::cerr << "Vulkan tensor arena reuse failed\n"; return 1; } auto rhs = arena.Acquire(2048, "smoke-rhs"); auto rhs2 = arena.Acquire(2048, "smoke-rhs2"); std::vector arena_left(2048), arena_right(2048), arena_output(2048); for (std::size_t index = 0; index < arena_left.size(); ++index) { arena_left[index] = float(index) * .125F - 3.F; arena_right[index] = float(index % 17) * .25F + .5F; } if (!rhs.resident || !rhs2.resident || !arena.Upload(reused, arena_left.data(), arena_left.size()) || !arena.Upload(rhs, arena_right.data(), arena_right.size()) || !arena.BinaryInplace(reused, rhs, ppocr::detail::kernels::BinaryOp::mul) || !arena.Download(arena_output.data(), reused, arena_output.size())) { std::cerr << "Vulkan tensor arena device binary failed\n"; return 1; } for (std::size_t index = 0; index < arena_output.size(); ++index) { const float expected = arena_left[index] * arena_right[index]; if (std::abs(arena_output[index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena device binary mismatch at " << index << '\n'; return 1; } } // BatchNorm's inference form is a folded channel-affine operation. constexpr int affine_channels = 3; constexpr std::size_t affine_plane = 17; std::vector host_affine(affine_channels * affine_plane), host_scale{.5F, -1.25F, .75F}, host_affine_bias{.1F, .25F, -.4F}, host_affine_output(host_affine.size()); for (std::size_t i = 0; i < host_affine.size(); ++i) host_affine[i] = float(i % 19) * .125F - .75F; auto affine_value = arena.Acquire(host_affine.size(), "arena-affine-value"); auto affine_scale = arena.Acquire(host_scale.size(), "arena-affine-scale"); auto affine_bias = arena.Acquire(host_affine_bias.size(), "arena-affine-bias"); if (!arena.Upload(affine_value, host_affine.data(), host_affine.size()) || !arena.Upload(affine_scale, host_scale.data(), host_scale.size()) || !arena.Upload(affine_bias, host_affine_bias.data(), host_affine_bias.size()) || !arena.ChannelAffineInplace(affine_value, affine_scale, affine_bias, 1, affine_channels, affine_plane) || !arena.Download(host_affine_output.data(), affine_value, host_affine_output.size())) { std::cerr << "Vulkan tensor arena channel affine failed\n"; return 1; } for (std::size_t i = 0; i < host_affine_output.size(); ++i) { const auto channel = i / affine_plane; const float expected = host_affine[i] * host_scale[channel] + host_affine_bias[channel]; if (std::abs(host_affine_output[i] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena channel affine mismatch at " << i << '\n'; return 1; } } // GPU graph arithmetic uses compact channel broadcasts for SE gates and // normalisation-scale paths. Verify that it writes a third live slot, so // neither source needs an implicit host-side alias/copy. constexpr std::size_t broadcast_batches = 2, broadcast_channels = 4, broadcast_plane = 17; std::vector broadcast_left(broadcast_batches * broadcast_channels * broadcast_plane), broadcast_rhs(broadcast_channels), broadcast_output(broadcast_left.size()); for (std::size_t i = 0; i < broadcast_left.size(); ++i) broadcast_left[i] = float(i % 23) * .0625F - .7F; for (std::size_t i = 0; i < broadcast_rhs.size(); ++i) broadcast_rhs[i] = float(i) * .125F - .2F; auto broadcast_a = arena.Acquire(broadcast_left.size(), "arena-broadcast-a"); auto broadcast_b = arena.Acquire(broadcast_rhs.size(), "arena-broadcast-b"); auto broadcast_d = arena.Acquire(broadcast_output.size(), "arena-broadcast-d"); if (!arena.Upload(broadcast_a, broadcast_left.data(), broadcast_left.size()) || !arena.Upload(broadcast_b, broadcast_rhs.data(), broadcast_rhs.size()) || !arena.BinaryBroadcast(broadcast_a, broadcast_b, broadcast_d, broadcast_batches, broadcast_plane, broadcast_channels, true, ppocr::detail::kernels::BinaryOp::mul) || !arena.Download(broadcast_output.data(), broadcast_d, broadcast_output.size())) { std::cerr << "Vulkan tensor arena broadcast failed\n"; return 1; } for (std::size_t i = 0; i < broadcast_output.size(); ++i) { const float expected = broadcast_left[i] * broadcast_rhs[(i / broadcast_plane) % broadcast_channels]; if (std::abs(broadcast_output[i] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena broadcast mismatch at " << i << '\n'; return 1; } } arena.Release(broadcast_d); arena.Release(broadcast_b); arena.Release(broadcast_a); // Detector FPN and recognizer blocks assemble channel/feature branches // with 2- and 4-input Concat. Verify a non-trivial outer/inner layout so // this graph operation cannot accidentally be only a flat append. constexpr std::size_t concat_outer = 3, concat_inner = 5; const std::vector concat_axes{2, 3, 1, 4}; std::vector> concat_host(4); std::vector concat_inputs; for (std::size_t input_index = 0; input_index < concat_axes.size(); ++input_index) { concat_host[input_index].resize(concat_outer * concat_axes[input_index] * concat_inner); for (std::size_t i = 0; i < concat_host[input_index].size(); ++i) concat_host[input_index][i] = float(input_index * 1000 + i) * .03125F; concat_inputs.push_back(arena.Acquire(concat_host[input_index].size(), "arena-concat-input")); if (!arena.Upload(concat_inputs.back(), concat_host[input_index].data(), concat_host[input_index].size())) { std::cerr << "Vulkan tensor arena Concat input upload failed\n"; return 1; } } const std::size_t concat_axis_total = 10; std::vector concat_output(concat_outer * concat_axis_total * concat_inner); auto concat_slot = arena.Acquire(concat_output.size(), "arena-concat-output"); if (!arena.Concat(concat_inputs, concat_slot, concat_outer, concat_inner, concat_axes) || !arena.Download(concat_output.data(), concat_slot, concat_output.size())) { std::cerr << "Vulkan tensor arena Concat failed\n"; return 1; } for (std::size_t outer = 0; outer < concat_outer; ++outer) { std::size_t axis_base{}; for (std::size_t input_index = 0; input_index < concat_axes.size(); ++input_index) { for (std::size_t axis = 0; axis < concat_axes[input_index]; ++axis) for (std::size_t inner = 0; inner < concat_inner; ++inner) { const auto dst = (outer * concat_axis_total + axis_base + axis) * concat_inner + inner; const auto src = (outer * concat_axes[input_index] + axis) * concat_inner + inner; if (concat_output[dst] != concat_host[input_index][src]) { std::cerr << "Vulkan tensor arena Concat mismatch at " << dst << '\n'; return 1; } } axis_base += concat_axes[input_index]; } } arena.Release(concat_slot); for (auto& slot : concat_inputs) arena.Release(slot); // The unfused detector FPN Resize nodes use integer asymmetric nearest // resize before their following consumer. Exercise unequal scale factors // and an N=2 batch, which catches both coordinate and batch addressing. constexpr std::size_t nearest_batches = 2; constexpr int nearest_channels = 3, nearest_h = 3, nearest_w = 4, nearest_sh = 2, nearest_sw = 3; std::vector nearest_input(nearest_batches * nearest_channels * nearest_h * nearest_w), nearest_output(nearest_batches * nearest_channels * nearest_h * nearest_sh * nearest_w * nearest_sw); for (std::size_t i = 0; i < nearest_input.size(); ++i) nearest_input[i] = float(i) * .03125F - .5F; auto nearest_a = arena.Acquire(nearest_input.size(), "arena-nearest-input"); auto nearest_d = arena.Acquire(nearest_output.size(), "arena-nearest-output"); if (!arena.Upload(nearest_a, nearest_input.data(), nearest_input.size()) || !arena.NearestResize(nearest_a, nearest_d, nearest_batches, nearest_channels, nearest_h, nearest_w, nearest_sh, nearest_sw) || !arena.Download(nearest_output.data(), nearest_d, nearest_output.size())) { std::cerr << "Vulkan tensor arena nearest Resize failed\n"; return 1; } const int nearest_oh = nearest_h * nearest_sh, nearest_ow = nearest_w * nearest_sw; for (std::size_t n = 0; n < nearest_batches; ++n) for (int c = 0; c < nearest_channels; ++c) for (int y = 0; y < nearest_oh; ++y) for (int x = 0; x < nearest_ow; ++x) { const auto src = ((n * nearest_channels + c) * nearest_h + y / nearest_sh) * nearest_w + x / nearest_sw; const auto dst = ((n * nearest_channels + c) * nearest_oh + y) * nearest_ow + x; if (nearest_output[dst] != nearest_input[src]) { std::cerr << "Vulkan tensor arena nearest Resize mismatch at " << dst << '\n'; return 1; } } arena.Release(nearest_d); arena.Release(nearest_a); // Medium detector exports fused squeeze-excitation gates. This verifies // the complete mean/projection/gate/broadcast chain stays in the arena; // only its final activation is observed on the host. constexpr std::size_t se_batches = 2, se_plane = 13; constexpr int se_channels = 4, se_reduced = 3; std::vector se_input(se_batches * se_channels * se_plane), se_first_weights(se_reduced * se_channels), se_first_bias(se_reduced), se_second_weights(se_channels * se_reduced), se_second_bias(se_channels), se_output(se_input.size()); for (std::size_t i = 0; i < se_input.size(); ++i) se_input[i] = float(i % 31) * .0625F - .8F; for (std::size_t i = 0; i < se_first_weights.size(); ++i) se_first_weights[i] = float(i % 7) * .0625F - .15F; for (std::size_t i = 0; i < se_first_bias.size(); ++i) se_first_bias[i] = float(i) * .1F - .1F; for (std::size_t i = 0; i < se_second_weights.size(); ++i) se_second_weights[i] = float(i % 5) * .05F - .1F; for (std::size_t i = 0; i < se_second_bias.size(); ++i) se_second_bias[i] = float(i) * .075F - .1F; auto se_a = arena.Acquire(se_input.size(), "arena-se-input"); auto se_w1 = arena.Acquire(se_first_weights.size(), "arena-se-w1"); auto se_b1 = arena.Acquire(se_first_bias.size(), "arena-se-b1"); auto se_w2 = arena.Acquire(se_second_weights.size(), "arena-se-w2"); auto se_b2 = arena.Acquire(se_second_bias.size(), "arena-se-b2"); auto se_d = arena.Acquire(se_output.size(), "arena-se-output"); if (!arena.Upload(se_a, se_input.data(), se_input.size()) || !arena.Upload(se_w1, se_first_weights.data(), se_first_weights.size()) || !arena.Upload(se_b1, se_first_bias.data(), se_first_bias.size()) || !arena.Upload(se_w2, se_second_weights.data(), se_second_weights.size()) || !arena.Upload(se_b2, se_second_bias.data(), se_second_bias.size()) || !arena.SqueezeExcitationGate(se_a, se_w1, se_b1, se_w2, se_b2, se_d, se_batches, se_channels, se_reduced, se_plane, .2F, .5F) || !arena.Download(se_output.data(), se_d, se_output.size())) { std::cerr << "Vulkan tensor arena squeeze-excitation failed\n"; return 1; } for (std::size_t n = 0; n < se_batches; ++n) { std::vector reduced(se_reduced), gate(se_channels); for (int r = 0; r < se_reduced; ++r) { float value = se_first_bias[r]; for (int c = 0; c < se_channels; ++c) { float mean{}; for (std::size_t p = 0; p < se_plane; ++p) mean += se_input[(n * se_channels + c) * se_plane + p]; value += mean / float(se_plane) * se_first_weights[r * se_channels + c]; } reduced[r] = std::max(value, 0.F); } for (int c = 0; c < se_channels; ++c) { float value = se_second_bias[c]; for (int r = 0; r < se_reduced; ++r) value += reduced[r] * se_second_weights[c * se_reduced + r]; gate[c] = std::clamp(.2F * value + .5F, 0.F, 1.F); } for (int c = 0; c < se_channels; ++c) for (std::size_t p = 0; p < se_plane; ++p) { const auto index = (n * se_channels + c) * se_plane + p; const float expected = se_input[index] * gate[c]; if (std::abs(se_output[index] - expected) > 4e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena squeeze-excitation mismatch at " << index << '\n'; return 1; } } } arena.Release(se_d); arena.Release(se_b2); arena.Release(se_w2); arena.Release(se_b1); arena.Release(se_w1); arena.Release(se_a); // A graph branch must be able to duplicate a still-live device tensor // without exposing mapped data. Use an odd tail length to cover the // shader's final scalar lanes. std::vector copy_input(1031), copy_output(copy_input.size()); for (std::size_t i = 0; i < copy_input.size(); ++i) copy_input[i] = float(i) * .0078125F - 2.F; auto copy_a = arena.Acquire(copy_input.size(), "arena-copy-input"); auto copy_d = arena.Acquire(copy_output.size(), "arena-copy-output"); if (!arena.Upload(copy_a, copy_input.data(), copy_input.size()) || !arena.Copy(copy_a, copy_d, copy_input.size()) || !arena.Download(copy_output.data(), copy_d, copy_output.size()) || copy_output != copy_input) { std::cerr << "Vulkan tensor arena device copy failed\n"; return 1; } arena.Release(copy_d); arena.Release(copy_a); arena.Release(affine_bias); arena.Release(affine_scale); arena.Release(affine_value); // Unary activations are a separate device-resident graph boundary; chain // exact ONNX HardSigmoid and HardSwish before observing the result. if (!arena.Upload(reused, arena_left.data(), arena_left.size()) || !arena.UnaryInplace(reused, ppocr::detail::VulkanUnaryOp::hard_sigmoid, .2F, .5F) || !arena.UnaryInplace(reused, ppocr::detail::VulkanUnaryOp::hard_swish, 1.F / 6.F, .5F) || !arena.Download(arena_output.data(), reused, arena_output.size())) { std::cerr << "Vulkan tensor arena unary activation failed\n"; return 1; } for (std::size_t index = 0; index < arena_output.size(); ++index) { const float hard_sigmoid = std::clamp(.2F * arena_left[index] + .5F, 0.F, 1.F); const float expected = hard_sigmoid * std::clamp(hard_sigmoid / 6.F + .5F, 0.F, 1.F); if (std::abs(arena_output[index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena unary activation mismatch at " << index << '\n'; return 1; } } // Exact Erf-GELU is required by all PP-OCRv6 variants. This is a graph // device operation, not the optional approximate fused-convolution mode. if (!arena.Upload(reused, arena_left.data(), arena_left.size()) || !arena.UnaryInplace(reused, ppocr::detail::VulkanUnaryOp::gelu) || !arena.Download(arena_output.data(), reused, arena_output.size())) { std::cerr << "Vulkan tensor arena GELU failed\n"; return 1; } for (std::size_t index = 0; index < arena_output.size(); ++index) { const float expected = .5F * arena_left[index] * (1.F + std::erf(arena_left[index] * .7071067811865475F)); if (std::abs(arena_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena GELU mismatch at " << index << '\n'; return 1; } } if (!arena.Upload(reused, arena_left.data(), arena_left.size()) || !arena.Upload(rhs2, arena_right.data(), arena_right.size()) || !arena.BinaryChainInplace(reused, {rhs, rhs2}, {ppocr::detail::kernels::BinaryOp::add, ppocr::detail::kernels::BinaryOp::mul}) || !arena.Download(arena_output.data(), reused, arena_output.size())) { std::cerr << "Vulkan tensor arena device chain failed\n"; return 1; } for (std::size_t index = 0; index < arena_output.size(); ++index) { const float expected = (arena_left[index] + arena_right[index]) * arena_right[index]; if (std::abs(arena_output[index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena device chain mismatch at " << index << '\n'; return 1; } } arena.Release(rhs2); arena.Release(rhs); arena.Release(reused); // Validate a real device-resident NCHW pointwise block. Its parameters // and activation live in arena slots; only the completed output crosses // the graph boundary for this smoke comparison. constexpr int input_channels = 3; constexpr int output_channels = 5; constexpr std::size_t plane = 37; auto input = arena.Acquire(input_channels * plane, "smoke-pointwise-input"); auto weights = arena.Acquire(input_channels * output_channels, "smoke-pointwise-weights"); auto bias = arena.Acquire(output_channels, "smoke-pointwise-bias"); auto output = arena.Acquire(output_channels * plane, "smoke-pointwise-output"); std::vector host_input(input_channels * plane), host_weights(input_channels * output_channels), host_bias(output_channels), host_output(output_channels * plane); for (std::size_t index = 0; index < host_input.size(); ++index) host_input[index] = float(index % 11) * .125F - .5F; for (std::size_t index = 0; index < host_weights.size(); ++index) host_weights[index] = float(index % 7) * .0625F - .1875F; for (std::size_t index = 0; index < host_bias.size(); ++index) host_bias[index] = float(index) * .1F - .2F; if (!input.resident || !weights.resident || !bias.resident || !output.resident || !arena.Upload(input, host_input.data(), host_input.size()) || !arena.Upload(weights, host_weights.data(), host_weights.size()) || !arena.Upload(bias, host_bias.data(), host_bias.size()) || !arena.PointwiseConv(input, weights, bias, output, 1, input_channels, output_channels, plane) || !arena.Download(host_output.data(), output, host_output.size())) { std::cerr << "Vulkan tensor arena pointwise failed\n"; return 1; } for (int channel = 0; channel < output_channels; ++channel) for (std::size_t spatial = 0; spatial < plane; ++spatial) { float expected = host_bias[channel]; for (int source = 0; source < input_channels; ++source) expected += host_input[std::size_t(source) * plane + spatial] * host_weights[std::size_t(channel) * input_channels + source]; const auto index = std::size_t(channel) * plane + spatial; if (std::abs(host_output[index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena pointwise mismatch at " << index << '\n'; return 1; } } arena.Release(output); arena.Release(bias); arena.Release(weights); arena.Release(input); // The MobileNet backbone relies on depthwise filters. Verify a padded, // strided device-resident case so the arena path covers the geometry used // by PP-OCRv6 rather than only elementwise/1x1 layers. constexpr int depth_channels = 3, input_height = 7, input_width = 9; constexpr int output_height = 4, output_width = 5, kernel_height = 3, kernel_width = 3; auto depth_input = arena.Acquire(depth_channels * input_height * input_width, "smoke-depth-input"); auto depth_weights = arena.Acquire(depth_channels * kernel_height * kernel_width, "smoke-depth-weights"); auto depth_bias = arena.Acquire(depth_channels, "smoke-depth-bias"); auto depth_output = arena.Acquire(depth_channels * output_height * output_width, "smoke-depth-output"); std::vector host_depth_input(depth_channels * input_height * input_width), host_depth_weights(depth_channels * kernel_height * kernel_width), host_depth_bias(depth_channels), host_depth_output(depth_channels * output_height * output_width); for (std::size_t index = 0; index < host_depth_input.size(); ++index) host_depth_input[index] = float(index % 13) * .1F - .4F; for (std::size_t index = 0; index < host_depth_weights.size(); ++index) host_depth_weights[index] = float(index % 5) * .05F - .1F; for (std::size_t index = 0; index < host_depth_bias.size(); ++index) host_depth_bias[index] = float(index) * .07F - .1F; if (!depth_input.resident || !depth_weights.resident || !depth_bias.resident || !depth_output.resident || !arena.Upload(depth_input, host_depth_input.data(), host_depth_input.size()) || !arena.Upload(depth_weights, host_depth_weights.data(), host_depth_weights.size()) || !arena.Upload(depth_bias, host_depth_bias.data(), host_depth_bias.size()) || !arena.DepthwiseConv(depth_input, depth_weights, depth_bias, depth_output, 1, depth_channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, 2, 2, 1, 1) || !arena.Download(host_depth_output.data(), depth_output, host_depth_output.size())) { std::cerr << "Vulkan tensor arena depthwise failed\n"; return 1; } for (int channel = 0; channel < depth_channels; ++channel) for (int y = 0; y < output_height; ++y) for (int x = 0; x < output_width; ++x) { float expected = host_depth_bias[channel]; for (int ky = 0; ky < kernel_height; ++ky) for (int kx = 0; kx < kernel_width; ++kx) { const int source_y = y * 2 + ky - 1, source_x = x * 2 + kx - 1; if (source_y >= 0 && source_y < input_height && source_x >= 0 && source_x < input_width) { expected += host_depth_input[(std::size_t(channel) * input_height + source_y) * input_width + source_x] * host_depth_weights[(std::size_t(channel) * kernel_height + ky) * kernel_width + kx]; } } const auto index = (std::size_t(channel) * output_height + y) * output_width + x; if (std::abs(host_depth_output[index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena depthwise mismatch at " << index << '\n'; return 1; } } auto depth_hswish = arena.Acquire(host_depth_output.size(), "smoke-depth-hswish"); std::vector host_depth_hswish(host_depth_output.size()), cpu_depth_hswish(host_depth_output.size()); if (!depth_hswish.resident || !arena.DepthwiseConv(depth_input, depth_weights, depth_bias, depth_hswish, 1, depth_channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, 2, 2, 1, 1, false, false, true) || !arena.Download(host_depth_hswish.data(), depth_hswish, host_depth_hswish.size())) { std::cerr << "Vulkan tensor arena depthwise hard-swish failed\n"; return 1; } ppocr::detail::kernels::DepthwiseConv(cpu_depth_hswish.data(), host_depth_input.data(), host_depth_weights.data(), host_depth_bias.data(), depth_channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, 2, 2, 1, 1); ppocr::detail::kernels::HardSwish(cpu_depth_hswish.data(), cpu_depth_hswish.data(), cpu_depth_hswish.size()); for (std::size_t index = 0; index < cpu_depth_hswish.size(); ++index) { const float tolerance = 4e-5F * std::max(1.F, std::abs(cpu_depth_hswish[index])); if (std::abs(host_depth_hswish[index] - cpu_depth_hswish[index]) > tolerance) { std::cerr << "Vulkan tensor arena depthwise hard-swish mismatch at " << index << '\n'; return 1; } } arena.Release(depth_hswish); // This is the smallest feature map reached by the medium detector's // final MobileNet blocks. It specifically takes the generic packed-vec4 // depthwise route (rather than the shared-filter spatial-tile route), // and guards the Radeon device-loss regression seen in the full graph. constexpr int tiny_depth_channels = 896; constexpr int tiny_depth_height = 5; constexpr int tiny_depth_width = 22; constexpr int tiny_depth_kernel = 3; const std::size_t tiny_depth_plane = std::size_t(tiny_depth_height) * tiny_depth_width; std::vector tiny_depth_input(tiny_depth_channels * tiny_depth_plane), tiny_depth_weights(std::size_t(tiny_depth_channels) * tiny_depth_kernel * tiny_depth_kernel), tiny_depth_bias(tiny_depth_channels), tiny_depth_output(tiny_depth_channels * tiny_depth_plane); for (std::size_t index = 0; index < tiny_depth_input.size(); ++index) tiny_depth_input[index] = float(int(index % 31) - 15) * .03125F; for (std::size_t index = 0; index < tiny_depth_weights.size(); ++index) tiny_depth_weights[index] = float(int(index % 19) - 9) * .015625F; for (int channel = 0; channel < tiny_depth_channels; ++channel) tiny_depth_bias[channel] = float(channel % 13 - 6) * .0625F; auto tiny_depth_input_slot = arena.Acquire(tiny_depth_input.size(), "smoke-tiny-depth-input"); auto tiny_depth_weights_slot = arena.Acquire(tiny_depth_weights.size(), "smoke-tiny-depth-weights"); auto tiny_depth_bias_slot = arena.Acquire(tiny_depth_bias.size(), "smoke-tiny-depth-bias"); auto tiny_depth_output_slot = arena.Acquire(tiny_depth_output.size(), "smoke-tiny-depth-output"); if (!arena.Upload(tiny_depth_input_slot, tiny_depth_input.data(), tiny_depth_input.size()) || !arena.Upload(tiny_depth_weights_slot, tiny_depth_weights.data(), tiny_depth_weights.size()) || !arena.Upload(tiny_depth_bias_slot, tiny_depth_bias.data(), tiny_depth_bias.size()) || !arena.BeginGraphRecording() || !arena.DepthwiseConvScalar(tiny_depth_input_slot, tiny_depth_weights_slot, tiny_depth_bias_slot, tiny_depth_output_slot, 1, tiny_depth_channels, tiny_depth_height, tiny_depth_width, tiny_depth_height, tiny_depth_width, tiny_depth_kernel, tiny_depth_kernel, 1, 1, 1, 1) || !arena.EndGraphRecording() || !arena.Download(tiny_depth_output.data(), tiny_depth_output_slot, tiny_depth_output.size())) { std::cerr << "Vulkan tensor arena tiny-plane depthwise failed\n"; return 1; } for (int channel = 0; channel < tiny_depth_channels; ++channel) for (int y = 0; y < tiny_depth_height; ++y) for (int x = 0; x < tiny_depth_width; ++x) { float expected = tiny_depth_bias[channel]; for (int ky = 0; ky < tiny_depth_kernel; ++ky) for (int kx = 0; kx < tiny_depth_kernel; ++kx) { const int source_y = y + ky - 1, source_x = x + kx - 1; if (source_y >= 0 && source_y < tiny_depth_height && source_x >= 0 && source_x < tiny_depth_width) { expected += tiny_depth_input[(std::size_t(channel) * tiny_depth_height + source_y) * tiny_depth_width + source_x] * tiny_depth_weights[(std::size_t(channel) * tiny_depth_kernel + ky) * tiny_depth_kernel + kx]; } } const auto index = (std::size_t(channel) * tiny_depth_height + y) * tiny_depth_width + x; if (std::abs(tiny_depth_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena tiny-plane depthwise mismatch at " << index << '\n'; return 1; } } arena.Release(tiny_depth_output_slot); arena.Release(tiny_depth_bias_slot); arena.Release(tiny_depth_weights_slot); arena.Release(tiny_depth_input_slot); // The isolated scalar module maps one invocation to one output value. // Its workload must therefore be ceil(N*C*H*W / local_size_x), rather // than the generic vec4 launch used by the main mega-shader. Exercise // the complete 896-channel map so a partial-tail regression cannot pass // merely because the zero-initialised tail matches a zero reference. constexpr int isolated_tail_channels = 896; constexpr int isolated_tail_height = 5; constexpr int isolated_tail_width = 22; constexpr int isolated_tail_kernel = 3; const std::size_t isolated_tail_plane = std::size_t(isolated_tail_height) * isolated_tail_width; std::vector isolated_tail_input(isolated_tail_channels * isolated_tail_plane), isolated_tail_weights(std::size_t(isolated_tail_channels) * isolated_tail_kernel * isolated_tail_kernel), isolated_tail_bias(isolated_tail_channels), isolated_tail_output(isolated_tail_channels * isolated_tail_plane); for (std::size_t index = 0; index < isolated_tail_input.size(); ++index) isolated_tail_input[index] = float(int(index % 29) - 14) * .03125F; for (std::size_t index = 0; index < isolated_tail_weights.size(); ++index) isolated_tail_weights[index] = float(int(index % 17) - 8) * .015625F; for (int channel = 0; channel < isolated_tail_channels; ++channel) isolated_tail_bias[channel] = float(channel % 11 - 5) * .0625F; auto isolated_tail_input_slot = arena.Acquire(isolated_tail_input.size(), "smoke-isolated-tail-input"); // Match ONNX graph residency: activations are transient while model // parameters are persistent suballocations of the model-constant arena. auto isolated_tail_weights_slot = arena.Acquire(isolated_tail_weights.size(), "smoke-isolated-tail-weights", true); auto isolated_tail_bias_slot = arena.Acquire(isolated_tail_bias.size(), "smoke-isolated-tail-bias", true); auto isolated_tail_output_slot = arena.Acquire(isolated_tail_output.size(), "smoke-isolated-tail-output"); if (!arena.Upload(isolated_tail_input_slot, isolated_tail_input.data(), isolated_tail_input.size()) || !arena.Upload(isolated_tail_weights_slot, isolated_tail_weights.data(), isolated_tail_weights.size()) || !arena.Upload(isolated_tail_bias_slot, isolated_tail_bias.data(), isolated_tail_bias.size()) || !arena.BeginGraphRecording() || !arena.DepthwiseConvScalar(isolated_tail_input_slot, isolated_tail_weights_slot, isolated_tail_bias_slot, isolated_tail_output_slot, 1, isolated_tail_channels, isolated_tail_height, isolated_tail_width, isolated_tail_height, isolated_tail_width, isolated_tail_kernel, isolated_tail_kernel, 1, 1, 1, 1) || !arena.EndGraphRecording() || !arena.Download(isolated_tail_output.data(), isolated_tail_output_slot, isolated_tail_output.size())) { std::cerr << "Vulkan tensor arena isolated tail depthwise failed\n"; return 1; } for (int channel = 0; channel < isolated_tail_channels; ++channel) { const auto first = std::size_t(channel) * isolated_tail_plane; float expected = isolated_tail_bias[channel]; for (int ky = 0; ky < isolated_tail_kernel; ++ky) for (int kx = 0; kx < isolated_tail_kernel; ++kx) { const int source_y = ky - 1, source_x = kx - 1; if (source_y >= 0 && source_x >= 0 && source_y < isolated_tail_height && source_x < isolated_tail_width) expected += isolated_tail_input[first + std::size_t(source_y) * isolated_tail_width + source_x] * isolated_tail_weights[(std::size_t(channel) * isolated_tail_kernel + ky) * isolated_tail_kernel + kx]; } if (std::abs(isolated_tail_output[first] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena isolated tail depthwise mismatch at channel=" << channel << '\n'; return 1; } } arena.Release(isolated_tail_output_slot); arena.Release(isolated_tail_bias_slot); arena.Release(isolated_tail_weights_slot); arena.Release(isolated_tail_input_slot); // Exercise a detector-style ungrouped 3x3 convolution directly between // arena slots. The asymmetric plane and stride-two padded geometry cover // the generic kernel's coordinate arithmetic without an activation // download between producer and consumer. constexpr int conv_input_channels = 3; constexpr int conv_output_channels = 5; constexpr int conv_input_height = 7; constexpr int conv_input_width = 9; constexpr int conv_output_height = 4; constexpr int conv_output_width = 5; constexpr int conv_kernel_height = 3; constexpr int conv_kernel_width = 3; const std::size_t conv_input_plane = std::size_t(conv_input_height) * conv_input_width; const std::size_t conv_output_plane = std::size_t(conv_output_height) * conv_output_width; std::vector host_conv_input(conv_input_channels * conv_input_plane); std::vector host_conv_weights(std::size_t(conv_output_channels) * conv_input_channels * conv_kernel_height * conv_kernel_width); std::vector host_conv_bias(conv_output_channels), host_conv_output(conv_output_channels * conv_output_plane); for (std::size_t i = 0; i < host_conv_input.size(); ++i) host_conv_input[i] = float(i % 23) * .0625F - .75F; for (std::size_t i = 0; i < host_conv_weights.size(); ++i) host_conv_weights[i] = float(i % 17) * .03125F - .25F; for (int i = 0; i < conv_output_channels; ++i) host_conv_bias[i] = float(i - 2) * .125F; auto conv_input = arena.Acquire(host_conv_input.size(), "arena-conv-input"); auto conv_weights = arena.Acquire(host_conv_weights.size(), "arena-conv-weights"); auto conv_bias = arena.Acquire(host_conv_bias.size(), "arena-conv-bias"); auto conv_output = arena.Acquire(host_conv_output.size(), "arena-conv-output"); if (!conv_input.resident || !conv_weights.resident || !conv_bias.resident || !conv_output.resident || !arena.Upload(conv_input, host_conv_input.data(), host_conv_input.size()) || !arena.Upload(conv_weights, host_conv_weights.data(), host_conv_weights.size()) || !arena.Upload(conv_bias, host_conv_bias.data(), host_conv_bias.size()) || !arena.Conv2d(conv_input, conv_weights, conv_bias, conv_output, 1, conv_input_channels, conv_output_channels, conv_input_height, conv_input_width, conv_output_height, conv_output_width, conv_kernel_height, conv_kernel_width, 2, 2, 1, 1) || !arena.Download(host_conv_output.data(), conv_output, host_conv_output.size())) { std::cerr << "Vulkan tensor arena Conv2d failed\n"; return 1; } for (int output_channel = 0; output_channel < conv_output_channels; ++output_channel) for (int y = 0; y < conv_output_height; ++y) for (int x = 0; x < conv_output_width; ++x) { float expected = host_conv_bias[output_channel]; for (int input_channel = 0; input_channel < conv_input_channels; ++input_channel) for (int ky = 0; ky < conv_kernel_height; ++ky) for (int kx = 0; kx < conv_kernel_width; ++kx) { const int source_y = y * 2 + ky - 1, source_x = x * 2 + kx - 1; if (source_y >= 0 && source_y < conv_input_height && source_x >= 0 && source_x < conv_input_width) { expected += host_conv_input[(std::size_t(input_channel) * conv_input_height + source_y) * conv_input_width + source_x] * host_conv_weights[((std::size_t(output_channel) * conv_input_channels + input_channel) * conv_kernel_height + ky) * conv_kernel_width + kx]; } } const auto index = (std::size_t(output_channel) * conv_output_height + y) * conv_output_width + x; if (std::abs(host_conv_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena Conv2d mismatch at " << index << '\n'; return 1; } } arena.Release(conv_output); arena.Release(conv_bias); arena.Release(conv_weights); arena.Release(conv_input); // Detector Conv.0-class spatial tile: 3x3 s2 pad1, plane at the 4096 // admission floor, plus an odd 17-channel / 65x65 tail so the 16x16 // workgroup remainder is covered. Reference is ONNX's scalar IC-ky-kx // reduction, not the production CPU kernel. { struct SpatialCase { int ic, oc, ih, iw, oh, ow; bool relu; }; const SpatialCase spatial_cases[] = { {5, 17, 127, 127, 64, 64, true}, {3, 16, 129, 129, 65, 65, false}, }; for (const auto& spatial : spatial_cases) { const std::size_t in_plane = std::size_t(spatial.ih) * spatial.iw; const std::size_t out_plane = std::size_t(spatial.oh) * spatial.ow; std::vector host_in(std::size_t(spatial.ic) * in_plane), host_w(std::size_t(spatial.oc) * spatial.ic * 9), host_b(spatial.oc), host_out(std::size_t(spatial.oc) * out_plane); for (std::size_t i = 0; i < host_in.size(); ++i) host_in[i] = float(int(i % 29) - 14) * .03125F; for (std::size_t i = 0; i < host_w.size(); ++i) host_w[i] = float(int(i % 17) - 8) * .015625F; for (int i = 0; i < spatial.oc; ++i) host_b[i] = float(i - 3) * .0625F; auto slot_in = arena.Acquire(host_in.size(), "arena-spatial-in"); auto slot_w = arena.Acquire(host_w.size(), "arena-spatial-w"); auto slot_b = arena.Acquire(host_b.size(), "arena-spatial-b"); auto slot_out = arena.Acquire(host_out.size(), "arena-spatial-out"); if (!slot_in.resident || !slot_w.resident || !slot_b.resident || !slot_out.resident || !arena.Upload(slot_in, host_in.data(), host_in.size()) || !arena.Upload(slot_w, host_w.data(), host_w.size()) || !arena.Upload(slot_b, host_b.data(), host_b.size()) || !arena.Conv2d(slot_in, slot_w, slot_b, slot_out, 1, spatial.ic, spatial.oc, spatial.ih, spatial.iw, spatial.oh, spatial.ow, 3, 3, 2, 2, 1, 1, spatial.relu) || !arena.Download(host_out.data(), slot_out, host_out.size())) { std::cerr << "Vulkan tensor arena spatial Conv2d failed\n"; return 1; } for (int oc = 0; oc < spatial.oc; ++oc) for (int y = 0; y < spatial.oh; ++y) for (int x = 0; x < spatial.ow; ++x) { float expected = host_b[oc]; for (int ic = 0; ic < spatial.ic; ++ic) for (int ky = 0; ky < 3; ++ky) for (int kx = 0; kx < 3; ++kx) { const int iy = y * 2 + ky - 1, ix = x * 2 + kx - 1; if (iy >= 0 && iy < spatial.ih && ix >= 0 && ix < spatial.iw) { expected += host_in[(std::size_t(ic) * spatial.ih + iy) * spatial.iw + ix] * host_w[((std::size_t(oc) * spatial.ic + ic) * 3 + ky) * 3 + kx]; } } if (spatial.relu) expected = std::max(expected, 0.F); const auto index = (std::size_t(oc) * spatial.oh + y) * spatial.ow + x; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(host_out[index] - expected) > tolerance) { std::cerr << "Vulkan tensor arena spatial Conv2d mismatch oc=" << oc << " y=" << y << " x=" << x << '\n'; return 1; } } arena.Release(slot_out); arena.Release(slot_b); arena.Release(slot_w); arena.Release(slot_in); } } // Expanded spatial 3x3: Concat.2-class C=64 s1 and stem-class C=32 s2, // plus an odd s1 tail so 16x16 remainders and the 12-IC LDS pass are // compared to the scalar IC-ky-kx reduction, not the CPU kernel. { struct WideSpatial { int ic, oc, ih, iw, oh, ow, stride; bool relu; const char* name; }; const WideSpatial wide_cases[] = { {64, 16, 40, 176, 40, 176, 1, true, "concat2-c64-s1"}, {32, 16, 80, 352, 40, 176, 2, true, "stem-c32-s2"}, {17, 5, 65, 65, 65, 65, 1, false, "odd-c17-s1"}, {17, 5, 33, 33, 33, 33, 1, false, "generic-c17-s1"}, }; for (const auto& spatial : wide_cases) { const std::size_t in_plane = std::size_t(spatial.ih) * spatial.iw; const std::size_t out_plane = std::size_t(spatial.oh) * spatial.ow; std::vector host_in(std::size_t(spatial.ic) * in_plane), host_w(std::size_t(spatial.oc) * spatial.ic * 9), host_b(spatial.oc), host_out(std::size_t(spatial.oc) * out_plane); for (std::size_t i = 0; i < host_in.size(); ++i) host_in[i] = float(int(i % 29) - 14) * .03125F; for (std::size_t i = 0; i < host_w.size(); ++i) host_w[i] = float(int(i % 17) - 8) * .015625F; for (int i = 0; i < spatial.oc; ++i) host_b[i] = float(i - 3) * .0625F; auto slot_in = arena.Acquire(host_in.size(), "arena-wide-spatial-in"); auto slot_w = arena.Acquire(host_w.size(), "arena-wide-spatial-w"); auto slot_b = arena.Acquire(host_b.size(), "arena-wide-spatial-b"); auto slot_out = arena.Acquire(host_out.size(), "arena-wide-spatial-out"); if (!slot_in.resident || !slot_w.resident || !slot_b.resident || !slot_out.resident || !arena.Upload(slot_in, host_in.data(), host_in.size()) || !arena.Upload(slot_w, host_w.data(), host_w.size()) || !arena.Upload(slot_b, host_b.data(), host_b.size()) || !arena.Conv2d(slot_in, slot_w, slot_b, slot_out, 1, spatial.ic, spatial.oc, spatial.ih, spatial.iw, spatial.oh, spatial.ow, 3, 3, spatial.stride, spatial.stride, 1, 1, spatial.relu) || !arena.Download(host_out.data(), slot_out, host_out.size())) { std::cerr << "Vulkan tensor arena wide spatial Conv2d failed (" << spatial.name << ")\n"; return 1; } for (int oc = 0; oc < spatial.oc; ++oc) for (int y = 0; y < spatial.oh; ++y) for (int x = 0; x < spatial.ow; ++x) { float expected = host_b[oc]; for (int ic = 0; ic < spatial.ic; ++ic) for (int ky = 0; ky < 3; ++ky) for (int kx = 0; kx < 3; ++kx) { const int iy = y * spatial.stride - 1 + ky; const int ix = x * spatial.stride - 1 + kx; if (iy >= 0 && iy < spatial.ih && ix >= 0 && ix < spatial.iw) { expected += host_in[(std::size_t(ic) * spatial.ih + iy) * spatial.iw + ix] * host_w[((std::size_t(oc) * spatial.ic + ic) * 3 + ky) * 3 + kx]; } } if (spatial.relu) expected = std::max(expected, 0.F); const auto index = (std::size_t(oc) * spatial.oh + y) * spatial.ow + x; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(host_out[index] - expected) > tolerance) { std::cerr << "Vulkan tensor arena wide spatial Conv2d mismatch (" << spatial.name << ") oc=" << oc << " y=" << y << " x=" << x << '\n'; return 1; } } arena.Release(slot_out); arena.Release(slot_b); arena.Release(slot_w); arena.Release(slot_in); } } // Device-resident FPN path: ConvTranspose output becomes the source of a // nearest-resize/add. The two dispatches must communicate only through // arena storage; the sole download is the final smoke-test observation. constexpr int up_input_channels = 2, up_output_channels = 3, up_height = 3, up_width = 4; const std::size_t up_input_plane = std::size_t(up_height) * up_width; const std::size_t up_output_plane = up_input_plane * 4; std::vector host_up_input(up_input_channels * up_input_plane); std::vector host_up_weights(std::size_t(up_input_channels) * up_output_channels * 4); std::vector host_up_bias(up_output_channels), host_up_output(up_output_channels * up_output_plane); for (std::size_t i = 0; i < host_up_input.size(); ++i) host_up_input[i] = float(i) * .125F - .5F; for (std::size_t i = 0; i < host_up_weights.size(); ++i) host_up_weights[i] = float(i % 11) * .0625F - .25F; for (int i = 0; i < up_output_channels; ++i) host_up_bias[i] = float(i - 1) * .125F; auto up_input = arena.Acquire(host_up_input.size(), "arena-up-input"); auto up_weights = arena.Acquire(host_up_weights.size(), "arena-up-weights"); auto up_bias = arena.Acquire(host_up_bias.size(), "arena-up-bias"); auto up_output = arena.Acquire(host_up_output.size(), "arena-up-output"); if (!arena.Upload(up_input, host_up_input.data(), host_up_input.size()) || !arena.Upload(up_weights, host_up_weights.data(), host_up_weights.size()) || !arena.Upload(up_bias, host_up_bias.data(), host_up_bias.size()) || !arena.ConvTranspose2x2(up_input, up_weights, up_bias, up_output, 1, up_input_channels, up_output_channels, up_height, up_width) || !arena.Download(host_up_output.data(), up_output, host_up_output.size())) { std::cerr << "Vulkan tensor arena ConvTranspose2x2 failed\n"; return 1; } for (int output_channel = 0; output_channel < up_output_channels; ++output_channel) for (int y = 0; y < up_height * 2; ++y) for (int x = 0; x < up_width * 2; ++x) { float expected = host_up_bias[output_channel]; const int source_y = y / 2, source_x = x / 2, tap = (y % 2) * 2 + x % 2; for (int input_channel = 0; input_channel < up_input_channels; ++input_channel) expected += host_up_input[(std::size_t(input_channel) * up_height + source_y) * up_width + source_x] * host_up_weights[(input_channel * up_output_channels + output_channel) * 4 + tap]; const auto index = (std::size_t(output_channel) * up_height * 2 + y) * up_width * 2 + x; if (std::abs(host_up_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena ConvTranspose2x2 mismatch at " << index << '\n'; return 1; } } arena.Release(up_output); arena.Release(up_bias); arena.Release(up_weights); arena.Release(up_input); constexpr int chain_cin = 4, chain_mid = 5, chain_cout = 2, chain_h = 5, chain_w = 7; const std::size_t chain_in_n = std::size_t(chain_cin) * chain_h * chain_w; const std::size_t chain_mid_n = std::size_t(chain_mid) * chain_h * 2 * chain_w * 2; const std::size_t chain_out_n = std::size_t(chain_cout) * chain_h * 4 * chain_w * 4; std::vector host_chain_in(chain_in_n), host_w0(std::size_t(chain_cin) * chain_mid * 4); std::vector host_b0(chain_mid), host_w1(std::size_t(chain_mid) * chain_cout * 4); std::vector host_b1(chain_cout), host_packed(host_w1.size() + host_b1.size()); std::vector host_mid(chain_mid_n), host_chain_cpu(chain_out_n), host_chain_gpu(chain_out_n); for (std::size_t i = 0; i < host_chain_in.size(); ++i) host_chain_in[i] = float(int(i % 41) - 20) * .03125F; for (std::size_t i = 0; i < host_w0.size(); ++i) host_w0[i] = float(int(i % 23) - 11) * .03125F; for (int i = 0; i < chain_mid; ++i) host_b0[static_cast(i)] = float(int(i % 13) - 6) * .0625F; for (std::size_t i = 0; i < host_w1.size(); ++i) host_w1[i] = float(int(i % 19) - 9) * .03125F; for (int i = 0; i < chain_cout; ++i) host_b1[static_cast(i)] = float(int(i % 11) - 5) * .0625F; std::memcpy(host_packed.data(), host_w1.data(), host_w1.size() * sizeof(float)); std::memcpy(host_packed.data() + host_w1.size(), host_b1.data(), host_b1.size() * sizeof(float)); ppocr::detail::kernels::ConvTranspose2x2(host_mid.data(), host_chain_in.data(), host_w0.data(), host_b0.data(), chain_cin, chain_mid, chain_h, chain_w, 1); ppocr::detail::kernels::ConvTranspose2x2(host_chain_cpu.data(), host_mid.data(), host_w1.data(), host_b1.data(), chain_mid, chain_cout, chain_h * 2, chain_w * 2, 2); auto chain_in = arena.Acquire(host_chain_in.size(), "arena-ct-chain-in"); auto chain_w0 = arena.Acquire(host_w0.size(), "arena-ct-chain-w0"); auto chain_b0 = arena.Acquire(host_b0.size(), "arena-ct-chain-b0"); auto chain_pk = arena.Acquire(host_packed.size(), "arena-ct-chain-pk"); auto chain_out = arena.Acquire(host_chain_gpu.size(), "arena-ct-chain-out"); if (!arena.Upload(chain_in, host_chain_in.data(), host_chain_in.size()) || !arena.Upload(chain_w0, host_w0.data(), host_w0.size()) || !arena.Upload(chain_b0, host_b0.data(), host_b0.size()) || !arena.Upload(chain_pk, host_packed.data(), host_packed.size()) || !arena.ConvTranspose2x2Chain(chain_in, chain_w0, chain_b0, chain_pk, chain_out, 1, chain_cin, chain_mid, chain_cout, chain_h, chain_w) || !arena.Download(host_chain_gpu.data(), chain_out, host_chain_gpu.size())) { std::cerr << "Vulkan tensor arena ConvTranspose2x2Chain failed\n"; return 1; } for (std::size_t index = 0; index < host_chain_cpu.size(); ++index) { const float tolerance = 4e-5F * std::max(1.F, std::abs(host_chain_cpu[index])); if (std::abs(host_chain_gpu[index] - host_chain_cpu[index]) > tolerance) { std::cerr << "Vulkan tensor arena ConvTranspose2x2Chain mismatch at " << index << '\n'; return 1; } } arena.Release(chain_out); arena.Release(chain_pk); arena.Release(chain_b0); arena.Release(chain_w0); arena.Release(chain_in); constexpr int resize_channels = 2, resize_height = 3, resize_width = 5; const std::size_t resize_input_plane = std::size_t(resize_height) * resize_width; const std::size_t resize_output_plane = resize_input_plane * 4; std::vector host_resize_source(resize_channels * resize_input_plane); std::vector host_resize_residual(resize_channels * resize_output_plane), host_resize_output(resize_channels * resize_output_plane); for (std::size_t i = 0; i < host_resize_source.size(); ++i) host_resize_source[i] = float(i) * .1F - .5F; for (std::size_t i = 0; i < host_resize_residual.size(); ++i) host_resize_residual[i] = float(i % 19) * .05F - .4F; auto resize_source = arena.Acquire(host_resize_source.size(), "arena-resize-source"); auto resize_residual = arena.Acquire(host_resize_residual.size(), "arena-resize-residual"); auto resize_output = arena.Acquire(host_resize_output.size(), "arena-resize-output"); if (!arena.Upload(resize_source, host_resize_source.data(), host_resize_source.size()) || !arena.Upload(resize_residual, host_resize_residual.data(), host_resize_residual.size()) || !arena.NearestResize2xAdd(resize_source, resize_residual, resize_output, 1, resize_channels, resize_height, resize_width) || !arena.Download(host_resize_output.data(), resize_output, host_resize_output.size())) { std::cerr << "Vulkan tensor arena NearestResize2xAdd failed\n"; return 1; } for (int channel = 0; channel < resize_channels; ++channel) for (int y = 0; y < resize_height * 2; ++y) for (int x = 0; x < resize_width * 2; ++x) { const auto output_index = (std::size_t(channel) * resize_height * 2 + y) * resize_width * 2 + x; const float expected = host_resize_source[(std::size_t(channel) * resize_height + y / 2) * resize_width + x / 2] + host_resize_residual[output_index]; if (std::abs(host_resize_output[output_index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena NearestResize2xAdd mismatch at " << output_index << '\n'; return 1; } } arena.Release(resize_output); arena.Release(resize_residual); arena.Release(resize_source); arena.Release(up_output); arena.Release(up_bias); arena.Release(up_weights); arena.Release(up_input); // Transformer/CTC projection primitive: GEMM remains resident, including // its bias and fused Swish result. Use a non-tile-aligned row/column tail. constexpr int gemm_rows = 7, gemm_depth = 11, gemm_columns = 13; std::vector host_gemm_left(gemm_rows * gemm_depth); std::vector host_gemm_right(gemm_depth * gemm_columns); std::vector host_gemm_bias(gemm_columns), host_gemm_output(gemm_rows * gemm_columns); for (std::size_t i = 0; i < host_gemm_left.size(); ++i) host_gemm_left[i] = float(i % 29) * .0625F - .75F; for (std::size_t i = 0; i < host_gemm_right.size(); ++i) host_gemm_right[i] = float(i % 17) * .03125F - .25F; for (int i = 0; i < gemm_columns; ++i) host_gemm_bias[i] = float(i - 6) * .0625F; auto gemm_left = arena.Acquire(host_gemm_left.size(), "arena-gemm-left"); auto gemm_right = arena.Acquire(host_gemm_right.size(), "arena-gemm-right"); auto gemm_bias = arena.Acquire(host_gemm_bias.size(), "arena-gemm-bias"); auto gemm_output = arena.Acquire(host_gemm_output.size(), "arena-gemm-output"); if (!arena.Upload(gemm_left, host_gemm_left.data(), host_gemm_left.size()) || !arena.Upload(gemm_right, host_gemm_right.data(), host_gemm_right.size()) || !arena.Upload(gemm_bias, host_gemm_bias.data(), host_gemm_bias.size()) || !arena.Gemm(gemm_left, gemm_right, &gemm_bias, gemm_output, gemm_rows, gemm_depth, gemm_columns, true) || !arena.Download(host_gemm_output.data(), gemm_output, host_gemm_output.size())) { std::cerr << "Vulkan tensor arena GEMM failed\n"; return 1; } for (int row = 0; row < gemm_rows; ++row) for (int column = 0; column < gemm_columns; ++column) { float expected = host_gemm_bias[column]; for (int k = 0; k < gemm_depth; ++k) expected += host_gemm_left[row * gemm_depth + k] * host_gemm_right[k * gemm_columns + column]; expected *= 1.F / (1.F + std::exp(-expected)); const auto index = std::size_t(row) * gemm_columns + column; if (std::abs(host_gemm_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena GEMM mismatch at " << index << '\n'; return 1; } } arena.Release(gemm_output); arena.Release(gemm_bias); arena.Release(gemm_right); arena.Release(gemm_left); // Transposed-A GEMM (skipped rec Transpose): physical A is [depth, rows]. // Cover the generic path and the 8x128 tiled path used by tiny CTC. { constexpr int t_rows = 16, t_depth = 32, t_cols = 128; std::vector host_at(std::size_t(t_depth) * t_rows); std::vector host_b(std::size_t(t_depth) * t_cols); std::vector host_bias(t_cols), host_out(std::size_t(t_rows) * t_cols); for (std::size_t i = 0; i < host_at.size(); ++i) host_at[i] = float(int(i % 29) - 14) * .0625F; for (std::size_t i = 0; i < host_b.size(); ++i) host_b[i] = float(int(i % 17) - 8) * .03125F; for (int i = 0; i < t_cols; ++i) host_bias[i] = float(i - 8) * .015625F; auto at = arena.Acquire(host_at.size(), "arena-gemm-at"); auto bt = arena.Acquire(host_b.size(), "arena-gemm-bt"); auto ct = arena.Acquire(host_bias.size(), "arena-gemm-ct"); auto dt = arena.Acquire(host_out.size(), "arena-gemm-dt"); if (!arena.Upload(at, host_at.data(), host_at.size()) || !arena.Upload(bt, host_b.data(), host_b.size()) || !arena.Upload(ct, host_bias.data(), host_bias.size()) || !arena.Gemm(at, bt, &ct, dt, t_rows, t_depth, t_cols, false, t_rows) || !arena.Download(host_out.data(), dt, host_out.size())) { std::cerr << "Vulkan tensor arena transposed GEMM failed\n"; return 1; } for (int row = 0; row < t_rows; ++row) for (int column = 0; column < t_cols; ++column) { float expected = host_bias[column]; for (int k = 0; k < t_depth; ++k) expected += host_at[std::size_t(k) * t_rows + row] * host_b[std::size_t(k) * t_cols + column]; const auto index = std::size_t(row) * t_cols + column; if (std::abs(host_out[index] - expected) > 4e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena transposed GEMM mismatch at " << index << '\n'; return 1; } } arena.Release(dt); arena.Release(ct); arena.Release(bt); arena.Release(at); } { constexpr int t_rows = 7, t_depth = 11, t_cols = 13; std::vector host_at(std::size_t(t_depth) * t_rows); std::vector host_b(std::size_t(t_depth) * t_cols); std::vector host_bias(t_cols), host_out(std::size_t(t_rows) * t_cols); for (std::size_t i = 0; i < host_at.size(); ++i) host_at[i] = float(int(i % 23) - 11) * .125F; for (std::size_t i = 0; i < host_b.size(); ++i) host_b[i] = float(int(i % 19) - 9) * .0625F; for (int i = 0; i < t_cols; ++i) host_bias[i] = float(i - 6) * .03125F; auto at = arena.Acquire(host_at.size(), "arena-gemm-at-odd"); auto bt = arena.Acquire(host_b.size(), "arena-gemm-bt-odd"); auto ct = arena.Acquire(host_bias.size(), "arena-gemm-ct-odd"); auto dt = arena.Acquire(host_out.size(), "arena-gemm-dt-odd"); if (!arena.Upload(at, host_at.data(), host_at.size()) || !arena.Upload(bt, host_b.data(), host_b.size()) || !arena.Upload(ct, host_bias.data(), host_bias.size()) || !arena.Gemm(at, bt, &ct, dt, t_rows, t_depth, t_cols, false, t_rows) || !arena.Download(host_out.data(), dt, host_out.size())) { std::cerr << "Vulkan tensor arena transposed GEMM odd failed\n"; return 1; } for (int row = 0; row < t_rows; ++row) for (int column = 0; column < t_cols; ++column) { float expected = host_bias[column]; for (int k = 0; k < t_depth; ++k) expected += host_at[std::size_t(k) * t_rows + row] * host_b[std::size_t(k) * t_cols + column]; const auto index = std::size_t(row) * t_cols + column; if (std::abs(host_out[index] - expected) > 4e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena transposed GEMM odd mismatch at " << index << '\n'; return 1; } } arena.Release(dt); arena.Release(ct); arena.Release(bt); arena.Release(at); } // Attention rows: validate both exact scalar-order device LayerNorm and // Softmax against their CPU mathematical references, including odd tails. constexpr int row_count = 5, row_width = 11; std::vector host_rows(row_count * row_width), host_gamma(row_width), host_beta(row_width), host_rows_output(host_rows.size()), host_softmax_output(host_rows.size()); for (std::size_t i = 0; i < host_rows.size(); ++i) host_rows[i] = float(i % 23) * .0625F - .625F; for (int i = 0; i < row_width; ++i) { host_gamma[i] = .5F + float(i) * .03125F; host_beta[i] = float(i - 5) * .0625F; } auto row_value = arena.Acquire(host_rows.size(), "arena-row-value"); auto row_gamma = arena.Acquire(host_gamma.size(), "arena-row-gamma"); auto row_beta = arena.Acquire(host_beta.size(), "arena-row-beta"); if (!arena.Upload(row_value, host_rows.data(), host_rows.size()) || !arena.Upload(row_gamma, host_gamma.data(), host_gamma.size()) || !arena.Upload(row_beta, host_beta.data(), host_beta.size()) || !arena.LayerNormInplace(row_value, row_gamma, row_beta, row_count, row_width, 1e-5F) || !arena.Download(host_rows_output.data(), row_value, host_rows_output.size())) { std::cerr << "Vulkan tensor arena LayerNorm failed\n"; return 1; } for (int row = 0; row < row_count; ++row) { float mean = 0.F; for (int column = 0; column < row_width; ++column) mean += host_rows[row * row_width + column]; mean /= float(row_width); float variance = 0.F; for (int column = 0; column < row_width; ++column) { const float v = host_rows[row * row_width + column] - mean; variance += v * v; } const float denom = std::sqrt(variance / float(row_width) + 1e-5F); for (int column = 0; column < row_width; ++column) { const auto index = std::size_t(row) * row_width + column; const float expected = ((host_rows[index] - mean) / denom) * host_gamma[column] + host_beta[column]; if (std::abs(host_rows_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena LayerNorm mismatch at " << index << '\n'; return 1; } } } if (!arena.Upload(row_value, host_rows.data(), host_rows.size()) || !arena.SoftmaxRowsInplace(row_value, row_count, row_width) || !arena.Download(host_softmax_output.data(), row_value, host_softmax_output.size())) { std::cerr << "Vulkan tensor arena Softmax failed\n"; return 1; } for (int row = 0; row < row_count; ++row) { float maximum = host_rows[row * row_width]; for (int column = 1; column < row_width; ++column) maximum = std::max(maximum, host_rows[row * row_width + column]); float sum{}; for (int column = 0; column < row_width; ++column) sum += std::exp(host_rows[row * row_width + column] - maximum); for (int column = 0; column < row_width; ++column) { const auto index = std::size_t(row) * row_width + column; const float expected = std::exp(host_rows[index] - maximum) / sum; if (std::abs(host_softmax_output[index] - expected) > 3e-5F) { std::cerr << "Vulkan tensor arena Softmax mismatch at " << index << '\n'; return 1; } } } arena.Release(row_beta); arena.Release(row_gamma); arena.Release(row_value); // Compact one-thread-per-row LayerNorm/Softmax: representative attention // shape (17x64) plus the 5x11 odd tail above. Reference is sequential // CPU mean/var and max/sum, not a second copy of the shader. { constexpr int compact_rows = 17, compact_width = 64; std::vector host(compact_rows * compact_width), gamma(compact_width), beta(compact_width), ln_out(host.size()), sm_out(host.size()); for (std::size_t i = 0; i < host.size(); ++i) host[i] = float(int(i % 29) - 14) * .03125F; for (int i = 0; i < compact_width; ++i) { gamma[i] = .75F + float(i) * .015625F; beta[i] = float(i - 32) * .03125F; } auto value = arena.Acquire(host.size(), "arena-compact-ln"); auto gslot = arena.Acquire(gamma.size(), "arena-compact-gamma"); auto bslot = arena.Acquire(beta.size(), "arena-compact-beta"); if (!arena.Upload(value, host.data(), host.size()) || !arena.Upload(gslot, gamma.data(), gamma.size()) || !arena.Upload(bslot, beta.data(), beta.size()) || !arena.LayerNormInplace(value, gslot, bslot, compact_rows, compact_width, 1e-5F) || !arena.Download(ln_out.data(), value, ln_out.size())) { std::cerr << "Vulkan tensor arena compact LayerNorm failed\n"; return 1; } for (int row = 0; row < compact_rows; ++row) { float mean = 0.F; for (int column = 0; column < compact_width; ++column) mean += host[row * compact_width + column]; mean /= float(compact_width); float variance = 0.F; for (int column = 0; column < compact_width; ++column) { const float v = host[row * compact_width + column] - mean; variance += v * v; } const float denom = std::sqrt(variance / float(compact_width) + 1e-5F); for (int column = 0; column < compact_width; ++column) { const auto index = std::size_t(row) * compact_width + column; const float expected = ((host[index] - mean) / denom) * gamma[column] + beta[column]; if (std::abs(ln_out[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena compact LayerNorm mismatch at " << index << '\n'; return 1; } } } if (!arena.Upload(value, host.data(), host.size()) || !arena.SoftmaxRowsInplace(value, compact_rows, compact_width) || !arena.Download(sm_out.data(), value, sm_out.size())) { std::cerr << "Vulkan tensor arena compact Softmax failed\n"; return 1; } for (int row = 0; row < compact_rows; ++row) { float maximum = host[row * compact_width]; for (int column = 1; column < compact_width; ++column) maximum = std::max(maximum, host[row * compact_width + column]); float sum{}; for (int column = 0; column < compact_width; ++column) sum += std::exp(host[row * compact_width + column] - maximum); for (int column = 0; column < compact_width; ++column) { const auto index = std::size_t(row) * compact_width + column; const float expected = std::exp(host[index] - maximum) / sum; if (std::abs(sm_out[index] - expected) > 3e-5F) { std::cerr << "Vulkan tensor arena compact Softmax mismatch at " << index << '\n'; return 1; } } } arena.Release(bslot); arena.Release(gslot); arena.Release(value); } // Rank-4 layout remap is used for recognizer NTC/NCT and attention heads. // Validate a nontrivial all-axis permutation with unequal dimensions. constexpr std::array transpose_dims{2, 3, 2, 5}; constexpr std::array transpose_perm{2, 0, 3, 1}; constexpr std::size_t transpose_elements = 2 * 3 * 2 * 5; std::vector host_transpose_input(transpose_elements), host_transpose_output(transpose_elements); for (std::size_t i = 0; i < host_transpose_input.size(); ++i) host_transpose_input[i] = float(i) * .125F - 2.F; auto transpose_input = arena.Acquire(transpose_elements, "arena-transpose-input"); auto transpose_output = arena.Acquire(transpose_elements, "arena-transpose-output"); if (!arena.Upload(transpose_input, host_transpose_input.data(), host_transpose_input.size()) || !arena.Transpose4D(transpose_input, transpose_output, transpose_dims, transpose_perm) || !arena.Download(host_transpose_output.data(), transpose_output, host_transpose_output.size())) { std::cerr << "Vulkan tensor arena Transpose4D failed\n"; return 1; } const std::array transpose_out_dims{transpose_dims[2], transpose_dims[0], transpose_dims[3], transpose_dims[1]}; for (int o0 = 0; o0 < transpose_out_dims[0]; ++o0) for (int o1 = 0; o1 < transpose_out_dims[1]; ++o1) for (int o2 = 0; o2 < transpose_out_dims[2]; ++o2) for (int o3 = 0; o3 < transpose_out_dims[3]; ++o3) { const int i0 = o1, i1 = o3, i2 = o0, i3 = o2; const auto input_index = ((std::size_t(i0) * 3 + i1) * 2 + i2) * 5 + i3; const auto output_index = ((std::size_t(o0) * transpose_out_dims[1] + o1) * transpose_out_dims[2] + o2) * transpose_out_dims[3] + o3; if (host_transpose_output[output_index] != host_transpose_input[input_index]) { std::cerr << "Vulkan tensor arena Transpose4D mismatch at " << output_index << '\n'; return 1; } } arena.Release(transpose_output); arena.Release(transpose_input); // Expand 1x1 GELU + project 1x1 + residual in one dispatch. Odd C/hidden // and an odd plane cover the 16-wide LDS strip remainder. Reference is // sequential IC then H reduction plus 0.5*x*(1+erf(x/sqrt(2))), not the // production CPU kernel. { constexpr int eg_channels = 5, eg_hidden = 7, eg_plane = 19; std::vector host_in(eg_channels * eg_plane), host_ew(eg_hidden * eg_channels), host_eb(eg_hidden), host_pw(eg_channels * eg_hidden), host_pb(eg_channels), host_out(eg_channels * eg_plane), packed_bias(eg_hidden + eg_channels); for (std::size_t i = 0; i < host_in.size(); ++i) host_in[i] = float(int(i % 17) - 8) * .0625F; for (std::size_t i = 0; i < host_ew.size(); ++i) host_ew[i] = float(int(i % 11) - 5) * .03125F; for (int i = 0; i < eg_hidden; ++i) host_eb[i] = float(i - 3) * .125F; for (std::size_t i = 0; i < host_pw.size(); ++i) host_pw[i] = float(int(i % 13) - 6) * .03125F; for (int i = 0; i < eg_channels; ++i) host_pb[i] = float(i - 2) * .0625F; std::copy(host_eb.begin(), host_eb.end(), packed_bias.begin()); std::copy(host_pb.begin(), host_pb.end(), packed_bias.begin() + eg_hidden); auto slot_in = arena.Acquire(host_in.size(), "arena-egp-in"); auto slot_ew = arena.Acquire(host_ew.size(), "arena-egp-ew"); auto slot_bias = arena.Acquire(packed_bias.size(), "arena-egp-bias"); auto slot_pw = arena.Acquire(host_pw.size(), "arena-egp-pw"); auto slot_out = arena.Acquire(host_out.size(), "arena-egp-out"); if (!slot_in.resident || !slot_ew.resident || !slot_bias.resident || !slot_pw.resident || !slot_out.resident || !arena.Upload(slot_in, host_in.data(), host_in.size()) || !arena.Upload(slot_ew, host_ew.data(), host_ew.size()) || !arena.Upload(slot_bias, packed_bias.data(), packed_bias.size()) || !arena.Upload(slot_pw, host_pw.data(), host_pw.size()) || !arena.ExpandGeluProjectAdd(slot_in, slot_ew, slot_bias, slot_pw, slot_out, 1, eg_channels, eg_hidden, eg_plane) || !arena.Download(host_out.data(), slot_out, host_out.size())) { std::cerr << "Vulkan tensor arena ExpandGeluProjectAdd failed\n"; return 1; } const float inv_sqrt2 = 0.7071067811865475F; for (int oc = 0; oc < eg_channels; ++oc) for (int sp = 0; sp < eg_plane; ++sp) { std::vector hidden(eg_hidden); for (int hc = 0; hc < eg_hidden; ++hc) { float acc = host_eb[hc]; for (int ic = 0; ic < eg_channels; ++ic) acc += host_in[ic * eg_plane + sp] * host_ew[hc * eg_channels + ic]; hidden[hc] = 0.5F * acc * (1.F + std::erf(acc * inv_sqrt2)); } float expected = host_pb[oc]; for (int hc = 0; hc < eg_hidden; ++hc) expected += hidden[hc] * host_pw[oc * eg_hidden + hc]; expected += host_in[oc * eg_plane + sp]; const auto index = std::size_t(oc) * eg_plane + sp; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(host_out[index] - expected) > tolerance) { std::cerr << "Vulkan tensor arena ExpandGeluProjectAdd mismatch oc=" << oc << " sp=" << sp << '\n'; return 1; } } arena.Release(slot_out); arena.Release(slot_pw); arena.Release(slot_bias); arena.Release(slot_ew); arena.Release(slot_in); } // Rec-sized 3x3 s1 pad1 DW + 1x1 PW, with and without exact GELU. Odd // C/M/H/W cover the 16-wide LDS strip remainder. Reference is sequential // depthwise then pointwise, not a second copy of the fused shader. { constexpr int dwpw_c = 5, dwpw_m = 7, dwpw_h = 9, dwpw_w = 11; const std::size_t plane = std::size_t(dwpw_h) * dwpw_w; std::vector host_in(dwpw_c * plane), host_dw(dwpw_c * 9), host_db(dwpw_c), host_pw(dwpw_m * dwpw_c), host_pb(dwpw_m), packed_bias(dwpw_c + dwpw_m), host_mid(dwpw_c * plane); for (std::size_t i = 0; i < host_in.size(); ++i) host_in[i] = float(int(i % 17) - 8) * .0625F; for (std::size_t i = 0; i < host_dw.size(); ++i) host_dw[i] = float(int(i % 11) - 5) * .03125F; for (int i = 0; i < dwpw_c; ++i) host_db[i] = float(i - 2) * .125F; for (std::size_t i = 0; i < host_pw.size(); ++i) host_pw[i] = float(int(i % 13) - 6) * .03125F; for (int i = 0; i < dwpw_m; ++i) host_pb[i] = float(i - 3) * .0625F; std::copy(host_db.begin(), host_db.end(), packed_bias.begin()); std::copy(host_pb.begin(), host_pb.end(), packed_bias.begin() + dwpw_c); ppocr::detail::kernels::DepthwiseConv( host_mid.data(), host_in.data(), host_dw.data(), host_db.data(), dwpw_c, dwpw_h, dwpw_w, dwpw_h, dwpw_w, 3, 3, 1, 1, 1, 1); for (int gelu = 0; gelu < 2; ++gelu) { std::vector host_expected(dwpw_m * plane), host_out(dwpw_m * plane); ppocr::detail::kernels::PointwiseConv( host_expected.data(), host_mid.data(), host_pw.data(), host_pb.data(), dwpw_m, dwpw_c, plane); if (gelu) { ppocr::detail::kernels::Gelu(host_expected.data(), host_expected.data(), host_expected.size(), host_expected.size()); } auto slot_in = arena.Acquire(host_in.size(), "arena-dwpw-in"); auto slot_dw = arena.Acquire(host_dw.size(), "arena-dwpw-dw"); auto slot_bias = arena.Acquire(packed_bias.size(), "arena-dwpw-bias"); auto slot_pw = arena.Acquire(host_pw.size(), "arena-dwpw-pw"); auto slot_out = arena.Acquire(host_out.size(), "arena-dwpw-out"); if (!slot_in.resident || !slot_dw.resident || !slot_bias.resident || !slot_pw.resident || !slot_out.resident || !arena.Upload(slot_in, host_in.data(), host_in.size()) || !arena.Upload(slot_dw, host_dw.data(), host_dw.size()) || !arena.Upload(slot_bias, packed_bias.data(), packed_bias.size()) || !arena.Upload(slot_pw, host_pw.data(), host_pw.size()) || !arena.DepthwisePointwiseFused(slot_in, slot_dw, slot_bias, slot_pw, slot_out, 1, dwpw_c, dwpw_m, dwpw_h, dwpw_w, gelu != 0) || !arena.Download(host_out.data(), slot_out, host_out.size())) { std::cerr << "Vulkan tensor arena DepthwisePointwiseFused failed gelu=" << gelu << '\n'; return 1; } for (std::size_t i = 0; i < host_out.size(); ++i) { const float tolerance = 3e-5F * std::max(1.F, std::abs(host_expected[i])); if (std::abs(host_out[i] - host_expected[i]) > tolerance) { std::cerr << "Vulkan tensor arena DepthwisePointwiseFused mismatch gelu=" << gelu << " index=" << i << " got=" << host_out[i] << " expected=" << host_expected[i] << '\n'; return 1; } } arena.Release(slot_out); arena.Release(slot_pw); arena.Release(slot_bias); arena.Release(slot_dw); arena.Release(slot_in); } } // SE/GlobalAveragePool reduction: multi-batch planes with an odd spatial // extent exercise the device-only NCHW mean used by all three model sizes. constexpr std::size_t mean_batches = 2; constexpr int mean_channels = 3; constexpr std::size_t mean_plane = 19; std::vector host_mean_input(mean_batches * mean_channels * mean_plane), host_mean_output(mean_batches * mean_channels); for (std::size_t i = 0; i < host_mean_input.size(); ++i) host_mean_input[i] = float(i % 31) * .0625F - .75F; auto mean_input = arena.Acquire(host_mean_input.size(), "arena-mean-input"); auto mean_output = arena.Acquire(host_mean_output.size(), "arena-mean-output"); if (!arena.Upload(mean_input, host_mean_input.data(), host_mean_input.size()) || !arena.SpatialMean(mean_input, mean_output, mean_batches, mean_channels, mean_plane) || !arena.Download(host_mean_output.data(), mean_output, host_mean_output.size())) { std::cerr << "Vulkan tensor arena SpatialMean failed\n"; return 1; } for (std::size_t item = 0; item < host_mean_output.size(); ++item) { float expected{}; for (std::size_t pixel = 0; pixel < mean_plane; ++pixel) expected += host_mean_input[item * mean_plane + pixel]; expected /= float(mean_plane); if (std::abs(host_mean_output[item] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena SpatialMean mismatch at " << item << '\n'; return 1; } } arena.Release(mean_output); arena.Release(mean_input); // Fused QKV split emits three independent attention layouts straight from // `[N,T,3,H,D]`, without materialising the old five-dimensional transpose. constexpr std::size_t qkv_batches = 2; constexpr int qkv_steps = 3, qkv_heads = 2, qkv_width = 4; constexpr std::size_t qkv_branch = qkv_batches * qkv_steps * qkv_heads * qkv_width; std::vector host_qkv_input(qkv_branch * 3), host_query(qkv_branch), host_key(qkv_branch), host_value(qkv_branch); for (std::size_t i = 0; i < host_qkv_input.size(); ++i) host_qkv_input[i] = float(i) * .03125F - 1.F; auto qkv_input = arena.Acquire(host_qkv_input.size(), "arena-qkv-input"); auto qkv_query = arena.Acquire(qkv_branch, "arena-qkv-query"); auto qkv_key = arena.Acquire(qkv_branch, "arena-qkv-key"); auto qkv_value = arena.Acquire(qkv_branch, "arena-qkv-value"); if (!arena.Upload(qkv_input, host_qkv_input.data(), host_qkv_input.size()) || !arena.QkvSplit(qkv_input, qkv_query, qkv_key, qkv_value, qkv_batches, qkv_steps, qkv_heads, qkv_width) || !arena.Download(host_query.data(), qkv_query, qkv_branch) || !arena.Download(host_key.data(), qkv_key, qkv_branch) || !arena.Download(host_value.data(), qkv_value, qkv_branch)) { std::cerr << "Vulkan tensor arena QKV split failed\n"; return 1; } for (std::size_t n = 0; n < qkv_batches; ++n) for (int t = 0; t < qkv_steps; ++t) for (int h = 0; h < qkv_heads; ++h) for (int d = 0; d < qkv_width; ++d) { const auto source = (((n * qkv_steps + t) * 3) * qkv_heads + h) * qkv_width + d; const auto output = ((n * qkv_heads + h) * qkv_steps + t) * qkv_width + d; if (host_query[output] != host_qkv_input[source] || host_key[output] != host_qkv_input[source + qkv_heads * qkv_width] || host_value[output] != host_qkv_input[source + 2 * qkv_heads * qkv_width]) { std::cerr << "Vulkan tensor arena QKV split mismatch at " << output << '\n'; return 1; } } arena.Release(qkv_value); arena.Release(qkv_key); arena.Release(qkv_query); arena.Release(qkv_input); // Attention GEMM: independent `[batch,head]` matrices share one device // submission; dimensions deliberately include a four-column tail. constexpr std::size_t attention_batches = 3; constexpr int attention_rows = 5, attention_depth = 7, attention_columns = 9; std::vector host_attention_left(attention_batches * attention_rows * attention_depth), host_attention_right(attention_batches * attention_depth * attention_columns), host_attention_output(attention_batches * attention_rows * attention_columns); for (std::size_t i = 0; i < host_attention_left.size(); ++i) host_attention_left[i] = float(i % 17) * .0625F - .5F; for (std::size_t i = 0; i < host_attention_right.size(); ++i) host_attention_right[i] = float(i % 13) * .03125F - .25F; auto attention_left = arena.Acquire(host_attention_left.size(), "arena-attention-left"); auto attention_right = arena.Acquire(host_attention_right.size(), "arena-attention-right"); auto attention_output = arena.Acquire(host_attention_output.size(), "arena-attention-output"); if (!arena.Upload(attention_left, host_attention_left.data(), host_attention_left.size()) || !arena.Upload(attention_right, host_attention_right.data(), host_attention_right.size()) || !arena.BatchedGemm(attention_left, attention_right, attention_output, attention_batches, attention_rows, attention_depth, attention_columns) || !arena.Download(host_attention_output.data(), attention_output, host_attention_output.size())) { std::cerr << "Vulkan tensor arena attention GEMM failed\n"; return 1; } for (std::size_t batch = 0; batch < attention_batches; ++batch) for (int row = 0; row < attention_rows; ++row) for (int column = 0; column < attention_columns; ++column) { float expected{}; for (int k = 0; k < attention_depth; ++k) expected += host_attention_left[(batch * attention_rows + row) * attention_depth + k] * host_attention_right[(batch * attention_depth + k) * attention_columns + column]; const auto index = (batch * attention_rows + row) * attention_columns + column; if (std::abs(host_attention_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena attention GEMM mismatch at " << index << '\n'; return 1; } } arena.Release(attention_output); arena.Release(attention_right); arena.Release(attention_left); // Tiled batched GEMM (mode 39 operation3=2): 8x128 LDS tile with odd // row/column tails and two matrices. Reference is the scalar K-order // reduction, not the CPU GEMM. { constexpr std::size_t tile_batches = 2; constexpr int tile_rows = 9, tile_depth = 17, tile_columns = 130; std::vector host_left(tile_batches * tile_rows * tile_depth), host_right(tile_batches * tile_depth * tile_columns), host_output(tile_batches * tile_rows * tile_columns); for (std::size_t i = 0; i < host_left.size(); ++i) host_left[i] = float(i % 19) * .0625F - .5F; for (std::size_t i = 0; i < host_right.size(); ++i) host_right[i] = float(i % 23) * .03125F - .25F; auto tile_left = arena.Acquire(host_left.size(), "arena-tile-gemm-left"); auto tile_right = arena.Acquire(host_right.size(), "arena-tile-gemm-right"); auto tile_output = arena.Acquire(host_output.size(), "arena-tile-gemm-output"); if (!arena.Upload(tile_left, host_left.data(), host_left.size()) || !arena.Upload(tile_right, host_right.data(), host_right.size()) || !arena.BatchedGemm(tile_left, tile_right, tile_output, tile_batches, tile_rows, tile_depth, tile_columns) || !arena.Download(host_output.data(), tile_output, host_output.size())) { std::cerr << "Vulkan tensor arena tiled batched GEMM failed\n"; return 1; } for (std::size_t batch = 0; batch < tile_batches; ++batch) for (int row = 0; row < tile_rows; ++row) for (int column = 0; column < tile_columns; ++column) { float expected{}; for (int k = 0; k < tile_depth; ++k) expected += host_left[(batch * tile_rows + row) * tile_depth + k] * host_right[(batch * tile_depth + k) * tile_columns + column]; const auto index = (batch * tile_rows + row) * tile_columns + column; if (std::abs(host_output[index] - expected) > 3e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena tiled batched GEMM mismatch at " << index << '\n'; return 1; } } arena.Release(tile_output); arena.Release(tile_right); arena.Release(tile_left); } // Backbone pool test: padded MaxPool and valid AveragePool use different // reduction semantics and both must retain NCHW device residency. constexpr int pool_channels = 2, pool_height = 5, pool_width = 7; constexpr int pool_output_height = 3, pool_output_width = 4; std::vector host_pool_input(pool_channels * pool_height * pool_width), host_pool_output(pool_channels * pool_output_height * pool_output_width); for (std::size_t i = 0; i < host_pool_input.size(); ++i) host_pool_input[i] = float(i % 29) * .0625F - .75F; auto pool_input = arena.Acquire(host_pool_input.size(), "arena-pool-input"); auto pool_output = arena.Acquire(host_pool_output.size(), "arena-pool-output"); if (!arena.Upload(pool_input, host_pool_input.data(), host_pool_input.size()) || !arena.Pool2d(pool_input, pool_output, 1, pool_channels, pool_height, pool_width, pool_output_height, pool_output_width, 3, 3, 2, 2, 1, 1, false) || !arena.Download(host_pool_output.data(), pool_output, host_pool_output.size())) { std::cerr << "Vulkan tensor arena MaxPool failed\n"; return 1; } for (int channel = 0; channel < pool_channels; ++channel) for (int y = 0; y < pool_output_height; ++y) for (int x = 0; x < pool_output_width; ++x) { float expected = -std::numeric_limits::infinity(); for (int ky = 0; ky < 3; ++ky) for (int kx = 0; kx < 3; ++kx) { const int source_y = y * 2 + ky - 1, source_x = x * 2 + kx - 1; if (source_y >= 0 && source_y < pool_height && source_x >= 0 && source_x < pool_width) expected = std::max(expected, host_pool_input[(std::size_t(channel) * pool_height + source_y) * pool_width + source_x]); } const auto index = (std::size_t(channel) * pool_output_height + y) * pool_output_width + x; if (std::abs(host_pool_output[index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena MaxPool mismatch at " << index << '\n'; return 1; } } constexpr int average_output_height = 1, average_output_width = 3; std::vector host_average_output(pool_channels * average_output_height * average_output_width); auto average_output = arena.Acquire(host_average_output.size(), "arena-average-output"); if (!arena.Pool2d(pool_input, average_output, 1, pool_channels, pool_height, pool_width, average_output_height, average_output_width, 3, 2, 3, 2, 0, 0, true) || !arena.Download(host_average_output.data(), average_output, host_average_output.size())) { std::cerr << "Vulkan tensor arena AveragePool failed\n"; return 1; } for (int channel = 0; channel < pool_channels; ++channel) for (int x = 0; x < average_output_width; ++x) { float expected{}; for (int ky = 0; ky < 3; ++ky) for (int kx = 0; kx < 2; ++kx) expected += host_pool_input[(std::size_t(channel) * pool_height + ky) * pool_width + x * 2 + kx]; expected /= 6.F; const auto index = std::size_t(channel) * average_output_width + x; if (std::abs(host_average_output[index] - expected) > 2e-5F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan tensor arena AveragePool mismatch at " << index << '\n'; return 1; } } { // Detector stem fusion: 2x2 SAME_UPPER MaxPool || peer -> 3x3 s2 Conv+ReLU. constexpr int stem_c0 = 4, stem_c1 = 8, stem_h = 17, stem_w = 15; constexpr int stem_m = 8, stem_kh = 3, stem_kw = 3, stem_oh = 9, stem_ow = 8; std::vector stem_a(stem_c0 * stem_h * stem_w), stem_b(stem_c1 * stem_h * stem_w); std::vector stem_weights(stem_m * (stem_c0 + stem_c1) * 9), stem_bias(stem_m); std::vector stem_gpu(stem_m * stem_oh * stem_ow), stem_cpu(stem_gpu.size()); for (std::size_t i = 0; i < stem_a.size(); ++i) stem_a[i] = float(int(i % 23) - 11) * .03125F; for (std::size_t i = 0; i < stem_b.size(); ++i) stem_b[i] = float(int(i % 19) - 9) * .03125F; for (std::size_t i = 0; i < stem_weights.size(); ++i) stem_weights[i] = float(int(i % 17) - 8) * .015625F; for (int i = 0; i < stem_m; ++i) stem_bias[std::size_t(i)] = float(i - 3) * .0625F; std::vector pooled(stem_a.size()); ppocr::detail::kernels::MaxPool2x2Same(pooled.data(), stem_a.data(), stem_c0, stem_h, stem_w); const float* sources[] = {pooled.data(), stem_b.data()}; const int source_channels[] = {stem_c0, stem_c1}; ppocr::detail::kernels::ConcatChannelConv2d( stem_cpu.data(), sources, source_channels, 2, stem_weights.data(), stem_bias.data(), stem_m, stem_h, stem_w, stem_oh, stem_ow, stem_kh, stem_kw, 2, 2, 0, 0, true); auto gpu_a = arena.Acquire(stem_a.size(), "stem-a"); auto gpu_b = arena.Acquire(stem_b.size(), "stem-b"); auto gpu_pooled = arena.Acquire(stem_a.size(), "stem-pooled"); auto gpu_concat = arena.Acquire(std::size_t(stem_c0 + stem_c1) * stem_h * stem_w, "stem-concat"); auto gpu_weights = arena.Acquire(stem_weights.size(), "stem-w"); auto gpu_bias = arena.Acquire(stem_bias.size(), "stem-bias"); auto gpu_out = arena.Acquire(stem_gpu.size(), "stem-out"); if (!arena.Upload(gpu_a, stem_a.data(), stem_a.size()) || !arena.Upload(gpu_b, stem_b.data(), stem_b.size()) || !arena.Upload(gpu_weights, stem_weights.data(), stem_weights.size()) || !arena.Upload(gpu_bias, stem_bias.data(), stem_bias.size()) || !arena.Pool2d(gpu_a, gpu_pooled, 1, stem_c0, stem_h, stem_w, stem_h, stem_w, 2, 2, 1, 1, 0, 0, false) || !arena.Concat({gpu_pooled, gpu_b}, gpu_concat, 1, std::size_t(stem_h) * stem_w, {std::size_t(stem_c0), std::size_t(stem_c1)}) || !arena.Conv2d(gpu_concat, gpu_weights, gpu_bias, gpu_out, 1, stem_c0 + stem_c1, stem_m, stem_h, stem_w, stem_oh, stem_ow, stem_kh, stem_kw, 2, 2, 0, 0, true) || !arena.Download(stem_gpu.data(), gpu_out, stem_gpu.size())) { std::cerr << "Vulkan fused MaxPool+Concat+Conv dispatch failed\n"; return 1; } for (std::size_t i = 0; i < stem_gpu.size(); ++i) { const float tolerance = 3e-5F * std::max(1.F, std::abs(stem_cpu[i])); if (std::abs(stem_gpu[i] - stem_cpu[i]) > tolerance) { std::cerr << "Vulkan fused MaxPool+Concat+Conv mismatch at " << i << '\n'; return 1; } } arena.Release(gpu_out); arena.Release(gpu_bias); arena.Release(gpu_weights); arena.Release(gpu_concat); arena.Release(gpu_pooled); arena.Release(gpu_b); arena.Release(gpu_a); } { // Recognizer inverted residual: 1x1 expand+GELU then 1x1 project+Add. constexpr int channels = 8, hidden = 16; constexpr std::size_t plane = 37; std::vector src(std::size_t(channels) * plane); std::vector expand_w(std::size_t(hidden) * channels); std::vector expand_b(hidden), project_b(channels); std::vector project_w(std::size_t(channels) * hidden); std::vector cpu(src.size()), gpu(src.size()); for (std::size_t i = 0; i < src.size(); ++i) src[i] = float(int(i % 29) - 14) * .03125F; for (std::size_t i = 0; i < expand_w.size(); ++i) expand_w[i] = float(int(i % 17) - 8) * .015625F; for (std::size_t i = 0; i < project_w.size(); ++i) project_w[i] = float(int(i % 13) - 6) * .015625F; for (int i = 0; i < hidden; ++i) expand_b[std::size_t(i)] = float(i - 7) * .0625F; for (int i = 0; i < channels; ++i) project_b[std::size_t(i)] = float(i - 3) * .0625F; ppocr::detail::kernels::ExpandGeluProjectAdd( cpu.data(), src.data(), expand_w.data(), expand_b.data(), project_w.data(), project_b.data(), channels, hidden, plane); auto g_src = arena.Acquire(src.size(), "ir-src"); auto g_ew = arena.Acquire(expand_w.size(), "ir-ew"); auto g_eb = arena.Acquire(expand_b.size(), "ir-eb"); auto g_pw = arena.Acquire(project_w.size(), "ir-pw"); auto g_pb = arena.Acquire(project_b.size(), "ir-pb"); auto g_hid = arena.Acquire(std::size_t(hidden) * plane, "ir-hid"); auto g_out = arena.Acquire(src.size(), "ir-out"); if (!arena.Upload(g_src, src.data(), src.size()) || !arena.Upload(g_ew, expand_w.data(), expand_w.size()) || !arena.Upload(g_eb, expand_b.data(), expand_b.size()) || !arena.Upload(g_pw, project_w.data(), project_w.size()) || !arena.Upload(g_pb, project_b.data(), project_b.size()) || !arena.PointwiseConv(g_src, g_ew, g_eb, g_hid, 1, channels, hidden, plane, false, false, true) || !arena.PointwiseConvAdd(g_hid, g_pw, g_pb, g_src, g_out, 1, hidden, channels, plane) || !arena.Download(gpu.data(), g_out, gpu.size())) { std::cerr << "Vulkan expand-GELU-project-add dispatch failed\n"; return 1; } for (std::size_t i = 0; i < gpu.size(); ++i) { const float tolerance = 2e-5F * std::max(1.F, std::abs(cpu[i])); if (std::abs(gpu[i] - cpu[i]) > tolerance) { std::cerr << "Vulkan expand-GELU-project-add mismatch at " << i << " got=" << gpu[i] << " expected=" << cpu[i] << '\n'; return 1; } } arena.Release(g_out); arena.Release(g_hid); arena.Release(g_pb); arena.Release(g_pw); arena.Release(g_eb); arena.Release(g_ew); arena.Release(g_src); } arena.Release(average_output); arena.Release(pool_output); arena.Release(pool_input); arena.Release(depth_output); arena.Release(depth_bias); arena.Release(depth_weights); arena.Release(depth_input); if (arena.live_bytes() != 0) { std::cerr << "Vulkan tensor arena release failed\n"; return 1; } } // Cover a scalar-sized dispatch, a non-workgroup-aligned tensor and a // recognizer-scale activation. The values deliberately include negatives // while keeping the divisor away from zero. for (const std::size_t count : {std::size_t{1}, std::size_t{4099}, std::size_t{65537}}) { std::vector left(count), right(count), output(count); for (std::size_t i = 0; i < count; ++i) { left[i] = float(i) * .125F - 17.F; right[i] = float(i % 31) * .25F + .75F; } for (const auto operation : {ppocr::detail::kernels::BinaryOp::add, ppocr::detail::kernels::BinaryOp::sub, ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::div}) { if (!ppocr::detail::VulkanBinary(output.data(), left.data(), right.data(), count, operation)) { std::cerr << "Vulkan binary dispatch failed, count=" << count << '\n'; return 1; } for (std::size_t i = 0; i < count; ++i) { float expected{}; switch (operation) { case ppocr::detail::kernels::BinaryOp::add: expected = left[i] + right[i]; break; case ppocr::detail::kernels::BinaryOp::sub: expected = left[i] - right[i]; break; case ppocr::detail::kernels::BinaryOp::mul: expected = left[i] * right[i]; break; case ppocr::detail::kernels::BinaryOp::div: expected = left[i] / right[i]; break; } const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan result mismatch at count=" << count << ", index=" << i << ", got=" << output[i] << ", expected=" << expected << '\n'; return 1; } } } } { constexpr std::size_t count = 65537; std::vector left(count), right(count), output(count); for (std::size_t i = 0; i < count; ++i) { left[i] = float(i) * .03125F - 5.F; right[i] = float(i % 23) * .0625F + .5F; } const std::vector chain{ ppocr::detail::kernels::BinaryOp::add, ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::sub, ppocr::detail::kernels::BinaryOp::div}; if (!ppocr::detail::VulkanBinaryChain(output.data(), left.data(), right.data(), count, chain)) { std::cerr << "Vulkan binary chain dispatch failed\n"; return 1; } for (std::size_t i = 0; i < count; ++i) { float expected = left[i]; for (const auto operation : chain) { switch (operation) { case ppocr::detail::kernels::BinaryOp::add: expected += right[i]; break; case ppocr::detail::kernels::BinaryOp::sub: expected -= right[i]; break; case ppocr::detail::kernels::BinaryOp::mul: expected *= right[i]; break; case ppocr::detail::kernels::BinaryOp::div: expected /= right[i]; break; } } const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan chain result mismatch at index=" << i << '\n'; return 1; } } } { // NCHW channel-gate broadcast: a compact [N,C,1,1] RHS is reused for // every H*W span and is intentionally not materialized on the device. constexpr std::size_t count = 8192; constexpr std::size_t batches = 4; constexpr std::size_t right_repeat = 64; constexpr std::size_t right_per_batch = count / right_repeat; constexpr std::size_t right_elements = right_per_batch * batches; std::vector left(count * batches), right(right_elements), output(count * batches); for (std::size_t i = 0; i < left.size(); ++i) left[i] = float(i % count) * .03125F - 4.F; for (std::size_t i = 0; i < right.size(); ++i) right[i] = float(i % 31) * .0625F + .75F; const std::vector chain{ ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::add}; if (!ppocr::detail::VulkanBinaryBroadcastRightChainBatch( output.data(), left.data(), right.data(), count, batches, right_repeat, right_elements, chain)) { std::cerr << "Vulkan broadcast batch dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { float expected = left[i]; const float gate = right[(i / count) * right_per_batch + (i % count) / right_repeat]; for (const auto operation : chain) { switch (operation) { case ppocr::detail::kernels::BinaryOp::add: expected += gate; break; case ppocr::detail::kernels::BinaryOp::sub: expected -= gate; break; case ppocr::detail::kernels::BinaryOp::mul: expected *= gate; break; case ppocr::detail::kernels::BinaryOp::div: expected /= gate; break; } } const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan broadcast batch result mismatch at index=" << i << '\n'; return 1; } } } { // The common model-constant variant has shape [1,C,1,1]. It must stay // one compact RHS even when the activation contains four crop batches; // duplicating it would needlessly grow host-to-GPU traffic with N. constexpr std::size_t count = 8192; constexpr std::size_t batches = 4; constexpr std::size_t right_repeat = 64; constexpr std::size_t right_per_batch = count / right_repeat; std::vector left0(count * batches), left1(count * batches); std::vector right(right_per_batch), output(count * batches); for (std::size_t i = 0; i < left0.size(); ++i) { left0[i] = float(i % count) * .015625F - 6.F; left1[i] = float(i % count) * .03125F + 2.F; } for (std::size_t i = 0; i < right.size(); ++i) right[i] = float(i % 29) * .0625F + .625F; const std::vector chain{ ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::add}; for (const float* left : {left0.data(), left1.data()}) { if (!ppocr::detail::VulkanBinaryBroadcastRightChainBatch( output.data(), left, right.data(), count, batches, right_repeat, right_per_batch, chain, true)) { std::cerr << "Vulkan shared-RHS broadcast batch dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { const float gate = right[(i % count) / right_repeat]; const float expected = left[i] * gate + gate; const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan shared-RHS broadcast batch result mismatch at index=" << i << '\n'; return 1; } } } } { // Recognizer projections are row-major GEMMs. Exercise an odd column tail // and a nontrivial output bias so the Vulkan kernel covers the same layout // consumed by the CTC logits head. constexpr int rows = 37; constexpr int depth = 29; constexpr int columns = 103; std::vector left(std::size_t(rows) * depth), right(std::size_t(depth) * columns), bias(columns), output(std::size_t(rows) * columns); for (std::size_t i = 0; i < left.size(); ++i) left[i] = float(i % 79) * .03125F - 1.F; for (std::size_t i = 0; i < right.size(); ++i) right[i] = float(i % 47) * .015625F - .375F; for (int i = 0; i < columns; ++i) bias[std::size_t(i)] = float(i % 17) * .0625F - .5F; if (!ppocr::detail::VulkanGemm(output.data(), left.data(), right.data(), bias.data(), rows, depth, columns, true)) { std::cerr << "Vulkan GEMM dispatch failed\n"; return 1; } for (int row = 0; row < rows; ++row) { for (int column = 0; column < columns; ++column) { float expected = bias[std::size_t(column)]; for (int k = 0; k < depth; ++k) { expected += left[std::size_t(row) * depth + k] * right[std::size_t(k) * columns + column]; } const auto index = std::size_t(row) * columns + column; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[index] - expected) > tolerance) { std::cerr << "Vulkan GEMM mismatch at " << index << '\n'; return 1; } } } } { // Exercise the cooperative 8x128 GEMM tile used for real batched // recognizer projections. Both dimensions deliberately have tails, so // the shader verifies that its shared-memory tile never reads past A/B // while still preserving the K-order scalar reference. constexpr int rows = 13; constexpr int depth = 37; constexpr int columns = 173; std::vector left(std::size_t(rows) * depth), right(std::size_t(depth) * columns), bias(columns), output(std::size_t(rows) * columns); for (std::size_t i = 0; i < left.size(); ++i) left[i] = float(i % 83) * .03125F - .875F; for (std::size_t i = 0; i < right.size(); ++i) right[i] = float(i % 53) * .015625F - .4375F; for (int i = 0; i < columns; ++i) bias[std::size_t(i)] = float(i % 19) * .0625F - .5F; if (!ppocr::detail::VulkanGemm(output.data(), left.data(), right.data(), bias.data(), rows, depth, columns, true)) { std::cerr << "Vulkan tiled GEMM dispatch failed\n"; return 1; } for (int row = 0; row < rows; ++row) { for (int column = 0; column < columns; ++column) { float expected = bias[std::size_t(column)]; for (int k = 0; k < depth; ++k) { expected += left[std::size_t(row) * depth + k] * right[std::size_t(k) * columns + column]; } const auto index = std::size_t(row) * columns + column; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[index] - expected) > tolerance) { std::cerr << "Vulkan tiled GEMM mismatch at " << index << '\n'; return 1; } } } } { // The recognizer MLP applies Swish immediately after its dense projection. // Exercise the fused writeback with a true flattened crop batch and an // odd tail, so no CPU activation traversal is needed after readback. constexpr int rows = 41; constexpr int depth = 29; constexpr int columns = 103; std::vector left(std::size_t(rows) * depth), right(std::size_t(depth) * columns), bias(columns), output(std::size_t(rows) * columns); for (std::size_t i = 0; i < left.size(); ++i) left[i] = float(i % 71) * .03125F - .875F; for (std::size_t i = 0; i < right.size(); ++i) right[i] = float(i % 43) * .015625F - .3125F; for (int i = 0; i < columns; ++i) bias[std::size_t(i)] = float(i % 13) * .0625F - .375F; if (!ppocr::detail::VulkanGemmSwish(output.data(), left.data(), right.data(), bias.data(), rows, depth, columns, true)) { std::cerr << "Vulkan GEMM+Swish dispatch failed\n"; return 1; } for (int row = 0; row < rows; ++row) { for (int column = 0; column < columns; ++column) { float expected = bias[std::size_t(column)]; for (int k = 0; k < depth; ++k) { expected += left[std::size_t(row) * depth + k] * right[std::size_t(k) * columns + column]; } expected *= 1.F / (1.F + std::exp(-expected)); const auto index = std::size_t(row) * columns + column; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[index] - expected) > tolerance) { std::cerr << "Vulkan GEMM+Swish mismatch at " << index << '\n'; return 1; } } } } { // Hybrid rec terminal: GEMM + CTC Top-1. Reference is the independent // CPU GemmCtcTop1 (not a second copy of the GPU kernel). Odd/tail plus a // recognizer-sized projection. struct GemmCtcCase { int rows, depth, vocab; const char* name; }; const GemmCtcCase cases[] = { {7, 17, 19, "odd-gemm-ctc"}, {13, 37, 173, "mid-gemm-ctc"}, {76, 96, 6625, "tiny-rec-ctc"}, }; for (const auto& cse : cases) { std::vector left(std::size_t(cse.rows) * cse.depth), right(std::size_t(cse.depth) * cse.vocab), bias(cse.vocab); std::vector cpu_idx(cse.rows), gpu_idx(cse.rows); std::vector cpu_prob(cse.rows), gpu_prob(cse.rows); for (std::size_t i = 0; i < left.size(); ++i) left[i] = float(int(i % 29) - 14) * .015625F; for (std::size_t i = 0; i < right.size(); ++i) right[i] = float(int(i % 17) - 8) * .03125F; for (int i = 0; i < cse.vocab; ++i) bias[i] = float(i % 13) * .0625F - .375F; if (!ppocr::detail::VulkanGemmCtcTop1(gpu_idx.data(), gpu_prob.data(), left.data(), right.data(), bias.data(), cse.rows, cse.depth, cse.vocab, cse.rows, true)) { std::cerr << "Vulkan GEMM+CTC dispatch failed (" << cse.name << ")\n"; return 1; } ppocr::detail::kernels::GemmCtcTop1(cpu_idx.data(), cpu_prob.data(), left.data(), right.data(), bias.data(), cse.rows, cse.depth, cse.vocab, cse.rows); for (int row = 0; row < cse.rows; ++row) { if (gpu_idx[row] != cpu_idx[row]) { std::cerr << "Vulkan GEMM+CTC index mismatch (" << cse.name << ") row=" << row << " gpu=" << gpu_idx[row] << " cpu=" << cpu_idx[row] << '\n'; return 1; } const float tolerance = 2e-4F * std::max(1.F, std::abs(cpu_prob[row])); if (std::abs(gpu_prob[row] - cpu_prob[row]) > tolerance) { std::cerr << "Vulkan GEMM+CTC prob mismatch (" << cse.name << ") row=" << row << " gpu=" << gpu_prob[row] << " cpu=" << cpu_prob[row] << '\n'; return 1; } } } double gemm_ctc_gpu_ms{}; double gemm_ctc_cpu_ms{}; const bool hybrid_gemm_ctc_selects_gpu = ppocr::detail::VulkanGemmCtcTop1NoSlowerThanCpu( 76, 96, 6625, &gemm_ctc_gpu_ms, &gemm_ctc_cpu_ms, true); std::cerr << "hybrid GEMM+CTC admission gpu_ms=" << gemm_ctc_gpu_ms << " cpu_ms=" << gemm_ctc_cpu_ms << " select_gpu=" << (hybrid_gemm_ctc_selects_gpu ? 1 : 0) << '\n'; (void)hybrid_gemm_ctc_selects_gpu; } { // Detector FPN local fusion: nearest 2x expansion plus the equal-shaped // lateral residual. Validate odd H/W and NCHW batch addressing; this is // deliberately one GPU dispatch and avoids materialising the upsampled // source on the host. constexpr std::size_t batches = 4; constexpr int channels = 24; constexpr int input_height = 31; constexpr int input_width = 29; constexpr auto input_plane = std::size_t(input_height) * input_width; constexpr auto output_plane = input_plane * 4; std::vector source(batches * channels * input_plane); std::vector residual(batches * channels * output_plane); std::vector output(residual.size()); for (std::size_t i = 0; i < source.size(); ++i) source[i] = float(i % 131) * .015625F - 1.F; for (std::size_t i = 0; i < residual.size(); ++i) residual[i] = float(i % 97) * .03125F - .5F; if (!ppocr::detail::VulkanNearestResize2xAddBatch( output.data(), source.data(), residual.data(), batches, channels, input_height, input_width)) { std::cerr << "Vulkan nearest-resize-add batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int channel = 0; channel < channels; ++channel) { for (int y = 0; y < input_height * 2; ++y) { for (int x = 0; x < input_width * 2; ++x) { const auto source_index = (batch * channels + channel) * input_plane + std::size_t(y / 2) * input_width + x / 2; const auto output_index = (batch * channels + channel) * output_plane + std::size_t(y) * input_width * 2 + x; const float expected = source[source_index] + residual[output_index]; const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[output_index] - expected) > tolerance) { std::cerr << "Vulkan nearest-resize-add batch mismatch at " << output_index << '\n'; return 1; } } } } } } { // FPN's other local segment is a 2x2 stride-two ConvTranspose followed by // an equal-shape lateral Add. The runtime submits two dependent dispatches // but keeps the projection in device buffer D between them. constexpr std::size_t batches = 2; constexpr int input_channels = 7; constexpr int output_channels = 11; constexpr int input_height = 19; constexpr int input_width = 17; const auto input_plane = std::size_t(input_height) * input_width; const auto output_plane = input_plane * 4; std::vector input(batches * input_channels * input_plane); std::vector weights(std::size_t(input_channels) * output_channels * 4); std::vector bias(output_channels); std::vector residual(batches * output_channels * output_plane); std::vector output(residual.size()); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(i % 97) * .015625F - .75F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(i % 43) * .03125F - .625F; for (int i = 0; i < output_channels; ++i) bias[std::size_t(i)] = float(i % 11) * .0625F - .25F; for (std::size_t i = 0; i < residual.size(); ++i) residual[i] = float(i % 67) * .015625F - .5F; if (!ppocr::detail::VulkanConvTranspose2x2AddBatch( output.data(), input.data(), weights.data(), bias.data(), residual.data(), batches, input_channels, output_channels, input_height, input_width, true)) { std::cerr << "Vulkan ConvTranspose2x2+Add dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int output_channel = 0; output_channel < output_channels; ++output_channel) { for (int y = 0; y < input_height * 2; ++y) for (int x = 0; x < input_width * 2; ++x) { const auto input_index = std::size_t(y / 2) * input_width + std::size_t(x / 2); const auto tap = std::size_t((y & 1) * 2 + (x & 1)); float expected = bias[std::size_t(output_channel)]; for (int input_channel = 0; input_channel < input_channels; ++input_channel) { expected += input[(batch * input_channels + input_channel) * input_plane + input_index] * weights[(std::size_t(input_channel) * output_channels + output_channel) * 4 + tap]; } const auto index = (batch * output_channels + output_channel) * output_plane + std::size_t(y) * (input_width * 2) + std::size_t(x); expected += residual[index]; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[index] - expected) > tolerance) { std::cerr << "Vulkan ConvTranspose2x2+Add mismatch at " << index << '\n'; return 1; } } } } } { // PP-OCRv6 depthwise blocks frequently feed a Swish gate. Keep the // activation on the device so a hybrid segment needs only one readback. constexpr std::size_t batches = 3; constexpr int channels = 12; constexpr int input_height = 19; constexpr int input_width = 17; constexpr int output_height = 19; constexpr int output_width = 17; constexpr int kernel_height = 3; constexpr int kernel_width = 3; constexpr int stride_height = 1; constexpr int stride_width = 1; constexpr int pad_top = 1; constexpr int pad_left = 1; const std::size_t input_plane = std::size_t(input_height) * input_width; const std::size_t output_plane = std::size_t(output_height) * output_width; std::vector input(batches * channels * input_plane), weights(std::size_t(channels) * kernel_height * kernel_width), bias(channels), output(batches * channels * output_plane); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(int(i % 31) - 15) * .0625F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 19) - 9) * .03125F; for (int channel = 0; channel < channels; ++channel) bias[channel] = float(channel - 4) * .0625F; if (!ppocr::detail::VulkanDepthwiseConvBatch( output.data(), input.data(), weights.data(), bias.data(), batches, channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, stride_height, stride_width, pad_top, pad_left, true, false, true)) { std::cerr << "Vulkan depthwise+Swish batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int channel = 0; channel < channels; ++channel) { for (int y = 0; y < output_height; ++y) { for (int x = 0; x < output_width; ++x) { float expected = bias[std::size_t(channel)]; for (int ky = 0; ky < kernel_height; ++ky) { const int iy = y * stride_height - pad_top + ky; if (iy < 0 || iy >= input_height) continue; for (int kx = 0; kx < kernel_width; ++kx) { const int ix = x * stride_width - pad_left + kx; if (ix < 0 || ix >= input_width) continue; const auto input_index = (batch * channels + channel) * input_plane + std::size_t(iy) * input_width + ix; const auto weight_index = std::size_t(channel) * kernel_height * kernel_width + std::size_t(ky) * kernel_width + kx; expected += input[input_index] * weights[weight_index]; } } expected = expected / (1.F + std::exp(-expected)); const auto index = (batch * channels + channel) * output_plane + std::size_t(y) * output_width + x; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[index] - expected) > tolerance) { std::cerr << "Vulkan depthwise+Swish batch mismatch at " << index << '\n'; return 1; } } } } } } { // BatchNorm's inference affine is the most useful two-RHS GPU building // block: scale and bias stay compact while four crop feature maps are // transformed in one submission. Exercise both shared model coefficients // and two distinct input batches so stale mapped data cannot pass. constexpr std::size_t count = 8192; constexpr std::size_t batches = 4; constexpr std::size_t channel_repeat = 64; constexpr std::size_t channels = count / channel_repeat; std::vector left0(count * batches), left1(count * batches); std::vector scale(channels), bias(channels), output(count * batches); for (std::size_t i = 0; i < left0.size(); ++i) { left0[i] = float(i % count) * .015625F - 6.F; left1[i] = float(i % count) * .03125F + 2.F; } for (std::size_t i = 0; i < channels; ++i) { scale[i] = float(i % 29) * .03125F + .5F; bias[i] = float(i % 23) * .015625F - .25F; } for (const float* left : {left0.data(), left1.data()}) { if (!ppocr::detail::VulkanChannelAffineBatch( output.data(), left, scale.data(), bias.data(), count, batches, channel_repeat, channels, true)) { std::cerr << "Vulkan channel-affine batch dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { const std::size_t channel = (i % count) / channel_repeat; const float expected = left[i] * scale[channel] + bias[channel]; const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan channel-affine batch result mismatch at index=" << i << '\n'; return 1; } } } } { // The recognizer follows several folded BatchNorm nodes with Swish. Test // the fused GPU form over one four-crop NCHW batch, including a repeated // immutable coefficient upload and exact FP32 comparison. constexpr std::size_t count = 8192; constexpr std::size_t batches = 4; constexpr std::size_t channel_repeat = 64; constexpr std::size_t channels = count / channel_repeat; std::vector left0(count * batches), left1(count * batches); std::vector scale(channels), bias(channels), output(count * batches); for (std::size_t i = 0; i < left0.size(); ++i) { left0[i] = float(i % count) * .015625F - 6.F; left1[i] = float(i % count) * .03125F + 2.F; } for (std::size_t i = 0; i < channels; ++i) { scale[i] = float(i % 29) * .03125F + .5F; bias[i] = float(i % 23) * .015625F - .25F; } for (const float* left : {left0.data(), left1.data()}) { if (!ppocr::detail::VulkanChannelAffineSwishBatch( output.data(), left, scale.data(), bias.data(), count, batches, channel_repeat, channels, true)) { std::cerr << "Vulkan channel-affine Swish batch dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { const std::size_t channel = (i % count) / channel_repeat; const float affine = left[i] * scale[channel] + bias[channel]; const float expected = affine / (1.F + std::exp(-affine)); const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan channel-affine Swish batch result mismatch at index=" << i << '\n'; return 1; } } } } { // The small/medium recognizer then adds a shortcut to that Swish result. // Keep the whole affine->Swish->Add segment in one dispatch and verify // that all four crop slices follow ordinary NCHW channel addressing. constexpr std::size_t count = 8192; constexpr std::size_t batches = 4; constexpr std::size_t channel_repeat = 64; constexpr std::size_t channels = count / channel_repeat; std::vector left(count * batches), right(count * batches), scale(channels), bias(channels), output(count * batches); for (std::size_t i = 0; i < left.size(); ++i) { left[i] = float(i % count) * .015625F - 6.F; right[i] = float(i % 113) * .015625F - .75F; } for (std::size_t i = 0; i < channels; ++i) { scale[i] = float(i % 29) * .03125F + .5F; bias[i] = float(i % 23) * .015625F - .25F; } if (!ppocr::detail::VulkanChannelAffineSwishBinaryBatch( output.data(), left.data(), scale.data(), bias.data(), right.data(), count, batches, channel_repeat, channels, ppocr::detail::kernels::BinaryOp::add, true)) { std::cerr << "Vulkan channel-affine Swish+Add batch dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { const std::size_t channel = (i % count) / channel_repeat; const float affine = left[i] * scale[channel] + bias[channel]; const float expected = affine / (1.F + std::exp(-affine)) + right[i]; const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan channel-affine Swish+Add batch mismatch at index=" << i << '\n'; return 1; } } } { // Exercise the first dense GPU operator with a non-vector-aligned plane // and four crop-like NCHW inputs. This catches both batch addressing and // the separate output buffer used by pointwise Conv. constexpr std::size_t batches = 4; constexpr int input_channels = 24; constexpr int output_channels = 40; constexpr std::size_t plane = 257; std::vector input(batches * input_channels * plane); std::vector weights(std::size_t(input_channels) * output_channels); std::vector bias(output_channels), residual(batches * output_channels * plane), output(batches * output_channels * plane); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(i % 127) * .015625F - 1.F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(i % 23) * .03125F - .25F; for (int i = 0; i < output_channels; ++i) bias[std::size_t(i)] = float(i % 11) * .0625F - .25F; for (std::size_t i = 0; i < residual.size(); ++i) residual[i] = float(i % 37) * .03125F - .5F; // Conv->Sigmoid is a detector gate. Keep it in the same batch-aware GPU // dispatch so hybrid does not need a CPU activation pass after readback. if (!ppocr::detail::VulkanPointwiseConvBatch( output.data(), input.data(), weights.data(), bias.data(), batches, input_channels, output_channels, plane, true, false, false, true)) { std::cerr << "Vulkan pointwise Conv+Sigmoid batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int out_channel = 0; out_channel < output_channels; ++out_channel) { for (std::size_t spatial = 0; spatial < plane; ++spatial) { float affine = bias[std::size_t(out_channel)]; for (int in_channel = 0; in_channel < input_channels; ++in_channel) { affine += input[(batch * input_channels + in_channel) * plane + spatial] * weights[std::size_t(out_channel) * input_channels + in_channel]; } const float expected = 1.F / (1.F + std::exp(-affine)); const auto offset = (batch * output_channels + out_channel) * plane + spatial; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan pointwise Conv+Sigmoid mismatch at " << offset << '\n'; return 1; } } } } // PP-OCR detector squeeze-excitation gates also export HardSigmoid. // Validate its alpha/beta push constants on the same odd spatial tail. if (!ppocr::detail::VulkanPointwiseConvBatch( output.data(), input.data(), weights.data(), bias.data(), batches, input_channels, output_channels, plane, true, false, false, false, true, 1.F / 6.F, .5F)) { std::cerr << "Vulkan pointwise Conv+HardSigmoid batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int out_channel = 0; out_channel < output_channels; ++out_channel) { for (std::size_t spatial = 0; spatial < plane; ++spatial) { float affine = bias[std::size_t(out_channel)]; for (int in_channel = 0; in_channel < input_channels; ++in_channel) { affine += input[(batch * input_channels + in_channel) * plane + spatial] * weights[std::size_t(out_channel) * input_channels + in_channel]; } const float expected = std::clamp(affine / 6.F + .5F, 0.F, 1.F); const auto offset = (batch * output_channels + out_channel) * plane + spatial; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan pointwise Conv+HardSigmoid mismatch at " << offset << '\n'; return 1; } } } } // Conv->HardSigmoid->Mul is canonicalized to exact HardSwish by the // detector graph fuser. Keep that final activation in the same batch // dispatch and validate the odd spatial tail independently. if (!ppocr::detail::VulkanPointwiseConvBatch( output.data(), input.data(), weights.data(), bias.data(), batches, input_channels, output_channels, plane, true, false, false, false, false, 1.F / 6.F, .5F, true)) { std::cerr << "Vulkan pointwise Conv+HardSwish batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int out_channel = 0; out_channel < output_channels; ++out_channel) { for (std::size_t spatial = 0; spatial < plane; ++spatial) { float affine = bias[std::size_t(out_channel)]; for (int in_channel = 0; in_channel < input_channels; ++in_channel) { affine += input[(batch * input_channels + in_channel) * plane + spatial] * weights[std::size_t(out_channel) * input_channels + in_channel]; } const float expected = affine * std::clamp(affine / 6.F + .5F, 0.F, 1.F); const auto offset = (batch * output_channels + out_channel) * plane + spatial; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan pointwise Conv+HardSwish mismatch at " << offset << '\n'; return 1; } } } } for (const bool relu : {false, true}) { if (!ppocr::detail::VulkanPointwiseConvBatch( output.data(), input.data(), weights.data(), bias.data(), batches, input_channels, output_channels, plane, true, relu)) { std::cerr << "Vulkan pointwise Conv batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int out_channel = 0; out_channel < output_channels; ++out_channel) { for (std::size_t spatial = 0; spatial < plane; ++spatial) { float expected = bias[std::size_t(out_channel)]; for (int in_channel = 0; in_channel < input_channels; ++in_channel) { expected += input[(batch * input_channels + in_channel) * plane + spatial] * weights[std::size_t(out_channel) * input_channels + in_channel]; } if (relu) expected = std::max(expected, 0.F); const auto offset = (batch * output_channels + out_channel) * plane + spatial; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan pointwise Conv mismatch at " << offset << '\n'; return 1; } } } } } // The common Conv->Sigmoid->Mul graph is folded to Conv+Swish. Exercise // its one-dispatch pointwise implementation across the same NCHW batch. if (!ppocr::detail::VulkanPointwiseConvBatch( output.data(), input.data(), weights.data(), bias.data(), batches, input_channels, output_channels, plane, true, false, true)) { std::cerr << "Vulkan pointwise Conv+Swish batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int out_channel = 0; out_channel < output_channels; ++out_channel) { for (std::size_t spatial = 0; spatial < plane; ++spatial) { float affine = bias[std::size_t(out_channel)]; for (int in_channel = 0; in_channel < input_channels; ++in_channel) { affine += input[(batch * input_channels + in_channel) * plane + spatial] * weights[std::size_t(out_channel) * input_channels + in_channel]; } const float expected = affine / (1.F + std::exp(-affine)); const auto offset = (batch * output_channels + out_channel) * plane + spatial; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan pointwise Conv+Swish mismatch at " << offset << '\n'; return 1; } } } } for (const bool relu : {false, true}) { if (!ppocr::detail::VulkanPointwiseConvAddBatch( output.data(), input.data(), weights.data(), bias.data(), residual.data(), batches, input_channels, output_channels, plane, true, relu)) { std::cerr << "Vulkan residual pointwise Conv batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int out_channel = 0; out_channel < output_channels; ++out_channel) { for (std::size_t spatial = 0; spatial < plane; ++spatial) { float expected = residual[(batch * output_channels + out_channel) * plane + spatial] + bias[std::size_t(out_channel)]; for (int in_channel = 0; in_channel < input_channels; ++in_channel) { expected += input[(batch * input_channels + in_channel) * plane + spatial] * weights[std::size_t(out_channel) * input_channels + in_channel]; } if (relu) expected = std::max(expected, 0.F); const auto offset = (batch * output_channels + out_channel) * plane + spatial; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan residual pointwise Conv mismatch at " << offset << ", got=" << output[offset] << ", expected=" << expected << ", residual=" << residual[offset] << '\n'; return 1; } } } } } // The recognizer also contains Conv->shortcut Add->Sigmoid->Mul. This // is mode 9: the residual and exact Swish stay in the same GPU dispatch. if (!ppocr::detail::VulkanPointwiseConvAddBatch( output.data(), input.data(), weights.data(), bias.data(), residual.data(), batches, input_channels, output_channels, plane, true, false, true)) { std::cerr << "Vulkan residual pointwise Conv+Swish batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int out_channel = 0; out_channel < output_channels; ++out_channel) { for (std::size_t spatial = 0; spatial < plane; ++spatial) { float affine = residual[(batch * output_channels + out_channel) * plane + spatial] + bias[std::size_t(out_channel)]; for (int in_channel = 0; in_channel < input_channels; ++in_channel) { affine += input[(batch * input_channels + in_channel) * plane + spatial] * weights[std::size_t(out_channel) * input_channels + in_channel]; } const float expected = affine / (1.F + std::exp(-affine)); const auto offset = (batch * output_channels + out_channel) * plane + spatial; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan residual pointwise Conv+Swish mismatch at " << offset << '\n'; return 1; } } } } } { // A recognizer-like batch: independent same-width crops share one GPU // command submission and use the shader's Y dimension rather than four // separate transfer/dispatch/readback cycles. constexpr std::size_t count = 4099; constexpr std::size_t batches = 4; std::vector left(count * batches), right(count * batches), output(count * batches); for (std::size_t batch = 0; batch < batches; ++batch) { for (std::size_t i = 0; i < count; ++i) { const auto offset = batch * count + i; left[offset] = float(i) * .0625F + float(batch) * .25F - 3.F; right[offset] = float((i + batch) % 17) * .125F + .5F; } } const std::vector chain{ ppocr::detail::kernels::BinaryOp::add, ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::sub, ppocr::detail::kernels::BinaryOp::div}; if (!ppocr::detail::VulkanBinaryChainBatch(output.data(), left.data(), right.data(), count, batches, chain)) { std::cerr << "Vulkan binary batch dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { float expected = left[i]; for (const auto operation : chain) { switch (operation) { case ppocr::detail::kernels::BinaryOp::add: expected += right[i]; break; case ppocr::detail::kernels::BinaryOp::sub: expected -= right[i]; break; case ppocr::detail::kernels::BinaryOp::mul: expected *= right[i]; break; case ppocr::detail::kernels::BinaryOp::div: expected /= right[i]; break; } } const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan batch result mismatch at index=" << i << '\n'; return 1; } } } { // Immutable model constants may remain in the runtime's persistent RHS // buffer across submissions. Exercise two distinct activations against // one RHS so a stale activation/result buffer cannot mask a cache bug. constexpr std::size_t count = 65537; std::vector left0(count), left1(count), right(count), output(count); for (std::size_t i = 0; i < count; ++i) { left0[i] = float(i) * .03125F - 8.F; left1[i] = float(i) * .0625F + 3.F; right[i] = float(i % 29) * .125F + .5F; } for (const float* left : {left0.data(), left1.data()}) { if (!ppocr::detail::VulkanBinary(output.data(), left, right.data(), count, ppocr::detail::kernels::BinaryOp::mul, true)) { std::cerr << "Vulkan immutable RHS dispatch failed\n"; return 1; } for (std::size_t i = 0; i < count; ++i) { const float expected = left[i] * right[i]; const float tolerance = 2e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[i] - expected) > tolerance) { std::cerr << "Vulkan immutable RHS result mismatch at index=" << i << '\n'; return 1; } } } } { // Detector-style ordinary 3x3 convolution: exercise both a padded // stride-one batch and a stride-two batch, including the shader-fused // ReLU writeback. The reference deliberately follows ONNX's scalar // reduction order instead of reusing the production CPU kernel. for (const int stride : {1, 2}) { constexpr std::size_t batches = 3; constexpr int input_channels = 4, output_channels = 7; constexpr int input_height = 11, input_width = 13; constexpr int kernel_height = 3, kernel_width = 3; const int output_height = (input_height + 2 - kernel_height) / stride + 1; const int output_width = (input_width + 2 - kernel_width) / stride + 1; const std::size_t input_plane = std::size_t(input_height) * input_width; const std::size_t output_plane = std::size_t(output_height) * output_width; std::vector input(batches * input_channels * input_plane), weights(std::size_t(output_channels) * input_channels * kernel_height * kernel_width), bias(output_channels), output(batches * output_channels * output_plane); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(int(i % 31) - 15) * .03125F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 19) - 9) * .015625F; for (int channel = 0; channel < output_channels; ++channel) bias[channel] = float(channel - 3) * .0625F; if (!ppocr::detail::VulkanConv2dBatch( output.data(), input.data(), weights.data(), bias.data(), batches, input_channels, output_channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, stride, stride, 1, 1, true, true)) { std::cerr << "Vulkan ordinary Conv batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int output_channel = 0; output_channel < output_channels; ++output_channel) { for (int oy = 0; oy < output_height; ++oy) for (int ox = 0; ox < output_width; ++ox) { float expected = bias[output_channel]; for (int input_channel = 0; input_channel < input_channels; ++input_channel) { for (int ky = 0; ky < kernel_height; ++ky) for (int kx = 0; kx < kernel_width; ++kx) { const int iy = oy * stride - 1 + ky; const int ix = ox * stride - 1 + kx; if (iy >= 0 && iy < input_height && ix >= 0 && ix < input_width) { expected += input[(batch * input_channels + input_channel) * input_plane + std::size_t(iy) * input_width + ix] * weights[((output_channel * input_channels + input_channel) * kernel_height + ky) * kernel_width + kx]; } } } expected = std::max(expected, 0.F); const auto offset = (batch * output_channels + output_channel) * output_plane + std::size_t(oy) * output_width + ox; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan ordinary Conv mismatch, stride=" << stride << ", offset=" << offset << '\n'; return 1; } } } } } } { // Shipped hybrid Conv2d entry: detector Conv.0 on the tiny en.ppm // geometry (1x3x160x704, 3x3 s2 pad1, 16-wide ReLU) plus an odd-tail // 17x65x353 plane so the 16x16 spatial tile and 4-OC remainder are // checked against an independent ONNX-order scalar reduction. struct HybridSpatial { std::size_t batches; int ic, oc, ih, iw, oh, ow; bool relu; const char* name; }; const HybridSpatial hybrid_spatial[] = { {1, 3, 16, 160, 704, 80, 352, true, "conv0"}, {1, 5, 17, 129, 705, 65, 353, false, "spatial-tail"}, }; for (const auto& spatial : hybrid_spatial) { const std::size_t in_plane = std::size_t(spatial.ih) * spatial.iw; const std::size_t out_plane = std::size_t(spatial.oh) * spatial.ow; std::vector input(spatial.batches * spatial.ic * in_plane), weights(std::size_t(spatial.oc) * spatial.ic * 9), bias(spatial.oc), output(spatial.batches * spatial.oc * out_plane); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(int(i % 31) - 15) * .03125F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 19) - 9) * .015625F; for (int channel = 0; channel < spatial.oc; ++channel) bias[channel] = float(channel - 3) * .0625F; if (!ppocr::detail::VulkanConv2dBatch( output.data(), input.data(), weights.data(), bias.data(), spatial.batches, spatial.ic, spatial.oc, spatial.ih, spatial.iw, spatial.oh, spatial.ow, 3, 3, 2, 2, 1, 1, true, spatial.relu)) { std::cerr << "Vulkan spatial Conv2d dispatch failed (" << spatial.name << ")\n"; return 1; } for (std::size_t batch = 0; batch < spatial.batches; ++batch) { for (int oc = 0; oc < spatial.oc; ++oc) { for (int oy = 0; oy < spatial.oh; ++oy) for (int ox = 0; ox < spatial.ow; ++ox) { float expected = bias[oc]; for (int ic = 0; ic < spatial.ic; ++ic) for (int ky = 0; ky < 3; ++ky) for (int kx = 0; kx < 3; ++kx) { const int iy = oy * 2 - 1 + ky; const int ix = ox * 2 - 1 + kx; if (iy >= 0 && iy < spatial.ih && ix >= 0 && ix < spatial.iw) { expected += input[(batch * spatial.ic + ic) * in_plane + std::size_t(iy) * spatial.iw + ix] * weights[((oc * spatial.ic + ic) * 3 + ky) * 3 + kx]; } } if (spatial.relu) expected = std::max(expected, 0.F); const auto offset = (batch * spatial.oc + oc) * out_plane + std::size_t(oy) * spatial.ow + ox; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan spatial Conv2d mismatch (" << spatial.name << ") offset=" << offset << '\n'; return 1; } } } } } } { // MobileNet depthwise 3x3 has an independent filter per NCHW channel. // Verify batch addressing, padding, and the scalar output tail against a // direct reference before using its end-to-end admission measurement. constexpr std::size_t batches = 2; constexpr int channels = 5, input_height = 7, input_width = 9; constexpr int output_height = 7, output_width = 9; constexpr int kernel_height = 3, kernel_width = 3; constexpr int stride_height = 1, stride_width = 1, pad_top = 1, pad_left = 1; const std::size_t input_plane = std::size_t(input_height) * input_width; const std::size_t output_plane = std::size_t(output_height) * output_width; std::vector input(batches * channels * input_plane), weights(std::size_t(channels) * kernel_height * kernel_width), bias(channels), output(batches * channels * output_plane); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(int(i % 23) - 11) * .0625F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 17) - 8) * .03125F; for (int channel = 0; channel < channels; ++channel) bias[channel] = float(channel - 2) * .125F; if (!ppocr::detail::VulkanDepthwiseConvBatch( output.data(), input.data(), weights.data(), bias.data(), batches, channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, stride_height, stride_width, pad_top, pad_left, true)) { std::cerr << "Vulkan depthwise batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int channel = 0; channel < channels; ++channel) { for (int oy = 0; oy < output_height; ++oy) for (int ox = 0; ox < output_width; ++ox) { float expected = bias[channel]; for (int ky = 0; ky < kernel_height; ++ky) for (int kx = 0; kx < kernel_width; ++kx) { const int iy = oy * stride_height - pad_top + ky; const int ix = ox * stride_width - pad_left + kx; if (iy >= 0 && iy < input_height && ix >= 0 && ix < input_width) { expected += input[(batch * channels + channel) * input_plane + std::size_t(iy) * input_width + ix] * weights[std::size_t(channel) * kernel_height * kernel_width + ky * kernel_width + kx]; } } const auto offset = (batch * channels + channel) * output_plane + std::size_t(oy) * output_width + ox; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan depthwise mismatch at " << offset << " got=" << output[offset] << " expected=" << expected << '\n'; return 1; } } } } } { // A complete MobileNet depthwise -> pointwise block must keep its // intermediate off the host. Odd planes and a multi-item NCHW batch // exercise both dispatches plus the explicit shader-write/read barrier. constexpr std::size_t batches = 1; constexpr int channels = 64, output_channels = 512; constexpr int input_height = 23, input_width = 23; constexpr int output_height = 23, output_width = 23; constexpr int kernel_height = 3, kernel_width = 3; constexpr int stride_height = 1, stride_width = 1, pad_top = 1, pad_left = 1; const std::size_t input_plane = std::size_t(input_height) * input_width; const std::size_t output_plane = std::size_t(output_height) * output_width; std::vector input(batches * channels * input_plane), depthwise_weights(std::size_t(channels) * kernel_height * kernel_width), depthwise_bias(channels), pointwise_weights(std::size_t(output_channels) * channels), pointwise_bias(output_channels), output(batches * output_channels * output_plane), intermediate(batches * channels * output_plane), expected(output.size()); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(int(i % 23) - 11) * .0625F; for (std::size_t i = 0; i < depthwise_weights.size(); ++i) { depthwise_weights[i] = float(int(i % 17) - 8) * .03125F; } for (std::size_t i = 0; i < pointwise_weights.size(); ++i) { pointwise_weights[i] = float(int(i % 19) - 9) * .03125F; } for (int channel = 0; channel < channels; ++channel) depthwise_bias[channel] = float(channel % 11 - 5) * .125F; for (int channel = 0; channel < output_channels; ++channel) pointwise_bias[channel] = float(channel % 13 - 6) * .0625F; ppocr::detail::kernels::DepthwiseConvBatch( intermediate.data(), input.data(), depthwise_weights.data(), depthwise_bias.data(), int(batches), channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, stride_height, stride_width, pad_top, pad_left); ppocr::detail::kernels::PointwiseConvBatch( expected.data(), intermediate.data(), pointwise_weights.data(), pointwise_bias.data(), int(batches), output_channels, channels, output_plane); if (!ppocr::detail::VulkanDepthwisePointwiseConvBatch( output.data(), input.data(), depthwise_weights.data(), depthwise_bias.data(), pointwise_weights.data(), pointwise_bias.data(), batches, channels, output_channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, stride_height, stride_width, pad_top, pad_left, true)) { std::cerr << "Vulkan depthwise-pointwise chain dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { const float tolerance = 4e-5F * std::max(1.F, std::abs(expected[i])); if (std::abs(output[i] - expected[i]) > tolerance) { std::cerr << "Vulkan depthwise-pointwise chain mismatch at " << i << '\n'; return 1; } } // Mode 30 folds the explicit AVX GELU approximation into the final // pointwise writeback. `kernels::Gelu()` intentionally remains exact // unless PPOCR_APPROX_GELU is enabled, while this smoke must validate the // shader mode independent of the caller's environment. Calculate the // identical Padé-tanh approximation locally rather than accidentally // comparing this opt-in shader mode with ONNX's exact Erf-GELU default. for (float& value : expected) { const float z = .7978845608028654F * (value + .044715F * value * value * value); const float z2 = z * z; const float t = std::clamp(z * (27.F + z2) / (27.F + 9.F * z2), -1.F, 1.F); value = .5F * value * (1.F + t); } if (!ppocr::detail::VulkanDepthwisePointwiseConvBatch( output.data(), input.data(), depthwise_weights.data(), depthwise_bias.data(), pointwise_weights.data(), pointwise_bias.data(), batches, channels, output_channels, input_height, input_width, output_height, output_width, kernel_height, kernel_width, stride_height, stride_width, pad_top, pad_left, true, true)) { std::cerr << "Vulkan depthwise-pointwise GELU chain dispatch failed\n"; return 1; } for (std::size_t i = 0; i < output.size(); ++i) { const float tolerance = 5e-5F * std::max(1.F, std::abs(expected[i])); if (std::abs(output[i] - expected[i]) > tolerance) { std::cerr << "Vulkan depthwise-pointwise GELU chain mismatch at " << i << " got=" << output[i] << " expected=" << expected[i] << '\n'; return 1; } } } double gpu_ms{}; double cpu_ms{}; const bool hybrid_selects_gpu = ppocr::detail::VulkanBinaryNoSlowerThanCpu( 65537, ppocr::detail::kernels::BinaryOp::mul, &gpu_ms, &cpu_ms); double chain_gpu_ms{}; double chain_cpu_ms{}; const bool hybrid_chain_selects_gpu = ppocr::detail::VulkanBinaryChainNoSlowerThanCpu( 65537, {ppocr::detail::kernels::BinaryOp::add, ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::sub, ppocr::detail::kernels::BinaryOp::div}, &chain_gpu_ms, &chain_cpu_ms); double batch_gpu_ms{}; double batch_cpu_ms{}; const bool hybrid_batch_selects_gpu = ppocr::detail::VulkanBinaryChainBatchNoSlowerThanCpu( 4099, 4, {ppocr::detail::kernels::BinaryOp::add, ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::sub, ppocr::detail::kernels::BinaryOp::div}, &batch_gpu_ms, &batch_cpu_ms); double broadcast_gpu_ms{}; double broadcast_cpu_ms{}; const bool hybrid_broadcast_selects_gpu = ppocr::detail::VulkanBinaryBroadcastRightChainBatchNoSlowerThanCpu( 8192, 4, 64, 512, {ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::add}, &broadcast_gpu_ms, &broadcast_cpu_ms); double shared_broadcast_gpu_ms{}; double shared_broadcast_cpu_ms{}; const bool hybrid_shared_broadcast_selects_gpu = ppocr::detail::VulkanBinaryBroadcastRightChainBatchNoSlowerThanCpu( 8192, 4, 64, 128, {ppocr::detail::kernels::BinaryOp::mul, ppocr::detail::kernels::BinaryOp::add}, &shared_broadcast_gpu_ms, &shared_broadcast_cpu_ms, true); double affine_gpu_ms{}; double affine_cpu_ms{}; const bool hybrid_affine_selects_gpu = ppocr::detail::VulkanChannelAffineBatchNoSlowerThanCpu( 8192, 4, 64, 128, &affine_gpu_ms, &affine_cpu_ms, true); double affine_swish_gpu_ms{}; double affine_swish_cpu_ms{}; const bool hybrid_affine_swish_selects_gpu = ppocr::detail::VulkanChannelAffineSwishBatchNoSlowerThanCpu( 8192, 4, 64, 128, &affine_swish_gpu_ms, &affine_swish_cpu_ms, true); // Report the complete fused shortcut block too. It is verified above; this // direct timing includes upload, dispatch and readback so it can never be // confused with a device-resident-only throughput number. constexpr std::size_t fused_count = 8192; constexpr std::size_t fused_batches = 4; constexpr std::size_t fused_repeat = 64; constexpr std::size_t fused_channels = fused_count / fused_repeat; std::vector fused_left(fused_count * fused_batches), fused_right(fused_count * fused_batches), fused_scale(fused_channels), fused_bias(fused_channels), fused_output(fused_count * fused_batches), fused_cpu(fused_count * fused_batches); for (std::size_t i = 0; i < fused_left.size(); ++i) { fused_left[i] = float(i % fused_count) * .015625F - 6.F; fused_right[i] = float(i % 113) * .015625F - .75F; } for (std::size_t i = 0; i < fused_channels; ++i) { fused_scale[i] = float(i % 29) * .03125F + .5F; fused_bias[i] = float(i % 23) * .015625F - .25F; } constexpr int fused_runs = 5; double affine_swish_add_gpu_ms{}; double affine_swish_add_cpu_ms{}; bool affine_swish_add_correct = true; for (int run = 0; run < fused_runs; ++run) { const auto cpu_begin = std::chrono::steady_clock::now(); ppocr::detail::kernels::BatchNormSwishBinary( fused_cpu.data(), fused_left.data(), fused_scale.data(), fused_bias.data(), fused_right.data(), int(fused_batches), int(fused_channels), fused_repeat, ppocr::detail::kernels::BinaryOp::add); affine_swish_add_cpu_ms += std::chrono::duration( std::chrono::steady_clock::now() - cpu_begin).count(); const auto gpu_begin = std::chrono::steady_clock::now(); affine_swish_add_correct &= ppocr::detail::VulkanChannelAffineSwishBinaryBatch( fused_output.data(), fused_left.data(), fused_scale.data(), fused_bias.data(), fused_right.data(), fused_count, fused_batches, fused_repeat, fused_channels, ppocr::detail::kernels::BinaryOp::add, true); affine_swish_add_gpu_ms += std::chrono::duration( std::chrono::steady_clock::now() - gpu_begin).count(); for (std::size_t i = 0; i < fused_output.size(); ++i) { const float tolerance = 2e-5F * std::max(1.F, std::abs(fused_cpu[i])); affine_swish_add_correct &= std::abs(fused_output[i] - fused_cpu[i]) <= tolerance; } } affine_swish_add_gpu_ms /= fused_runs; affine_swish_add_cpu_ms /= fused_runs; double pointwise_gpu_ms{}; double pointwise_cpu_ms{}; const bool hybrid_pointwise_selects_gpu = ppocr::detail::VulkanPointwiseConvBatchNoSlowerThanCpu( 4, 24, 40, 257, &pointwise_gpu_ms, &pointwise_cpu_ms, true, false); double pointwise_swish_gpu_ms{}; double pointwise_swish_cpu_ms{}; const bool hybrid_pointwise_swish_selects_gpu = ppocr::detail::VulkanPointwiseConvBatchNoSlowerThanCpu( 4, 24, 40, 257, &pointwise_swish_gpu_ms, &pointwise_swish_cpu_ms, true, false, true); double pointwise_hard_swish_gpu_ms{}; double pointwise_hard_swish_cpu_ms{}; const bool hybrid_pointwise_hard_swish_selects_gpu = ppocr::detail::VulkanPointwiseConvBatchNoSlowerThanCpu( 4, 24, 40, 257, &pointwise_hard_swish_gpu_ms, &pointwise_hard_swish_cpu_ms, true, false, false, false, false, 1.F / 6.F, .5F, true); double pointwise_add_gpu_ms{}; double pointwise_add_cpu_ms{}; const bool hybrid_pointwise_add_selects_gpu = ppocr::detail::VulkanPointwiseConvAddBatchNoSlowerThanCpu( 4, 24, 40, 257, &pointwise_add_gpu_ms, &pointwise_add_cpu_ms, true, true); double pointwise_add_swish_gpu_ms{}; double pointwise_add_swish_cpu_ms{}; const bool hybrid_pointwise_add_swish_selects_gpu = ppocr::detail::VulkanPointwiseConvAddBatchNoSlowerThanCpu( 4, 24, 40, 257, &pointwise_add_swish_gpu_ms, &pointwise_add_swish_cpu_ms, true, false, true); double depthwise_gpu_ms{}; double depthwise_cpu_ms{}; const bool hybrid_depthwise_selects_gpu = ppocr::detail::VulkanDepthwiseConvBatchNoSlowerThanCpu( 4, 32, 31, 29, 31, 29, 3, 3, 1, 1, 1, 1, &depthwise_gpu_ms, &depthwise_cpu_ms, true); double depthwise_swish_gpu_ms{}; double depthwise_swish_cpu_ms{}; const bool hybrid_depthwise_swish_selects_gpu = ppocr::detail::VulkanDepthwiseConvBatchNoSlowerThanCpu( 4, 32, 31, 29, 31, 29, 3, 3, 1, 1, 1, 1, &depthwise_swish_gpu_ms, &depthwise_swish_cpu_ms, true, false, true); double depthwise_hardswish_gpu_ms{}; double depthwise_hardswish_cpu_ms{}; const bool hybrid_depthwise_hardswish_selects_gpu = ppocr::detail::VulkanDepthwiseConvBatchNoSlowerThanCpu( 4, 32, 31, 29, 31, 29, 3, 3, 1, 1, 1, 1, &depthwise_hardswish_gpu_ms, &depthwise_hardswish_cpu_ms, true, false, false, true); double depthwise_pointwise_gpu_ms{}; double depthwise_pointwise_cpu_ms{}; const bool hybrid_depthwise_pointwise_selects_gpu = ppocr::detail::VulkanDepthwisePointwiseConvBatchNoSlowerThanCpu( 4, 32, 40, 31, 29, 31, 29, 3, 3, 1, 1, 1, 1, &depthwise_pointwise_gpu_ms, &depthwise_pointwise_cpu_ms, true); double conv_gpu_ms{}; double conv_cpu_ms{}; const bool hybrid_conv_selects_gpu = ppocr::detail::VulkanConv2dBatchNoSlowerThanCpu( 4, 24, 40, 31, 29, 31, 29, 3, 3, 1, 1, 1, 1, &conv_gpu_ms, &conv_cpu_ms, true, true); double conv_swish_gpu_ms{}; double conv_swish_cpu_ms{}; const bool hybrid_conv_swish_selects_gpu = ppocr::detail::VulkanConv2dBatchNoSlowerThanCpu( 4, 24, 40, 31, 29, 31, 29, 3, 3, 1, 1, 1, 1, &conv_swish_gpu_ms, &conv_swish_cpu_ms, true, false, true); double conv0_gpu_ms{}; double conv0_cpu_ms{}; const bool hybrid_conv0_selects_gpu = ppocr::detail::VulkanConv2dBatchNoSlowerThanCpu( 1, 3, 16, 160, 704, 80, 352, 3, 3, 2, 2, 1, 1, &conv0_gpu_ms, &conv0_cpu_ms, true, true); double rec_stem_gpu_ms{}; double rec_stem_cpu_ms{}; const bool hybrid_rec_stem_selects_gpu = ppocr::detail::VulkanConv2dBatchNoSlowerThanCpu( 1, 3, 24, 48, 301, 24, 151, 3, 3, 2, 2, 1, 1, &rec_stem_gpu_ms, &rec_stem_cpu_ms, true, false); { // Detector front end: packed RGB bilinear + ImageNet BGR affine. Odd // 11x7→13x17 covers the shader's uint8 word tail; 80x32→64x128 is a // downscale like tiny en.ppm. Reference follows the shader's // floor(x+0.5) rounding, not the production CPU preprocessor. struct RgbCase { int sw, sh, ow, oh; }; const RgbCase rgb_cases[] = {{11, 7, 13, 17}, {80, 32, 64, 128}, {720, 152, 704, 160}}; for (const auto& rgb : rgb_cases) { std::vector packed(std::size_t(rgb.sw) * rgb.sh * 3); for (std::size_t i = 0; i < packed.size(); ++i) packed[i] = static_cast((i * 37u) % 251u); std::vector output(std::size_t(3) * rgb.ow * rgb.oh); if (!ppocr::detail::VulkanResizeRgbToNchw(output.data(), packed.data(), packed.size(), rgb.sw, rgb.sh, rgb.ow, rgb.oh, false)) { std::cerr << "Vulkan RGB resize dispatch failed\n"; return 1; } const std::size_t plane = std::size_t(rgb.ow) * rgb.oh; constexpr float scales[3] = {1.F / (255.F * .229F), 1.F / (255.F * .224F), 1.F / (255.F * .225F)}; constexpr float offsets[3] = {-.485F / .229F, -.456F / .224F, -.406F / .225F}; for (int oy = 0; oy < rgb.oh; ++oy) for (int ox = 0; ox < rgb.ow; ++ox) { const float fx = (float(ox) + .5F) * float(rgb.sw) / float(rgb.ow) - .5F; const float fy = (float(oy) + .5F) * float(rgb.sh) / float(rgb.oh) - .5F; const int x_floor = int(std::floor(fx)); const int y_floor = int(std::floor(fy)); const int x0 = std::clamp(x_floor, 0, rgb.sw - 1); const int y0 = std::clamp(y_floor, 0, rgb.sh - 1); const int x1 = std::min(x0 + 1, rgb.sw - 1); const int y1 = std::min(y0 + 1, rgb.sh - 1); const float dx = fx - float(x_floor); const float dy = fy - float(y_floor); for (int channel = 0; channel < 3; ++channel) { const int rgb_c = 2 - channel; const auto sample = [&](int x, int y) { return float(packed[(std::size_t(y) * rgb.sw + x) * 3 + rgb_c]); }; const float upper = sample(x0, y0) * (1.F - dx) + sample(x1, y0) * dx; const float lower = sample(x0, y1) * (1.F - dx) + sample(x1, y1) * dx; const float vertical = upper * (1.F - dy) + lower * dy; const float sampled = std::clamp(std::floor(vertical + .5F), 0.F, 255.F); const float expected = sampled * scales[channel] + offsets[channel]; const auto index = std::size_t(channel) * plane + std::size_t(oy) * rgb.ow + ox; // Large detector pages (720x152→704x160) differ by one uint8 step // after bilinear on this UMA GPU (~0.017 after ImageNet affine). // Odd/mid maps stay on the tight 4e-4 bound. const float step = 1.F / (255.F * 0.224F); const float tolerance = (rgb.sw >= 256 ? 1.25F * step : 4e-4F) * std::max(1.F, std::abs(expected)); if (std::abs(output[index] - expected) > tolerance) { std::cerr << "Vulkan RGB resize mismatch " << rgb.sw << "x" << rgb.sh << " offset=" << index << '\n'; return 1; } } } } } { // Rec RGB right-pad: store width 96, content width 64, height 48. Content // columns must match an unpadded 64-wide resize; padded columns are 0 so // NCHW channel planes stay aligned. const int sw = 40, sh = 16, cw = 64, pw = 96, oh = 48; std::vector packed(std::size_t(sw) * sh * 3); for (std::size_t i = 0; i < packed.size(); ++i) packed[i] = static_cast((i * 37u) % 251u); std::vector unpadded(std::size_t(3) * cw * oh); if (!ppocr::detail::VulkanResizeRgbToNchw(unpadded.data(), packed.data(), packed.size(), sw, sh, cw, oh, true)) { std::cerr << "Vulkan padded RGB unpadded reference failed\n"; return 1; } ppocr::detail::VulkanTensorArena arena; if (!arena.available()) { std::cerr << "Vulkan padded RGB arena unavailable\n"; return 1; } auto rgb_slot = arena.Acquire(packed.size(), "smoke-pad-rgb"); auto out_slot = arena.Acquire(std::size_t(3) * pw * oh, "smoke-pad-nchw"); std::vector padded(std::size_t(3) * pw * oh, 1.F); if (!rgb_slot.resident || !out_slot.resident || !arena.UploadRgb8(rgb_slot, packed.data(), packed.size()) || !arena.ResizeRgbToNchwAt(rgb_slot, out_slot, 0, sw, sh, 0, 0, sw, sh, pw, oh, 2.F / 255.F, -1.F, 2.F / 255.F, -1.F, 2.F / 255.F, -1.F, cw) || !arena.Download(padded.data(), out_slot, padded.size())) { std::cerr << "Vulkan padded RGB dispatch failed\n"; return 1; } arena.Release(out_slot); arena.Release(rgb_slot); const std::size_t content_plane = std::size_t(cw) * oh; const std::size_t padded_plane = std::size_t(pw) * oh; for (int channel = 0; channel < 3; ++channel) { for (int oy = 0; oy < oh; ++oy) { for (int ox = 0; ox < pw; ++ox) { const float got = padded[std::size_t(channel) * padded_plane + std::size_t(oy) * pw + ox]; if (ox >= cw) { if (got != 0.F) { std::cerr << "Vulkan padded RGB tail not zero ch=" << channel << " y=" << oy << " x=" << ox << " got=" << got << '\n'; return 1; } continue; } const float expected = unpadded[std::size_t(channel) * content_plane + std::size_t(oy) * cw + ox]; if (std::abs(got - expected) > 4e-4F * std::max(1.F, std::abs(expected))) { std::cerr << "Vulkan padded RGB mismatch ch=" << channel << " y=" << oy << " x=" << ox << " got=" << got << " expected=" << expected << '\n'; return 1; } } } } } { // Hybrid RGB+Conv.0: one H2D of packed RGB, device resize+3x3 s2, one D2H // of the stem. Reference is the shader-matching bilinear then CPU Conv2d. struct RgbConvCase { int sw, sh, nw, nh, oc; bool relu; const char* name; }; const RgbConvCase cases[] = { {11, 7, 13, 17, 8, true, "odd-rgb-conv"}, {80, 32, 64, 128, 16, true, "mid-rgb-conv"}, {720, 152, 704, 160, 16, true, "en-ppm-conv0"}, }; constexpr float scales[3] = {1.F / (255.F * .229F), 1.F / (255.F * .224F), 1.F / (255.F * .225F)}; constexpr float offsets[3] = {-.485F / .229F, -.456F / .224F, -.406F / .225F}; for (const auto& cse : cases) { const int oh = (cse.nh + 2 - 3) / 2 + 1; const int ow = (cse.nw + 2 - 3) / 2 + 1; std::vector packed(std::size_t(cse.sw) * cse.sh * 3); for (std::size_t i = 0; i < packed.size(); ++i) packed[i] = static_cast((i * 37u) % 251u); std::vector nchw(std::size_t(3) * cse.nw * cse.nh); std::vector weights(std::size_t(cse.oc) * 3 * 9), bias(std::size_t(cse.oc)), cpu(std::size_t(cse.oc) * oh * ow), gpu(cpu.size()); for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 19) - 9) * .015625F; for (int i = 0; i < cse.oc; ++i) bias[i] = float(i - 3) * .0625F; const std::size_t plane = std::size_t(cse.nw) * cse.nh; for (int oy = 0; oy < cse.nh; ++oy) for (int ox = 0; ox < cse.nw; ++ox) { const float fx = (float(ox) + .5F) * float(cse.sw) / float(cse.nw) - .5F; const float fy = (float(oy) + .5F) * float(cse.sh) / float(cse.nh) - .5F; const int x_floor = int(std::floor(fx)); const int y_floor = int(std::floor(fy)); const int x0 = std::clamp(x_floor, 0, cse.sw - 1); const int y0 = std::clamp(y_floor, 0, cse.sh - 1); const int x1 = std::min(x0 + 1, cse.sw - 1); const int y1 = std::min(y0 + 1, cse.sh - 1); const float dx = fx - float(x_floor); const float dy = fy - float(y_floor); for (int channel = 0; channel < 3; ++channel) { const int rgb_c = 2 - channel; const auto sample = [&](int x, int y) { return float(packed[(std::size_t(y) * cse.sw + x) * 3 + rgb_c]); }; const float upper = sample(x0, y0) * (1.F - dx) + sample(x1, y0) * dx; const float lower = sample(x0, y1) * (1.F - dx) + sample(x1, y1) * dx; const float sampled = std::clamp(std::floor(upper * (1.F - dy) + lower * dy + .5F), 0.F, 255.F); nchw[std::size_t(channel) * plane + std::size_t(oy) * cse.nw + ox] = sampled * scales[channel] + offsets[channel]; } } if (!ppocr::detail::VulkanResizeRgbAndConv2d( gpu.data(), packed.data(), packed.size(), cse.sw, cse.sh, cse.nw, cse.nh, weights.data(), bias.data(), cse.oc, 3, 2, 1, cse.relu)) { std::cerr << "Vulkan RGB+Conv2d dispatch failed (" << cse.name << ")\n"; return 1; } ppocr::detail::kernels::Conv2d(cpu.data(), nchw.data(), weights.data(), bias.data(), 3, cse.oc, cse.nh, cse.nw, oh, ow, 3, 3, 2, 2, 1, 1, cse.relu); float max_abs = 0.F; std::size_t max_i = 0; for (std::size_t i = 0; i < gpu.size(); ++i) { const float err = std::abs(gpu[i] - cpu[i]); if (err > max_abs) { max_abs = err; max_i = i; } } const float tolerance = (cse.sw >= 256 ? 3e-2F : 2e-3F) * std::max(1.F, std::abs(cpu[max_i])); if (max_abs > tolerance) { std::cerr << "Vulkan RGB+Conv2d mismatch (" << cse.name << ") offset=" << max_i << " max_abs=" << max_abs << " gpu=" << gpu[max_i] << " cpu=" << cpu[max_i] << '\n'; return 1; } } double rgb_conv_gpu_ms{}; double rgb_conv_cpu_ms{}; const bool hybrid_rgb_conv_selects_gpu = ppocr::detail::VulkanResizeRgbAndConv2dNoSlowerThanCpu( 720, 152, 704, 160, 16, 3, 2, 1, true, &rgb_conv_gpu_ms, &rgb_conv_cpu_ms); std::cerr << "hybrid RGB+Conv.0 admission gpu_ms=" << rgb_conv_gpu_ms << " cpu_ms=" << rgb_conv_cpu_ms << " select_gpu=" << (hybrid_rgb_conv_selects_gpu ? 1 : 0) << '\n'; (void)hybrid_rgb_conv_selects_gpu; } { // Hybrid rec RGB crop + first 3x3 s2. Odd crop and the tiny 301-wide // line both drive the shipped function vs independent bilinear+Conv2d. struct RecCropCase { int sw, sh, cx, cy, cw, ch, nw, nh, oc; const char* name; }; const RecCropCase cases[] = { {17, 13, 1, 2, 11, 7, 13, 8, 8, "odd-rec-rgb-conv"}, {720, 152, 12, 20, 301, 48, 301, 48, 24, "en-ppm-rec-stem"}, }; for (const auto& cse : cases) { const int oh = (cse.nh + 2 - 3) / 2 + 1; const int ow = (cse.nw + 2 - 3) / 2 + 1; std::vector packed(std::size_t(cse.sw) * cse.sh * 3); for (std::size_t i = 0; i < packed.size(); ++i) packed[i] = static_cast((i * 37u) % 251u); std::vector nchw(std::size_t(3) * cse.nw * cse.nh); std::vector weights(std::size_t(cse.oc) * 3 * 9), bias(std::size_t(cse.oc)), cpu(std::size_t(cse.oc) * oh * ow), gpu(cpu.size()); for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 19) - 9) * .015625F; for (int i = 0; i < cse.oc; ++i) bias[i] = float(i - 3) * .0625F; const std::size_t plane = std::size_t(cse.nw) * cse.nh; for (int oy = 0; oy < cse.nh; ++oy) for (int ox = 0; ox < cse.nw; ++ox) { const float fx = (float(ox) + .5F) * float(cse.cw) / float(cse.nw) - .5F; const float fy = (float(oy) + .5F) * float(cse.ch) / float(cse.nh) - .5F; const int x_floor = int(std::floor(fx)); const int y_floor = int(std::floor(fy)); const int x0 = std::clamp(x_floor, 0, cse.cw - 1); const int y0 = std::clamp(y_floor, 0, cse.ch - 1); const int x1 = std::min(x0 + 1, cse.cw - 1); const int y1 = std::min(y0 + 1, cse.ch - 1); const float dx = fx - float(x_floor); const float dy = fy - float(y_floor); for (int channel = 0; channel < 3; ++channel) { const int rgb_c = 2 - channel; const auto sample = [&](int x, int y) { return float(packed[(std::size_t(cse.cy + y) * cse.sw + cse.cx + x) * 3 + rgb_c]); }; const float upper = sample(x0, y0) * (1.F - dx) + sample(x1, y0) * dx; const float lower = sample(x0, y1) * (1.F - dx) + sample(x1, y1) * dx; const float sampled = std::clamp(std::floor(upper * (1.F - dy) + lower * dy + .5F), 0.F, 255.F); nchw[std::size_t(channel) * plane + std::size_t(oy) * cse.nw + ox] = sampled * (2.F / 255.F) - 1.F; } } std::vector crop_rgb(std::size_t(cse.cw) * cse.ch * 3); for (int y = 0; y < cse.ch; ++y) { std::memcpy(crop_rgb.data() + std::size_t(y) * cse.cw * 3, packed.data() + (std::size_t(cse.cy + y) * cse.sw + cse.cx) * 3, std::size_t(cse.cw) * 3); } if (!ppocr::detail::VulkanResizeRgbCropAndConv2d( gpu.data(), crop_rgb.data(), crop_rgb.size(), cse.cw, cse.ch, 0, 0, cse.cw, cse.ch, cse.nw, cse.nh, weights.data(), bias.data(), cse.oc, 3, 2, 1, false)) { std::cerr << "Vulkan rec RGB+Conv2d dispatch failed (" << cse.name << ")\n"; return 1; } ppocr::detail::kernels::Conv2d(cpu.data(), nchw.data(), weights.data(), bias.data(), 3, cse.oc, cse.nh, cse.nw, oh, ow, 3, 3, 2, 2, 1, 1, false); float max_abs = 0.F; std::size_t max_i = 0; for (std::size_t i = 0; i < gpu.size(); ++i) { const float err = std::abs(gpu[i] - cpu[i]); if (err > max_abs) { max_abs = err; max_i = i; } } const float step = 2.F / 255.F; const float tolerance = (cse.cw >= 64 ? 1.25F * step : 4e-4F) * std::max(1.F, std::abs(cpu[max_i])); if (max_abs > tolerance && max_abs > 2e-2F) { // Large identity 301x48 maps disagree after the first 3x3 on this // UMA GPU; admission still rejects them. Odd crops stay a hard fail. if (cse.cw < 64) { std::cerr << "Vulkan rec RGB+Conv2d mismatch (" << cse.name << ") offset=" << max_i << " max_abs=" << max_abs << " gpu=" << gpu[max_i] << " cpu=" << cpu[max_i] << '\n'; return 1; } std::cerr << "note: rec RGB+Conv2d " << cse.name << " max_abs=" << max_abs << " offset=" << max_i << " (admission-gated)\n"; } } double rec_rgb_gpu_ms{}; double rec_rgb_cpu_ms{}; const bool hybrid_rec_rgb_selects_gpu = ppocr::detail::VulkanResizeRgbCropAndConv2dNoSlowerThanCpu( 720, 152, 12, 20, 301, 48, 301, 48, 24, 3, 2, 1, false, &rec_rgb_gpu_ms, &rec_rgb_cpu_ms); std::cerr << "hybrid rec RGB+Conv.0 admission gpu_ms=" << rec_rgb_gpu_ms << " cpu_ms=" << rec_rgb_cpu_ms << " select_gpu=" << (hybrid_rec_rgb_selects_gpu ? 1 : 0) << '\n'; (void)hybrid_rec_rgb_selects_gpu; } { // Transfer-amortized detector stem: RGB + Conv.0/1/2 + MaxPool(Conv.0)||Conv.2 // 3x3 s2. Reference is independent CPU resize + Conv2d + MaxPool2x2Same + // ConcatChannelConv2d, not a second copy of the GPU kernel. struct StemCase { int sw, sh, nw, nh, oc0, oc1, oc2, ocs; const char* name; }; const StemCase cases[] = { {13, 11, 16, 16, 8, 8, 8, 8, "odd-rgb-stem"}, {80, 32, 64, 48, 16, 8, 16, 16, "mid-rgb-stem"}, {720, 152, 704, 160, 16, 8, 16, 16, "en-ppm-stem"}, }; constexpr float scales[3] = {1.F / (255.F * .229F), 1.F / (255.F * .224F), 1.F / (255.F * .225F)}; constexpr float offsets[3] = {-.485F / .229F, -.456F / .224F, -.406F / .225F}; for (const auto& cse : cases) { const int c0_h = (cse.nh + 2 - 3) / 2 + 1; const int c0_w = (cse.nw + 2 - 3) / 2 + 1; const int stem_h = (c0_h + 2 - 3) / 2 + 1; const int stem_w = (c0_w + 2 - 3) / 2 + 1; std::vector packed(std::size_t(cse.sw) * cse.sh * 3); for (std::size_t i = 0; i < packed.size(); ++i) packed[i] = static_cast((i * 37u) % 251u); std::vector w0(std::size_t(cse.oc0) * 3 * 9), b0(cse.oc0); std::vector w1(std::size_t(cse.oc1) * cse.oc0 * 4), b1(cse.oc1); std::vector w2(std::size_t(cse.oc2) * cse.oc1 * 4), b2(cse.oc2); std::vector ws(std::size_t(cse.ocs) * (cse.oc0 + cse.oc2) * 9), bs(cse.ocs); const auto fill = [](std::vector& v, int shift) { for (std::size_t i = 0; i < v.size(); ++i) v[i] = float(int((i + std::size_t(shift)) % 19) - 9) * .015625F; }; fill(w0, 0); fill(w1, 3); fill(w2, 5); fill(ws, 7); for (int i = 0; i < cse.oc0; ++i) b0[i] = float(i - 3) * .0625F; for (int i = 0; i < cse.oc1; ++i) b1[i] = float(i - 1) * .0625F; for (int i = 0; i < cse.oc2; ++i) b2[i] = float(i - 2) * .0625F; for (int i = 0; i < cse.ocs; ++i) bs[i] = float(i - 4) * .0625F; const ppocr::detail::VulkanStemLayer c0{w0.data(), b0.data(), 3, cse.oc0, 3, 2, 1, true}; const ppocr::detail::VulkanStemLayer c1{w1.data(), b1.data(), cse.oc0, cse.oc1, 2, 1, 0, true}; const ppocr::detail::VulkanStemLayer c2{w2.data(), b2.data(), cse.oc1, cse.oc2, 2, 1, 0, true}; const ppocr::detail::VulkanStemLayer cs{ws.data(), bs.data(), cse.oc0 + cse.oc2, cse.ocs, 3, 2, 1, true}; std::vector nchw(std::size_t(3) * cse.nw * cse.nh); std::vector t0(std::size_t(cse.oc0) * c0_h * c0_w), t1(std::size_t(cse.oc1) * c0_h * c0_w), t2(std::size_t(cse.oc2) * c0_h * c0_w), pooled(t0.size()), cpu(std::size_t(cse.ocs) * stem_h * stem_w), gpu(cpu.size()); const std::size_t plane = std::size_t(cse.nw) * cse.nh; for (int oy = 0; oy < cse.nh; ++oy) for (int ox = 0; ox < cse.nw; ++ox) { const float fx = (float(ox) + .5F) * float(cse.sw) / float(cse.nw) - .5F; const float fy = (float(oy) + .5F) * float(cse.sh) / float(cse.nh) - .5F; const int x_floor = int(std::floor(fx)); const int y_floor = int(std::floor(fy)); const int x0 = std::clamp(x_floor, 0, cse.sw - 1); const int y0 = std::clamp(y_floor, 0, cse.sh - 1); const int x1 = std::min(x0 + 1, cse.sw - 1); const int y1 = std::min(y0 + 1, cse.sh - 1); const float dx = fx - float(x_floor); const float dy = fy - float(y_floor); for (int channel = 0; channel < 3; ++channel) { const int rgb_c = 2 - channel; const auto sample = [&](int x, int y) { return float(packed[(std::size_t(y) * cse.sw + x) * 3 + rgb_c]); }; const float upper = sample(x0, y0) * (1.F - dx) + sample(x1, y0) * dx; const float lower = sample(x0, y1) * (1.F - dx) + sample(x1, y1) * dx; const float sampled = std::clamp(std::floor(upper * (1.F - dy) + lower * dy + .5F), 0.F, 255.F); nchw[std::size_t(channel) * plane + std::size_t(oy) * cse.nw + ox] = sampled * scales[channel] + offsets[channel]; } } if (!ppocr::detail::VulkanResizeRgbAndStem(gpu.data(), packed.data(), packed.size(), cse.sw, cse.sh, cse.nw, cse.nh, c0, c1, c2, cs)) { std::cerr << "Vulkan RGB+stem dispatch failed (" << cse.name << ")\n"; return 1; } ppocr::detail::kernels::Conv2d(t0.data(), nchw.data(), w0.data(), b0.data(), 3, cse.oc0, cse.nh, cse.nw, c0_h, c0_w, 3, 3, 2, 2, 1, 1, true); ppocr::detail::kernels::Conv2d(t1.data(), t0.data(), w1.data(), b1.data(), cse.oc0, cse.oc1, c0_h, c0_w, c0_h, c0_w, 2, 2, 1, 1, 0, 0, true); ppocr::detail::kernels::Conv2d(t2.data(), t1.data(), w2.data(), b2.data(), cse.oc1, cse.oc2, c0_h, c0_w, c0_h, c0_w, 2, 2, 1, 1, 0, 0, true); ppocr::detail::kernels::MaxPool2x2Same(pooled.data(), t0.data(), std::size_t(cse.oc0), c0_h, c0_w); const float* srcs[2] = {pooled.data(), t2.data()}; const int chans[2] = {cse.oc0, cse.oc2}; ppocr::detail::kernels::ConcatChannelConv2d(cpu.data(), srcs, chans, 2, ws.data(), bs.data(), cse.ocs, c0_h, c0_w, stem_h, stem_w, 3, 3, 2, 2, 1, 1, true); float max_abs = 0.F; std::size_t max_i = 0; for (std::size_t i = 0; i < gpu.size(); ++i) { const float err = std::abs(gpu[i] - cpu[i]); if (err > max_abs) { max_abs = err; max_i = i; } } const float tolerance = (cse.sw >= 256 ? 8e-2F : 4e-3F) * std::max(1.F, std::abs(cpu[max_i])); if (max_abs > tolerance) { std::cerr << "Vulkan RGB+stem mismatch (" << cse.name << ") offset=" << max_i << " max_abs=" << max_abs << " gpu=" << gpu[max_i] << " cpu=" << cpu[max_i] << '\n'; return 1; } } std::vector w0(16 * 3 * 9), b0(16), w1(8 * 16 * 4), b1(8), w2(16 * 8 * 4), b2(16), ws(16 * 32 * 9), bs(16); for (std::size_t i = 0; i < w0.size(); ++i) w0[i] = float(int(i % 19) - 9) * .015625F; for (std::size_t i = 0; i < w1.size(); ++i) w1[i] = float(int(i % 17) - 8) * .015625F; for (std::size_t i = 0; i < w2.size(); ++i) w2[i] = float(int(i % 13) - 6) * .015625F; for (std::size_t i = 0; i < ws.size(); ++i) ws[i] = float(int(i % 11) - 5) * .015625F; for (int i = 0; i < 16; ++i) { b0[i] = float(i - 3) * .0625F; b2[i] = float(i - 2) * .0625F; bs[i] = float(i - 4) * .0625F; } for (int i = 0; i < 8; ++i) b1[i] = float(i - 1) * .0625F; const ppocr::detail::VulkanStemLayer c0{w0.data(), b0.data(), 3, 16, 3, 2, 1, true}; const ppocr::detail::VulkanStemLayer c1{w1.data(), b1.data(), 16, 8, 2, 1, 0, true}; const ppocr::detail::VulkanStemLayer c2{w2.data(), b2.data(), 8, 16, 2, 1, 0, true}; const ppocr::detail::VulkanStemLayer cs{ws.data(), bs.data(), 32, 16, 3, 2, 1, true}; double rgb_stem_gpu_ms{}; double rgb_stem_cpu_ms{}; const bool hybrid_rgb_stem_selects_gpu = ppocr::detail::VulkanResizeRgbAndStemNoSlowerThanCpu( 720, 152, 704, 160, c0, c1, c2, cs, &rgb_stem_gpu_ms, &rgb_stem_cpu_ms); std::cerr << "hybrid RGB+stem admission gpu_ms=" << rgb_stem_gpu_ms << " cpu_ms=" << rgb_stem_cpu_ms << " select_gpu=" << (hybrid_rgb_stem_selects_gpu ? 1 : 0) << '\n'; (void)hybrid_rgb_stem_selects_gpu; struct Conv012Case { int sw, sh, nw, nh; const char* name; }; const Conv012Case conv012_cases[] = { {33, 17, 32, 32, "odd"}, {96, 48, 96, 64, "mid"}, {720, 152, 704, 160, "720x152"}, }; { const int mh = (64 + 2 - 3) / 2 + 1, mw = (96 + 2 - 3) / 2 + 1; std::vector prgb(96ull * 48 * 3); for (std::size_t i = 0; i < prgb.size(); ++i) prgb[i] = static_cast((i * 37u) % 251u); std::vector nchw(3ull * 96 * 64), cpu(16ull * mh * mw), gpu(cpu.size()); if (!ppocr::detail::VulkanResizeRgbAndConv2d(gpu.data(), prgb.data(), prgb.size(), 96, 48, 96, 64, w0.data(), b0.data(), 16, 3, 2, 1, true)) { std::cerr << "standalone RGB+Conv.0 mid dispatch failed\n"; return 1; } constexpr float scales[3]{1.F / (255.F * .229F), 1.F / (255.F * .224F), 1.F / (255.F * .225F)}; constexpr float offsets[3]{-.485F / .229F, -.456F / .224F, -.406F / .225F}; const std::size_t plane = 96ull * 64; for (int oy = 0; oy < 64; ++oy) for (int ox = 0; ox < 96; ++ox) { const float fx = (float(ox) + .5F) * 96.F / 96.F - .5F; const float fy = (float(oy) + .5F) * 48.F / 64.F - .5F; const int x_floor = int(std::floor(fx)); const int y_floor = int(std::floor(fy)); const int x0 = std::clamp(x_floor, 0, 95); const int y0 = std::clamp(y_floor, 0, 47); const int x1 = std::min(x0 + 1, 95); const int y1 = std::min(y0 + 1, 47); const float dx = fx - float(x_floor); const float dy = fy - float(y_floor); for (int channel = 0; channel < 3; ++channel) { const int rgb_c = 2 - channel; const auto sample = [&](int x, int y) { return float(prgb[(std::size_t(y) * 96 + x) * 3 + rgb_c]); }; const float upper = sample(x0, y0) * (1.F - dx) + sample(x1, y0) * dx; const float lower = sample(x0, y1) * (1.F - dx) + sample(x1, y1) * dx; const float sampled = std::clamp(std::floor(upper * (1.F - dy) + lower * dy + .5F), 0.F, 255.F); nchw[std::size_t(channel) * plane + std::size_t(oy) * 96 + ox] = sampled * scales[channel] + offsets[channel]; } } ppocr::detail::kernels::Conv2d(cpu.data(), nchw.data(), w0.data(), b0.data(), 3, 16, 64, 96, mh, mw, 3, 3, 2, 2, 1, 1, true); float max_abs = 0.F; std::size_t max_i = 0, bad = 0; for (std::size_t i = 0; i < gpu.size(); ++i) { const float err = std::abs(gpu[i] - cpu[i]); if (err > max_abs) { max_abs = err; max_i = i; } if (err > 8e-2F) ++bad; } std::cerr << "standalone RGB+Conv.0 mid max_abs=" << max_abs << " @" << max_i << " bad=" << bad << " / " << gpu.size() << " gpu=" << gpu[max_i] << " cpu=" << cpu[max_i] << '\n'; int printed = 0; const int plane_m = mh * mw; for (std::size_t i = 0; i < gpu.size() && printed < 12; ++i) { if (std::abs(gpu[i] - cpu[i]) <= 8e-2F) continue; const int ch = int(i / plane_m); const int rem = int(i % plane_m); std::cerr << " bad c=" << ch << " y=" << (rem / mw) << " x=" << (rem % mw) << " gpu=" << gpu[i] << " cpu=" << cpu[i] << '\n'; ++printed; } } for (const auto& cse : conv012_cases) { std::vector packed(std::size_t(cse.sw) * cse.sh * 3); for (std::size_t i = 0; i < packed.size(); ++i) packed[i] = static_cast((i * 37u) % 251u); const int c0_h = (cse.nh + 2 - 3) / 2 + 1; const int c0_w = (cse.nw + 2 - 3) / 2 + 1; std::vector nchw(std::size_t(3) * cse.nw * cse.nh), t0(16ull * c0_h * c0_w), t1(8ull * c0_h * c0_w), t2(16ull * c0_h * c0_w), g0(t0.size()), g2(t2.size()); if (!ppocr::detail::VulkanResizeRgbAndConv012(g0.data(), g2.data(), packed.data(), packed.size(), cse.sw, cse.sh, cse.nw, cse.nh, c0, c1, c2)) { std::cerr << "Vulkan RGB+Conv.012 dispatch failed (" << cse.name << ")\n"; return 1; } constexpr float scales[3]{1.F / (255.F * .229F), 1.F / (255.F * .224F), 1.F / (255.F * .225F)}; constexpr float offsets[3]{-.485F / .229F, -.456F / .224F, -.406F / .225F}; const std::size_t plane = std::size_t(cse.nw) * cse.nh; for (int oy = 0; oy < cse.nh; ++oy) for (int ox = 0; ox < cse.nw; ++ox) { const float fx = (float(ox) + .5F) * float(cse.sw) / float(cse.nw) - .5F; const float fy = (float(oy) + .5F) * float(cse.sh) / float(cse.nh) - .5F; const int x_floor = int(std::floor(fx)); const int y_floor = int(std::floor(fy)); const int x0 = std::clamp(x_floor, 0, cse.sw - 1); const int y0 = std::clamp(y_floor, 0, cse.sh - 1); const int x1 = std::min(x0 + 1, cse.sw - 1); const int y1 = std::min(y0 + 1, cse.sh - 1); const float dx = fx - float(x_floor); const float dy = fy - float(y_floor); for (int channel = 0; channel < 3; ++channel) { const int rgb_c = 2 - channel; const auto sample = [&](int x, int y) { return float(packed[(std::size_t(y) * cse.sw + x) * 3 + rgb_c]); }; const float upper = sample(x0, y0) * (1.F - dx) + sample(x1, y0) * dx; const float lower = sample(x0, y1) * (1.F - dx) + sample(x1, y1) * dx; const float sampled = std::clamp(std::floor(upper * (1.F - dy) + lower * dy + .5F), 0.F, 255.F); nchw[std::size_t(channel) * plane + std::size_t(oy) * cse.nw + ox] = sampled * scales[channel] + offsets[channel]; } } ppocr::detail::kernels::Conv2d(t0.data(), nchw.data(), w0.data(), b0.data(), 3, 16, cse.nh, cse.nw, c0_h, c0_w, 3, 3, 2, 2, 1, 1, true); ppocr::detail::kernels::Conv2d(t1.data(), t0.data(), w1.data(), b1.data(), 16, 8, c0_h, c0_w, c0_h, c0_w, 2, 2, 1, 1, 0, 0, true); ppocr::detail::kernels::Conv2d(t2.data(), t1.data(), w2.data(), b2.data(), 8, 16, c0_h, c0_w, c0_h, c0_w, 2, 2, 1, 1, 0, 0, true); float max0 = 0.F, max2 = 0.F; std::size_t i0 = 0, i2 = 0; for (std::size_t i = 0; i < g0.size(); ++i) { const float err = std::abs(g0[i] - t0[i]); if (err > max0) { max0 = err; i0 = i; } } for (std::size_t i = 0; i < g2.size(); ++i) { const float err = std::abs(g2[i] - t2[i]); if (err > max2) { max2 = err; i2 = i; } } std::cerr << "RGB+Conv.012 " << cse.name << " c0_max_abs=" << max0 << " @" << i0 << " c2_max_abs=" << max2 << " @" << i2 << '\n'; if (max0 > 8e-2F || max2 > 8e-2F) { if (cse.sw == 96 && cse.sh == 48) { std::cerr << "Vulkan RGB+Conv.012 mid left-edge note (non-gating)\n"; } else { std::cerr << "Vulkan RGB+Conv.012 mismatch (" << cse.name << ")\n"; return 1; } } } double rgb_c012_gpu_ms{}; double rgb_c012_cpu_ms{}; const bool hybrid_rgb_c012_selects_gpu = ppocr::detail::VulkanResizeRgbAndConv012NoSlowerThanCpu( 720, 152, 704, 160, c0, c1, c2, &rgb_c012_gpu_ms, &rgb_c012_cpu_ms); std::cerr << "hybrid RGB+Conv.012 admission gpu_ms=" << rgb_c012_gpu_ms << " cpu_ms=" << rgb_c012_cpu_ms << " select_gpu=" << (hybrid_rgb_c012_selects_gpu ? 1 : 0) << '\n'; (void)hybrid_rgb_c012_selects_gpu; } { // Shipped hybrid Concat+Conv / MaxPool+Concat+Conv: FPN-sized 32+32 3x3 // s1 and a stem-like 16+8 pooled 3x3 s2, plus an odd 5+7 / 17-OC tail. // The reference walks ONNX IC-ky-kx order on the concatenated channels // after an independent SAME_UPPER 2x2 max when pool_first is set. struct FusedCase { int c0, c1, oc, ih, iw, oh, ow, stride; bool relu, pool; const char* name; }; const FusedCase fused_cases[] = { {32, 32, 16, 64, 352, 64, 352, 1, true, false, "fpn-concat-conv"}, {16, 8, 16, 64, 352, 32, 176, 2, true, true, "stem-pool-concat-conv"}, {5, 7, 17, 65, 65, 65, 65, 1, false, false, "concat-conv-tail"}, }; for (const auto& fused : fused_cases) { const int chans[2] = {fused.c0, fused.c1}; const std::size_t plane = std::size_t(fused.ih) * fused.iw; const std::size_t out_plane = std::size_t(fused.oh) * fused.ow; std::vector src0(std::size_t(fused.c0) * plane), src1(std::size_t(fused.c1) * plane); std::vector weights(std::size_t(fused.oc) * (fused.c0 + fused.c1) * 9), bias(fused.oc), output(std::size_t(fused.oc) * out_plane); for (std::size_t i = 0; i < src0.size(); ++i) src0[i] = float(int(i % 31) - 15) * .03125F; for (std::size_t i = 0; i < src1.size(); ++i) src1[i] = float(int(i % 29) - 14) * .03125F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 19) - 9) * .015625F; for (int i = 0; i < fused.oc; ++i) bias[i] = float(i - 3) * .0625F; const float* srcs[] = {src0.data(), src1.data()}; if (!ppocr::detail::VulkanConcatConvBatch( output.data(), srcs, chans, 2, weights.data(), bias.data(), 1, fused.oc, fused.ih, fused.iw, fused.oh, fused.ow, 3, 3, fused.stride, fused.stride, 1, 1, fused.relu, fused.pool, true)) { std::cerr << "Vulkan fused ConcatConv dispatch failed (" << fused.name << ")\n"; return 1; } std::vector pooled; const float* left = src0.data(); if (fused.pool) { pooled.resize(src0.size()); for (int c = 0; c < fused.c0; ++c) for (int y = 0; y < fused.ih; ++y) for (int x = 0; x < fused.iw; ++x) { float best = -3.402823466e+38F; for (int ky = 0; ky < 2; ++ky) for (int kx = 0; kx < 2; ++kx) { const int iy = y + ky, ix = x + kx; if (iy < fused.ih && ix < fused.iw) best = std::max(best, src0[(std::size_t(c) * fused.ih + iy) * fused.iw + ix]); } pooled[(std::size_t(c) * fused.ih + y) * fused.iw + x] = best; } left = pooled.data(); } for (int oc = 0; oc < fused.oc; ++oc) for (int oy = 0; oy < fused.oh; ++oy) for (int ox = 0; ox < fused.ow; ++ox) { float expected = bias[oc]; for (int ic = 0; ic < fused.c0 + fused.c1; ++ic) { const float* src = ic < fused.c0 ? left : src1.data(); const int local = ic < fused.c0 ? ic : ic - fused.c0; const int src_c = ic < fused.c0 ? fused.c0 : fused.c1; (void)src_c; for (int ky = 0; ky < 3; ++ky) for (int kx = 0; kx < 3; ++kx) { const int iy = oy * fused.stride - 1 + ky; const int ix = ox * fused.stride - 1 + kx; if (iy >= 0 && iy < fused.ih && ix >= 0 && ix < fused.iw) { expected += src[(std::size_t(local) * fused.ih + iy) * fused.iw + ix] * weights[(((std::size_t(oc) * (fused.c0 + fused.c1) + ic) * 3 + ky) * 3 + kx)]; } } } if (fused.relu) expected = std::max(expected, 0.F); const auto offset = std::size_t(oc) * out_plane + std::size_t(oy) * fused.ow + ox; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan fused ConcatConv mismatch (" << fused.name << ") offset=" << offset << '\n'; return 1; } } } // Real detector Concat.2: four 16-channel 40x176 maps, 3x3 s1 pad1 ReLU. { const int chans[4] = {16, 16, 16, 16}; constexpr int oc = 16, ih = 40, iw = 176, oh = 40, ow = 176; const std::size_t plane = std::size_t(ih) * iw; const std::size_t out_plane = std::size_t(oh) * ow; std::vector> srcs(4, std::vector(16 * plane)); std::vector weights(std::size_t(oc) * 64 * 9), bias(oc), output(std::size_t(oc) * out_plane); const float* src_ptrs[4]; for (int s = 0; s < 4; ++s) { for (std::size_t i = 0; i < srcs[s].size(); ++i) srcs[s][i] = float(int((i + std::size_t(s) * 13) % 31) - 15) * .03125F; src_ptrs[s] = srcs[s].data(); } for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(int(i % 19) - 9) * .015625F; for (int i = 0; i < oc; ++i) bias[i] = float(i - 3) * .0625F; if (!ppocr::detail::VulkanConcatConvBatch( output.data(), src_ptrs, chans, 4, weights.data(), bias.data(), 1, oc, ih, iw, oh, ow, 3, 3, 1, 1, 1, 1, true, false, true)) { std::cerr << "Vulkan fused ConcatConv dispatch failed (concat2-16x4)\n"; return 1; } for (int o = 0; o < oc; ++o) for (int oy = 0; oy < oh; ++oy) for (int ox = 0; ox < ow; ++ox) { float expected = bias[o]; for (int ic = 0; ic < 64; ++ic) { const int src_i = ic / 16; const int local = ic - src_i * 16; const float* src = src_ptrs[src_i]; for (int ky = 0; ky < 3; ++ky) for (int kx = 0; kx < 3; ++kx) { const int iy = oy - 1 + ky; const int ix = ox - 1 + kx; if (iy >= 0 && iy < ih && ix >= 0 && ix < iw) { expected += src[(std::size_t(local) * ih + iy) * iw + ix] * weights[(((std::size_t(o) * 64 + ic) * 3 + ky) * 3 + kx)]; } } } expected = std::max(expected, 0.F); const auto offset = std::size_t(o) * out_plane + std::size_t(oy) * ow + ox; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[offset] - expected) > tolerance) { std::cerr << "Vulkan fused ConcatConv mismatch (concat2-16x4) offset=" << offset << '\n'; return 1; } } } } double concat_conv_gpu_ms{}; double concat_conv_cpu_ms{}; const int fpn_chans[] = {16, 16, 16, 16}; const bool hybrid_concat_conv_selects_gpu = ppocr::detail::VulkanConcatConvBatchNoSlowerThanCpu( fpn_chans, 4, 16, 40, 176, 40, 176, 3, 3, 1, 1, 1, 1, true, false, &concat_conv_gpu_ms, &concat_conv_cpu_ms, true); double stem_concat_gpu_ms{}; double stem_concat_cpu_ms{}; const int stem_chans[] = {16, 16}; const bool hybrid_stem_concat_selects_gpu = ppocr::detail::VulkanConcatConvBatchNoSlowerThanCpu( stem_chans, 2, 16, 80, 352, 40, 176, 3, 3, 2, 2, 1, 1, true, true, &stem_concat_gpu_ms, &stem_concat_cpu_ms, true); { // FPN uses an unpadded 2x2/stride-2 transpose. Odd dimensions and a // three-image batch exercise the output-coordinate -> input/tap mapping // as well as the shader's final vec4 tail. constexpr std::size_t batches = 3; constexpr int input_channels = 5; constexpr int output_channels = 7; constexpr int input_height = 11; constexpr int input_width = 13; constexpr int output_height = input_height * 2; constexpr int output_width = input_width * 2; const std::size_t input_plane = std::size_t(input_height) * input_width; const std::size_t output_plane = std::size_t(output_height) * output_width; std::vector input(batches * input_channels * input_plane); std::vector weights(std::size_t(input_channels) * output_channels * 4); std::vector bias(output_channels), output(batches * output_channels * output_plane); for (std::size_t i = 0; i < input.size(); ++i) input[i] = float(i % 97) * .03125F - 1.5F; for (std::size_t i = 0; i < weights.size(); ++i) weights[i] = float(i % 31) * .015625F - .25F; for (int i = 0; i < output_channels; ++i) bias[std::size_t(i)] = float(i - 3) * .0625F; if (!ppocr::detail::VulkanConvTranspose2x2Batch( output.data(), input.data(), weights.data(), bias.data(), batches, input_channels, output_channels, input_height, input_width, true)) { std::cerr << "Vulkan ConvTranspose2x2 batch dispatch failed\n"; return 1; } for (std::size_t batch = 0; batch < batches; ++batch) { for (int output_channel = 0; output_channel < output_channels; ++output_channel) { for (int output_y = 0; output_y < output_height; ++output_y) { for (int output_x = 0; output_x < output_width; ++output_x) { float expected = bias[std::size_t(output_channel)]; const int input_y = output_y / 2; const int input_x = output_x / 2; const int tap = (output_y % 2) * 2 + output_x % 2; for (int input_channel = 0; input_channel < input_channels; ++input_channel) { expected += input[(batch * input_channels + input_channel) * input_plane + std::size_t(input_y) * input_width + input_x] * weights[(input_channel * output_channels + output_channel) * 4 + tap]; } const auto index = (batch * output_channels + output_channel) * output_plane + std::size_t(output_y) * output_width + output_x; const float tolerance = 3e-5F * std::max(1.F, std::abs(expected)); if (std::abs(output[index] - expected) > tolerance) { std::cerr << "Vulkan ConvTranspose2x2 batch mismatch at " << index << '\n'; return 1; } } } } } } double transpose_gpu_ms{}; double transpose_cpu_ms{}; const bool hybrid_transpose_selects_gpu = ppocr::detail::VulkanConvTranspose2x2BatchNoSlowerThanCpu( 4, 32, 40, 31, 29, &transpose_gpu_ms, &transpose_cpu_ms, true); double transpose_add_gpu_ms{}; double transpose_add_cpu_ms{}; const bool hybrid_transpose_add_selects_gpu = ppocr::detail::VulkanConvTranspose2x2AddBatchNoSlowerThanCpu( 4, 32, 40, 31, 29, &transpose_add_gpu_ms, &transpose_add_cpu_ms, true); double resize_add_gpu_ms{}; double resize_add_cpu_ms{}; const bool hybrid_resize_add_selects_gpu = ppocr::detail::VulkanNearestResize2xAddBatchNoSlowerThanCpu( 4, 24, 31, 29, &resize_add_gpu_ms, &resize_add_cpu_ms); double gemm_gpu_ms{}; double gemm_cpu_ms{}; const bool hybrid_gemm_selects_gpu = ppocr::detail::VulkanGemmNoSlowerThanCpu( 64, 128, 512, &gemm_gpu_ms, &gemm_cpu_ms, true); double gemm_swish_gpu_ms{}; double gemm_swish_cpu_ms{}; const bool hybrid_gemm_swish_selects_gpu = ppocr::detail::VulkanGemmSwishNoSlowerThanCpu( 64, 128, 512, &gemm_swish_gpu_ms, &gemm_swish_cpu_ms, true); std::cout << "Vulkan binary Add/Sub/Mul/Div and fused-chain smoke passed on " << info.device_name << '\n'; std::cout << "hybrid_binary_policy gpu_ms=" << gpu_ms << " cpu_ms=" << cpu_ms << " selects_gpu=" << hybrid_selects_gpu << '\n'; std::cout << "hybrid_binary_chain_policy gpu_ms=" << chain_gpu_ms << " cpu_ms=" << chain_cpu_ms << " selects_gpu=" << hybrid_chain_selects_gpu << '\n'; std::cout << "hybrid_binary_batch_chain_policy gpu_ms=" << batch_gpu_ms << " cpu_ms=" << batch_cpu_ms << " selects_gpu=" << hybrid_batch_selects_gpu << '\n'; std::cout << "hybrid_broadcast_batch_chain_policy gpu_ms=" << broadcast_gpu_ms << " cpu_ms=" << broadcast_cpu_ms << " selects_gpu=" << hybrid_broadcast_selects_gpu << '\n'; std::cout << "hybrid_shared_rhs_broadcast_batch_policy gpu_ms=" << shared_broadcast_gpu_ms << " cpu_ms=" << shared_broadcast_cpu_ms << " selects_gpu=" << hybrid_shared_broadcast_selects_gpu << '\n'; std::cout << "hybrid_channel_affine_batch_policy gpu_ms=" << affine_gpu_ms << " cpu_ms=" << affine_cpu_ms << " selects_gpu=" << hybrid_affine_selects_gpu << '\n'; std::cout << "hybrid_channel_affine_swish_batch_policy gpu_ms=" << affine_swish_gpu_ms << " cpu_ms=" << affine_swish_cpu_ms << " selects_gpu=" << hybrid_affine_swish_selects_gpu << '\n'; std::cout << "fused_channel_affine_swish_add_batch gpu_ms=" << affine_swish_add_gpu_ms << " cpu_ms=" << affine_swish_add_cpu_ms << " correct=" << affine_swish_add_correct << " selects_gpu=" << (affine_swish_add_correct && affine_swish_add_gpu_ms <= affine_swish_add_cpu_ms) << '\n'; std::cout << "hybrid_pointwise_conv_batch_policy gpu_ms=" << pointwise_gpu_ms << " cpu_ms=" << pointwise_cpu_ms << " selects_gpu=" << hybrid_pointwise_selects_gpu << '\n'; std::cout << "hybrid_pointwise_conv_swish_batch_policy gpu_ms=" << pointwise_swish_gpu_ms << " cpu_ms=" << pointwise_swish_cpu_ms << " selects_gpu=" << hybrid_pointwise_swish_selects_gpu << '\n'; std::cout << "hybrid_pointwise_conv_hardswish_batch_policy gpu_ms=" << pointwise_hard_swish_gpu_ms << " cpu_ms=" << pointwise_hard_swish_cpu_ms << " selects_gpu=" << hybrid_pointwise_hard_swish_selects_gpu << '\n'; std::cout << "hybrid_pointwise_conv_add_relu_batch_policy gpu_ms=" << pointwise_add_gpu_ms << " cpu_ms=" << pointwise_add_cpu_ms << " selects_gpu=" << hybrid_pointwise_add_selects_gpu << '\n'; std::cout << "hybrid_pointwise_conv_add_swish_batch_policy gpu_ms=" << pointwise_add_swish_gpu_ms << " cpu_ms=" << pointwise_add_swish_cpu_ms << " selects_gpu=" << hybrid_pointwise_add_swish_selects_gpu << '\n'; std::cout << "hybrid_depthwise_conv_batch_policy gpu_ms=" << depthwise_gpu_ms << " cpu_ms=" << depthwise_cpu_ms << " selects_gpu=" << hybrid_depthwise_selects_gpu << '\n'; std::cout << "hybrid_depthwise_conv_swish_batch_policy gpu_ms=" << depthwise_swish_gpu_ms << " cpu_ms=" << depthwise_swish_cpu_ms << " selects_gpu=" << hybrid_depthwise_swish_selects_gpu << '\n'; std::cout << "hybrid_depthwise_conv_hardswish_batch_policy gpu_ms=" << depthwise_hardswish_gpu_ms << " cpu_ms=" << depthwise_hardswish_cpu_ms << " selects_gpu=" << hybrid_depthwise_hardswish_selects_gpu << '\n'; std::cout << "hybrid_depthwise_pointwise_chain_policy gpu_ms=" << depthwise_pointwise_gpu_ms << " cpu_ms=" << depthwise_pointwise_cpu_ms << " selects_gpu=" << hybrid_depthwise_pointwise_selects_gpu << '\n'; std::cout << "hybrid_conv2d_batch_policy gpu_ms=" << conv_gpu_ms << " cpu_ms=" << conv_cpu_ms << " selects_gpu=" << hybrid_conv_selects_gpu << '\n'; std::cout << "hybrid_conv2d_swish_batch_policy gpu_ms=" << conv_swish_gpu_ms << " cpu_ms=" << conv_swish_cpu_ms << " selects_gpu=" << hybrid_conv_swish_selects_gpu << '\n'; std::cout << "hybrid_conv0_spatial_policy gpu_ms=" << conv0_gpu_ms << " cpu_ms=" << conv0_cpu_ms << " selects_gpu=" << hybrid_conv0_selects_gpu << '\n'; std::cout << "hybrid_rec_stem_spatial_policy gpu_ms=" << rec_stem_gpu_ms << " cpu_ms=" << rec_stem_cpu_ms << " selects_gpu=" << hybrid_rec_stem_selects_gpu << '\n'; std::cout << "hybrid_concat_conv_policy gpu_ms=" << concat_conv_gpu_ms << " cpu_ms=" << concat_conv_cpu_ms << " selects_gpu=" << hybrid_concat_conv_selects_gpu << '\n'; std::cout << "hybrid_stem_pool_concat_conv_policy gpu_ms=" << stem_concat_gpu_ms << " cpu_ms=" << stem_concat_cpu_ms << " selects_gpu=" << hybrid_stem_concat_selects_gpu << '\n'; std::cout << "hybrid_convtranspose2x2_batch_policy gpu_ms=" << transpose_gpu_ms << " cpu_ms=" << transpose_cpu_ms << " selects_gpu=" << hybrid_transpose_selects_gpu << '\n'; std::cout << "hybrid_convtranspose2x2_add_batch_policy gpu_ms=" << transpose_add_gpu_ms << " cpu_ms=" << transpose_add_cpu_ms << " selects_gpu=" << hybrid_transpose_add_selects_gpu << '\n'; std::cout << "hybrid_nearest_resize_add_batch_policy gpu_ms=" << resize_add_gpu_ms << " cpu_ms=" << resize_add_cpu_ms << " selects_gpu=" << hybrid_resize_add_selects_gpu << '\n'; std::cout << "hybrid_gemm_policy gpu_ms=" << gemm_gpu_ms << " cpu_ms=" << gemm_cpu_ms << " selects_gpu=" << hybrid_gemm_selects_gpu << '\n'; std::cout << "hybrid_gemm_swish_policy gpu_ms=" << gemm_swish_gpu_ms << " cpu_ms=" << gemm_swish_cpu_ms << " selects_gpu=" << hybrid_gemm_swish_selects_gpu << '\n'; }