voice_audio_multi/tests/unittests/test_neon_library.cpp
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

515 lines
25 KiB
C++

#include "engine/framework/neon/blocks.h"
#include "test_assert.h"
#include <algorithm>
#include <cmath>
#include <future>
#include <iostream>
#include <stdexcept>
namespace arm = engine::neon;
namespace {
std::vector<float> values(size_t size) {
std::vector<float> result(size);
for (size_t i = 0; i < size; ++i) {
result[i] = 0.15F * std::sin(float(i) * 0.21F);
}
return result;
}
void close(float actual, double expected) {
engine::test::require(std::isfinite(actual) && std::abs(actual - expected) < 4e-5 * (1 + std::abs(expected)),
"native library scalar reference");
}
void linear() {
for (int features : {3, 16, 31, 32, 320}) {
constexpr int outputs = 7;
const auto weight = values(features * outputs), bias = values(outputs);
const arm::Linear operation(features, outputs, weight, bias);
for (int frames : {1, 3, 17}) {
const auto input = values(features * frames);
for (auto order : {arm::Layout::ChannelFirst, arm::Layout::FrameMajor}) {
arm::ExecutionPlan plan;
const auto source = plan.input();
plan.set_output(plan.add(operation, {source}), order);
for (int threads : {1, 4, 7}) {
const auto &actual =
plan.run({arm::TensorView(input, arm::Shape({features, frames}, order))}, threads);
for (int row = 0; row < outputs; ++row) {
for (int frame = 0; frame < frames; ++frame) {
double expected = bias[row];
for (int feature = 0; feature < features; ++feature) {
const size_t index = order == arm::Layout::FrameMajor ? frame * features + feature
: feature * frames + frame;
expected += double(input[index]) * weight[row * features + feature];
}
close(actual.values[order == arm::Layout::FrameMajor ? frame * outputs + row
: row * frames + frame],
expected);
}
}
}
}
}
}
}
void quantized_linear() {
for (int features : {32, 64, 320}) {
constexpr int outputs = 7;
arm::BlockInt8Weights weights;
weights.values.resize(features * outputs);
weights.scales.resize(features * outputs / 32);
for (size_t i = 0; i < weights.values.size(); ++i) {
weights.values[i] = static_cast<int8_t>(int(i % 255) - 127);
}
for (size_t i = 0; i < weights.scales.size(); ++i) {
weights.scales[i] = 0.001F * float(i % 5 + 1);
}
const arm::QuantizedLinear operation(features, outputs, weights);
for (int frames : {1, 3, 17}) {
const auto input = values(features * frames);
for (auto order : {arm::Layout::ChannelFirst, arm::Layout::FrameMajor}) {
arm::ExecutionPlan plan;
const auto source = plan.input();
plan.set_output(plan.add(operation, {source}), order);
for (int threads : {1, 4, 7}) {
const auto &actual =
plan.run({arm::TensorView(input, arm::Shape({features, frames}, order))}, threads);
for (int row = 0; row < outputs; ++row) {
for (int frame = 0; frame < frames; ++frame) {
double expected = 0;
for (int block = 0; block < features / 32; ++block) {
float maximum = 0;
for (int i = 0; i < 32; ++i) {
const int feature = block * 32 + i;
const size_t index = order == arm::Layout::FrameMajor ? frame * features + feature
: feature * frames + frame;
maximum = std::max(maximum, std::abs(input[index]));
}
const float scale = maximum / 127;
int exponent = 0;
std::frexp(scale, &exponent);
const double step = std::ldexp(1.0, std::max(exponent - 11, -24));
const double rounded = std::nearbyint(scale / step) * step;
int dot = 0;
for (int i = 0; i < 32; ++i) {
const int feature = block * 32 + i;
const size_t index = order == arm::Layout::FrameMajor ? frame * features + feature
: feature * frames + frame;
dot += weights.values[row * features + feature] *
static_cast<int>(std::nearbyint(input[index] * (scale ? 1.0F / scale : 0)));
}
expected += dot * rounded * weights.scales[row * (features / 32) + block];
}
close(actual.values[order == arm::Layout::FrameMajor ? frame * outputs + row
: row * frames + frame],
expected);
}
}
}
}
}
}
}
void embedding_indices() {
arm::BlockInt8Weights weight;
weight.values.resize(3 * 64);
weight.scales.assign(6, 0.025F);
for (size_t i = 0; i < weight.values.size(); ++i) {
weight.values[i] = static_cast<int8_t>(int(i % 255) - 127);
}
arm::ExecutionPlan plan;
const auto input = plan.input();
plan.set_output(plan.add(arm::Embedding(64, 3, weight), {input}));
for (int32_t row : {2, 0, 1, 2}) {
for (int threads : {1, 4, 7}) {
const auto &output = plan.run({arm::TensorView(&row, 1, {1})}, threads);
for (int i = 0; i < 64; ++i) {
close(output.values[i], weight.values[row * 64 + i] * double(weight.scales[row * 2 + i / 32]));
}
}
}
int32_t invalid = 3;
bool rejected = false;
try {
plan.run({arm::TensorView(&invalid, 1, {1})}, 4);
} catch (const std::invalid_argument &) {
rejected = true;
}
engine::test::require(rejected, "embedding rejects out-of-range index before dispatch");
invalid = 0;
plan.run({arm::TensorView(&invalid, 1, {1})}, 4);
}
void decoder_operators() {
for (auto layout : {arm::Layout::ChannelFirst, arm::Layout::FrameMajor}) {
arm::ExecutionPlan rope;
const auto features = rope.input(), position = rope.input();
rope.set_output(rope.add(arm::RotaryEmbedding(5, 4, 10000), {features, position}), layout);
const auto input = values(10 * 3);
int32_t start = 7;
const auto &actual =
rope.run({arm::TensorView(input, arm::Shape({10, 3}, layout)), arm::TensorView(&start, 1, {1})}, 4);
for (int head = 0; head < 2; ++head) {
for (int frame = 0; frame < 3; ++frame) {
auto offset = [&](int d) {
return layout == arm::Layout::FrameMajor ? frame * 10 + head * 5 + d : (head * 5 + d) * 3 + frame;
};
for (int pair = 0; pair < 2; ++pair) {
const double angle = (start + frame) * std::pow(10000.0, -2.0 * pair / 4);
close(actual.values[offset(2 * pair)],
input[offset(2 * pair)] * std::cos(angle) - input[offset(2 * pair + 1)] * std::sin(angle));
close(actual.values[offset(2 * pair + 1)],
input[offset(2 * pair)] * std::sin(angle) + input[offset(2 * pair + 1)] * std::cos(angle));
}
close(actual.values[offset(4)], input[offset(4)]);
}
}
}
constexpr int heads = 4, kv_heads = 2, dimension = 5, capacity = 7;
const auto q = values(heads * dimension), keys = values(capacity * kv_heads * dimension);
auto vals = values(keys.size()), current_key = values(kv_heads * dimension), current_value = current_key;
for (float &v : vals) {
v += 0.3F;
}
for (float &v : current_value) {
v -= 0.2F;
}
arm::ExecutionPlan attention;
const auto query = attention.input(), key = attention.input(), value = attention.input();
const auto new_key = attention.input(), new_value = attention.input(), position = attention.input();
attention.set_output(
attention.add(arm::Attention(heads, kv_heads, dimension), {query, key, value, new_key, new_value, position}));
for (int32_t index : {0, 1, 5, 6, 0}) {
for (int threads : {1, 4, 7}) {
const auto &result = attention.run(
{arm::TensorView(q, {heads * dimension, 1}),
arm::TensorView(keys, arm::Shape({kv_heads * dimension, capacity}, arm::Layout::FrameMajor)),
arm::TensorView(vals, arm::Shape({kv_heads * dimension, capacity}, arm::Layout::FrameMajor)),
arm::TensorView(current_key, {kv_heads * dimension, 1}),
arm::TensorView(current_value, {kv_heads * dimension, 1}), arm::TensorView(&index, 1, {1})},
threads);
for (int h = 0; h < heads; ++h) {
const int kh = h / (heads / kv_heads);
std::vector<double> probabilities(index + 1);
double total = 0;
for (int frame = 0; frame <= index; ++frame) {
double score = 0;
for (int d = 0; d < dimension; ++d) {
const float k = frame == index ? current_key[kh * dimension + d]
: keys[(frame * kv_heads + kh) * dimension + d];
score += double(q[h * dimension + d]) * k;
}
probabilities[frame] = std::exp(score / std::sqrt(double(dimension)));
total += probabilities[frame];
}
for (int d = 0; d < dimension; ++d) {
double expected = 0;
for (int frame = 0; frame <= index; ++frame) {
const float v = frame == index ? current_value[kh * dimension + d]
: vals[(frame * kv_heads + kh) * dimension + d];
expected += probabilities[frame] / total * v;
}
close(result.values[h * dimension + d], expected);
}
}
}
}
arm::ExecutionPlan retained;
const auto input = retained.input();
const auto intermediate = retained.add(arm::Activation(arm::ActivationKind::SiLU), {input});
retained.retain(intermediate);
retained.set_output(retained.add(arm::Activation(arm::ActivationKind::SiLU), {intermediate}));
const auto source = values(17);
retained.run({arm::TensorView(source, {17, 1})}, 4);
const auto view = retained.view(intermediate);
for (size_t i = 0; i < source.size(); ++i) {
close(view.data[i], source[i] / (1 + std::exp(-double(source[i]))));
}
arm::ExecutionPlan glu;
const auto gates = glu.input();
glu.set_output(glu.add(arm::SwiGLU(), {gates}));
const auto packed = values(14 * 3);
const auto &output = glu.run({arm::TensorView(packed, {14, 3})}, 7);
for (size_t i = 0; i < output.values.size(); ++i) {
const double gate = packed[output.values.size() + i];
close(output.values[i], packed[i] * gate / (1 + std::exp(-gate)));
}
}
void convolution() {
for (int kernel : {1, 2, 3, 7}) {
for (int stride : {1, 2}) {
for (int dilation : {1, 2}) {
constexpr int channels = 5, output_channels = 7;
const int padding = dilation * kernel / 2;
arm::Conv1dConfig config{channels, output_channels, kernel, stride, dilation, padding, padding};
const auto weight = values(channels * output_channels * kernel), bias = values(output_channels);
const arm::Conv1d operation(config, weight, bias);
arm::ExecutionPlan plan;
const auto input = plan.input();
plan.set_output(plan.add(operation, {input}));
for (int frames : {17, 3, 63, 1, 17}) {
for (int threads : {1, 4, 2, 7}) {
const auto source = values(channels * frames);
const auto &result = plan.run({arm::TensorView(source, {channels, frames})}, threads);
const int output_frames = result.shape.dimensions[1];
for (int out = 0; out < output_channels; ++out) {
for (int frame = 0; frame < output_frames; ++frame) {
double expected = bias[out];
for (int channel = 0; channel < channels; ++channel) {
for (int tap = 0; tap < kernel; ++tap) {
const int position = frame * stride + tap * dilation - padding;
if (position >= 0 && position < frames) {
expected += double(weight[(out * channels + channel) * kernel + tap]) *
source[channel * frames + position];
}
}
}
close(result.values[out * output_frames + frame], expected);
}
}
}
}
}
}
}
}
void branching_and_layout() {
arm::ExecutionPlan plan;
const auto input = plan.input();
const auto first = plan.add(arm::Activation(arm::ActivationKind::SiLU), {input});
const auto second = plan.add(arm::Activation(arm::ActivationKind::GELU), {input});
const auto sum = plan.add(arm::ScaleAdd({0.3F, -0.2F}), {first, second});
const auto constant = plan.add(arm::ConstantChannels(1, 2.0F), {sum});
const auto joined = plan.add(arm::ConcatChannels(), {sum, constant});
plan.set_output(joined, arm::Layout::FrameMajor);
for (int threads : {1, 4, 7, 2, 1}) {
for (int frames : {1, 17, 3, 63, 17}) {
const auto source = values(2 * frames);
const auto &result = plan.run({arm::TensorView(source, {2, frames})}, threads);
for (int frame = 0; frame < frames; ++frame) {
for (int channel = 0; channel < 2; ++channel) {
const double x = source[channel * frames + frame];
const double silu = x / (1 + std::exp(-x));
const double gelu = 0.5 * x * (1 + std::erf(x / std::sqrt(2.0)));
close(result.values[frame * 3 + channel], silu + (channel == 0 ? 0.3 : -0.2) * gelu);
}
close(result.values[frame * 3 + 2], 2.0);
}
}
}
plan.set_output(input);
const std::vector<float> source{1, 2, 3, 4};
engine::test::require(plan.run({arm::TensorView(source, {2, 2})}, 4).values == source,
"borrowed input output preserved");
}
void convolution_2d() {
constexpr int channels = 2, height = 7, width = 9, out_channels = 3;
arm::Conv2dConfig config{channels, out_channels, 3, 3, 1, 1, 1, 1, 1, 1};
const auto weight = values(channels * out_channels * 9), bias = values(out_channels),
source = values(channels * height * width);
arm::ExecutionPlan plan;
const auto input = plan.input();
auto value = plan.add(arm::Conv2d(config, weight, bias), {input});
value = plan.add(arm::Activation(arm::ActivationKind::LeakyReLU, 0.2F), {value});
value = plan.add(arm::ClampMin(-0.02F), {value});
value = plan.add(arm::MaxPool2d(2, 2, 2, 2), {value});
plan.set_output(value);
std::vector<double> reference(out_channels * height * width);
for (int out = 0; out < out_channels; ++out) {
for (int h = 0; h < height; ++h) {
for (int w = 0; w < width; ++w) {
double value = bias[out];
for (int channel = 0; channel < channels; ++channel) {
for (int kh = 0; kh < 3; ++kh) {
for (int kw = 0; kw < 3; ++kw) {
const int ih = h + kh - 1, iw = w + kw - 1;
if (ih >= 0 && ih < height && iw >= 0 && iw < width) {
value += double(weight[((out * channels + channel) * 3 + kh) * 3 + kw]) *
source[(channel * height + ih) * width + iw];
}
}
}
}
reference[(out * height + h) * width + w] = std::max(value < 0 ? value * 0.2 : value, -0.02);
}
}
}
for (int threads : {1, 4, 2, 7}) {
const auto &result = plan.run({arm::TensorView(source, {channels, height, width})}, threads);
for (int out = 0; out < out_channels; ++out) {
for (int h = 0; h < 3; ++h) {
for (int w = 0; w < 4; ++w) {
double expected = -1e30;
for (int kh = 0; kh < 2; ++kh) {
for (int kw = 0; kw < 2; ++kw) {
expected = std::max(expected, reference[(out * height + h * 2 + kh) * width + w * 2 + kw]);
}
}
close(result.values[(out * 3 + h) * 4 + w], expected);
}
}
}
}
}
void validation() {
auto rejected = [](const auto &operation) {
bool failed = false;
try {
operation();
} catch (const std::invalid_argument &) {
failed = true;
}
engine::test::require(failed, "invalid library contract rejected");
};
rejected([] { arm::Conv2d({1, 2, 3, 3}, {}, {}); });
rejected([] { arm::Conv1d({1, 2, 3}, {}, {}); });
rejected([] { arm::Linear(3, 2, {}); });
rejected([] { arm::QuantizedLinear(31, 2, {}); });
rejected([] { arm::QuantizedLinear(32, 2, {std::vector<int8_t>(64), {1, -1}}); });
rejected([] { arm::TensorView(std::vector<float>{1}, {2, 2}); });
rejected([] {
arm::ExecutionPlan a, b;
const auto input = a.input();
b.add(arm::Activation(arm::ActivationKind::GELU), {input});
});
arm::ExecutionPlan plan;
const auto input = plan.input();
plan.set_output(plan.add(arm::LayerNorm({1, 1}, {0, 0}, 1e-6F), {input}));
rejected([&] { plan.run({arm::TensorView(std::vector<float>{1, 2, 3}, {3, 1})}, 4); });
rejected([&] { plan.run({arm::TensorView(std::vector<float>{1, 2, 3}, {3, 1})}, 4); });
const auto &result = plan.run({arm::TensorView(std::vector<float>{1, 3}, {2, 1})}, 4);
close(result.values[0], -1 / std::sqrt(1 + 1e-6));
close(result.values[1], 1 / std::sqrt(1 + 1e-6));
rejected([&] { plan.run({arm::TensorView(std::vector<float>{1, 2, 3}, {3, 1})}, 4); });
rejected([&] { plan.run({arm::TensorView(std::vector<float>{1, 2, 3}, {3, 1})}, 4); });
close(plan.run({arm::TensorView(std::vector<float>{1, 3}, {2, 1})}, 4).values[0], -1 / std::sqrt(1 + 1e-6));
}
void output_layout_conversion() {
const std::vector<float> source{1, 2, 3, 4, 5, 6};
arm::ExecutionPlan plan;
const auto input = plan.input();
for (int threads : {1, 4, 7}) {
plan.set_output(input, arm::Layout::ChannelFirst);
const auto &result =
plan.run({arm::TensorView(source, arm::Shape({2, 3}, arm::Layout::FrameMajor))}, threads);
engine::test::require(result.values == std::vector<float>({1, 3, 5, 2, 4, 6}),
"executor converts frame-major output to channel-first");
plan.set_output(input, arm::Layout::FrameMajor);
engine::test::require(
plan.run({arm::TensorView(source, arm::Shape({2, 3}, arm::Layout::FrameMajor))}, threads).values ==
source,
"executor preserves frame-major output");
}
}
void blocks_and_shared_weights() {
constexpr int channels = 3, expanded = 5, kernel = 3;
const arm::ConvNeXtWeights weight{values(channels * kernel),
values(channels),
std::vector<float>(channels, 1),
values(channels),
values(expanded * channels),
values(expanded),
values(channels * expanded),
values(channels),
values(channels)};
arm::ExecutionPlan plan;
const auto input = plan.input();
plan.set_output(arm::build_convnext(plan, input, channels, expanded, kernel, 1e-5F, weight));
for (int frames : {1, 17, 63, 2}) {
for (int threads : {1, 4, 7}) {
const auto source = values(channels * frames);
std::vector<double> normalized(channels * frames), hidden(expanded * frames);
for (int frame = 0; frame < frames; ++frame) {
std::vector<double> convolution(channels);
double mean = 0, variance = 0;
for (int channel = 0; channel < channels; ++channel) {
convolution[channel] = weight.depthwise_bias[channel];
for (int tap = 0; tap < kernel; ++tap) {
const int position = frame + tap - kernel / 2;
if (position >= 0 && position < frames) {
convolution[channel] += double(weight.depthwise_weight[channel * kernel + tap]) *
source[channel * frames + position];
}
}
mean += convolution[channel];
}
mean /= channels;
for (double value : convolution) {
variance += (value - mean) * (value - mean);
}
const double inverse = 1 / std::sqrt(variance / channels + 1e-5F);
for (int channel = 0; channel < channels; ++channel) {
normalized[channel * frames + frame] =
(convolution[channel] - mean) * inverse * weight.norm_weight[channel] +
weight.norm_bias[channel];
}
for (int out = 0; out < expanded; ++out) {
double value = weight.expand_bias[out];
for (int channel = 0; channel < channels; ++channel) {
value += double(weight.expand_weight[out * channels + channel]) *
normalized[channel * frames + frame];
}
hidden[out * frames + frame] = 0.5 * value * (1 + std::erf(value / std::sqrt(2.0)));
}
}
const auto &result = plan.run({arm::TensorView(source, {channels, frames})}, threads);
for (int channel = 0; channel < channels; ++channel) {
for (int frame = 0; frame < frames; ++frame) {
double projected = weight.project_bias[channel];
for (int hidden_channel = 0; hidden_channel < expanded; ++hidden_channel) {
projected += double(weight.project_weight[channel * expanded + hidden_channel]) *
hidden[hidden_channel * frames + frame];
}
close(result.values[channel * frames + frame],
source[channel * frames + frame] + weight.scale[channel] * projected);
}
}
}
}
const arm::Conv1d shared({2, 3, 1}, values(6), values(3));
auto run = [&] {
arm::ExecutionPlan local;
const auto input = local.input();
local.set_output(local.add(shared, {input}));
return local.run({arm::TensorView(values(34), {2, 17})}, 4).values;
};
auto first = std::async(std::launch::async, run);
const auto second = run();
engine::test::require(first.get() == second, "shared immutable operator with independent execution workspaces");
}
} // namespace
int main() {
try {
linear();
quantized_linear();
decoder_operators();
embedding_indices();
convolution();
branching_and_layout();
convolution_2d();
validation();
output_layout_conversion();
blocks_and_shared_weights();
std::cout << "NEON library contracts and scalar parity passed\n";
return 0;
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}
}