mirror of
https://github.com/RapidAI/RapidOCR.git
synced 2026-11-11 05:20:49 +08:00
3562 lines
204 KiB
C++
3562 lines
204 KiB
C++
#include "vulkan_backend.hpp"
|
|
#include "kernels.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
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<float> 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<float> 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<float> 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<std::size_t> concat_axes{2, 3, 1, 4};
|
|
std::vector<std::vector<float>> concat_host(4);
|
|
std::vector<ppocr::detail::VulkanTensorSlot> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> host_conv_input(conv_input_channels * conv_input_plane);
|
|
std::vector<float> host_conv_weights(std::size_t(conv_output_channels) * conv_input_channels *
|
|
conv_kernel_height * conv_kernel_width);
|
|
std::vector<float> 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<float> 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<float> 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<float> host_up_input(up_input_channels * up_input_plane);
|
|
std::vector<float> host_up_weights(std::size_t(up_input_channels) * up_output_channels * 4);
|
|
std::vector<float> 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<float> host_chain_in(chain_in_n), host_w0(std::size_t(chain_cin) * chain_mid * 4);
|
|
std::vector<float> host_b0(chain_mid), host_w1(std::size_t(chain_mid) * chain_cout * 4);
|
|
std::vector<float> host_b1(chain_cout), host_packed(host_w1.size() + host_b1.size());
|
|
std::vector<float> 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<std::size_t>(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<std::size_t>(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<float> host_resize_source(resize_channels * resize_input_plane);
|
|
std::vector<float> 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<float> host_gemm_left(gemm_rows * gemm_depth);
|
|
std::vector<float> host_gemm_right(gemm_depth * gemm_columns);
|
|
std::vector<float> 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<float> host_at(std::size_t(t_depth) * t_rows);
|
|
std::vector<float> host_b(std::size_t(t_depth) * t_cols);
|
|
std::vector<float> 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<float> host_at(std::size_t(t_depth) * t_rows);
|
|
std::vector<float> host_b(std::size_t(t_depth) * t_cols);
|
|
std::vector<float> 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<float> 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<float> 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<int, 4> transpose_dims{2, 3, 2, 5};
|
|
constexpr std::array<int, 4> transpose_perm{2, 0, 3, 1};
|
|
constexpr std::size_t transpose_elements = 2 * 3 * 2 * 5;
|
|
std::vector<float> 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<int, 4> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float>::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<float> 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<float> stem_a(stem_c0 * stem_h * stem_w), stem_b(stem_c1 * stem_h * stem_w);
|
|
std::vector<float> stem_weights(stem_m * (stem_c0 + stem_c1) * 9), stem_bias(stem_m);
|
|
std::vector<float> 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<float> 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<float> src(std::size_t(channels) * plane);
|
|
std::vector<float> expand_w(std::size_t(hidden) * channels);
|
|
std::vector<float> expand_b(hidden), project_b(channels);
|
|
std::vector<float> project_w(std::size_t(channels) * hidden);
|
|
std::vector<float> 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<float> 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<float> 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<ppocr::detail::kernels::BinaryOp> 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<float> 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<ppocr::detail::kernels::BinaryOp> 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<float> left0(count * batches), left1(count * batches);
|
|
std::vector<float> 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<ppocr::detail::kernels::BinaryOp> 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<float> 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<float> 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<float> 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<float> left(std::size_t(cse.rows) * cse.depth),
|
|
right(std::size_t(cse.depth) * cse.vocab), bias(cse.vocab);
|
|
std::vector<int> cpu_idx(cse.rows), gpu_idx(cse.rows);
|
|
std::vector<float> 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<float> source(batches * channels * input_plane);
|
|
std::vector<float> residual(batches * channels * output_plane);
|
|
std::vector<float> 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<float> input(batches * input_channels * input_plane);
|
|
std::vector<float> weights(std::size_t(input_channels) * output_channels * 4);
|
|
std::vector<float> bias(output_channels);
|
|
std::vector<float> residual(batches * output_channels * output_plane);
|
|
std::vector<float> 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<float> 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<float> left0(count * batches), left1(count * batches);
|
|
std::vector<float> 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<float> left0(count * batches), left1(count * batches);
|
|
std::vector<float> 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<float> 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<float> input(batches * input_channels * plane);
|
|
std::vector<float> weights(std::size_t(input_channels) * output_channels);
|
|
std::vector<float> 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<float> 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<ppocr::detail::kernels::BinaryOp> 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<float> 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<float> 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<float> 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<float> 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<float> 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<float> 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<double, std::milli>(
|
|
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<double, std::milli>(
|
|
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<std::uint8_t> packed(std::size_t(rgb.sw) * rgb.sh * 3);
|
|
for (std::size_t i = 0; i < packed.size(); ++i)
|
|
packed[i] = static_cast<std::uint8_t>((i * 37u) % 251u);
|
|
std::vector<float> 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<std::uint8_t> packed(std::size_t(sw) * sh * 3);
|
|
for (std::size_t i = 0; i < packed.size(); ++i)
|
|
packed[i] = static_cast<std::uint8_t>((i * 37u) % 251u);
|
|
std::vector<float> 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<float> 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<std::uint8_t> packed(std::size_t(cse.sw) * cse.sh * 3);
|
|
for (std::size_t i = 0; i < packed.size(); ++i)
|
|
packed[i] = static_cast<std::uint8_t>((i * 37u) % 251u);
|
|
std::vector<float> nchw(std::size_t(3) * cse.nw * cse.nh);
|
|
std::vector<float> 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<std::uint8_t> packed(std::size_t(cse.sw) * cse.sh * 3);
|
|
for (std::size_t i = 0; i < packed.size(); ++i)
|
|
packed[i] = static_cast<std::uint8_t>((i * 37u) % 251u);
|
|
std::vector<float> nchw(std::size_t(3) * cse.nw * cse.nh);
|
|
std::vector<float> 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<std::uint8_t> 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<std::uint8_t> packed(std::size_t(cse.sw) * cse.sh * 3);
|
|
for (std::size_t i = 0; i < packed.size(); ++i)
|
|
packed[i] = static_cast<std::uint8_t>((i * 37u) % 251u);
|
|
std::vector<float> w0(std::size_t(cse.oc0) * 3 * 9), b0(cse.oc0);
|
|
std::vector<float> w1(std::size_t(cse.oc1) * cse.oc0 * 4), b1(cse.oc1);
|
|
std::vector<float> w2(std::size_t(cse.oc2) * cse.oc1 * 4), b2(cse.oc2);
|
|
std::vector<float> ws(std::size_t(cse.ocs) * (cse.oc0 + cse.oc2) * 9), bs(cse.ocs);
|
|
const auto fill = [](std::vector<float>& 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<float> nchw(std::size_t(3) * cse.nw * cse.nh);
|
|
std::vector<float> 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<float> 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<std::uint8_t> prgb(96ull * 48 * 3);
|
|
for (std::size_t i = 0; i < prgb.size(); ++i)
|
|
prgb[i] = static_cast<std::uint8_t>((i * 37u) % 251u);
|
|
std::vector<float> 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<std::uint8_t> packed(std::size_t(cse.sw) * cse.sh * 3);
|
|
for (std::size_t i = 0; i < packed.size(); ++i)
|
|
packed[i] = static_cast<std::uint8_t>((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<float> 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<float> src0(std::size_t(fused.c0) * plane), src1(std::size_t(fused.c1) * plane);
|
|
std::vector<float> 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<float> 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<std::vector<float>> srcs(4, std::vector<float>(16 * plane));
|
|
std::vector<float> 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<float> input(batches * input_channels * input_plane);
|
|
std::vector<float> weights(std::size_t(input_channels) * output_channels * 4);
|
|
std::vector<float> 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';
|
|
}
|