voice_audio_multi/tests/unittests/test_encoder_modules.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

1385 lines
69 KiB
C++

#include "engine/framework/core/backend.h"
#include "engine/framework/core/backend_weight_store.h"
#include "engine/framework/debug/profiler.h"
#include "engine/framework/modules/attention_modules.h"
#include "engine/framework/modules/activation_modules.h"
#include "engine/framework/modules/conditioning_modules.h"
#include "engine/framework/modules/conformer_modules.h"
#include "engine/framework/modules/convnext_modules.h"
#include "engine/framework/modules/primitive_modules.h"
#include "engine/framework/modules/positional_modules.h"
#include "engine/framework/modules/structural_modules.h"
#include "engine/framework/modules/weight_binding.h"
#include "engine/framework/runtime/graph_optimizer.h"
#include <cmath>
#include <cstring>
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <unordered_map>
namespace {
constexpr size_t kTestGraphBytes = 128 * 1024 * 1024;
constexpr size_t kTestGraphNodes = 4096;
void require(bool condition, const std::string & message) {
if (!condition) {
throw std::runtime_error(message);
}
}
void require_allclose(
const std::vector<float> & actual,
const std::vector<float> & expected,
float atol,
const std::string & label) {
if (actual.size() != expected.size()) {
throw std::runtime_error(label + " size mismatch");
}
for (size_t i = 0; i < actual.size(); ++i) {
const float diff = std::fabs(actual[i] - expected[i]);
if (!std::isfinite(diff) || diff > atol) {
std::ostringstream oss;
oss << label << " mismatch at " << i << ": expected " << expected[i] << ", got " << actual[i]
<< ", diff=" << diff;
throw std::runtime_error(oss.str());
}
}
}
void require_max_abs_diff_below(
const std::vector<float> & actual,
const std::vector<float> & expected,
float max_allowed,
double mean_allowed,
const std::string & label) {
if (actual.size() != expected.size()) {
throw std::runtime_error(label + " size mismatch");
}
float max_diff = 0.0f;
size_t max_index = 0;
double mean_diff = 0.0;
for (size_t i = 0; i < actual.size(); ++i) {
const float diff = std::fabs(actual[i] - expected[i]);
mean_diff += diff;
if (diff > max_diff) {
max_diff = diff;
max_index = i;
}
}
mean_diff /= static_cast<double>(actual.size());
if (max_diff > max_allowed || mean_diff > mean_allowed) {
std::ostringstream oss;
oss << label << " drift exceeds bounds: max diff " << max_diff << " (limit " << max_allowed << ")"
<< ", mean diff=" << mean_diff << " (limit " << mean_allowed << ")"
<< " at " << max_index << " (expected " << expected[max_index]
<< ", got " << actual[max_index] << ")";
throw std::runtime_error(oss.str());
}
}
std::vector<float> project_sequence(
const std::vector<float> & input,
int64_t rows,
int64_t in_features,
const std::vector<float> & weight,
int64_t out_features) {
std::vector<float> output(static_cast<size_t>(rows * out_features), 0.0f);
for (int64_t row = 0; row < rows; ++row) {
for (int64_t out = 0; out < out_features; ++out) {
float sum = 0.0f;
for (int64_t in = 0; in < in_features; ++in) {
sum += input[static_cast<size_t>(row * in_features + in)] *
weight[static_cast<size_t>(out * in_features + in)];
}
output[static_cast<size_t>(row * out_features + out)] = sum;
}
}
return output;
}
struct CpuModuleRunner {
engine::core::BackendConfig backend_config{engine::core::BackendType::Cpu, 0, 4};
ggml_backend_t backend = nullptr;
ggml_backend_buffer_t buffer = nullptr;
ggml_context * ggml = nullptr;
engine::core::ModuleBuildContext ctx{};
CpuModuleRunner() {
backend = engine::core::init_backend(backend_config);
ggml_init_params params{};
params.mem_size = kTestGraphBytes;
params.mem_buffer = nullptr;
params.no_alloc = true;
ggml = ggml_init(params);
if (ggml == nullptr) {
throw std::runtime_error("failed to init test ggml context");
}
ctx.ggml = ggml;
ctx.module_instance_name = "encoder_module_test";
}
~CpuModuleRunner() {
if (buffer != nullptr) {
ggml_backend_buffer_free(buffer);
}
if (ggml != nullptr) {
ggml_free(ggml);
}
if (backend != nullptr) {
ggml_backend_free(backend);
}
}
engine::core::TensorValue make_f32(const engine::core::TensorShape & shape) {
return engine::core::make_tensor(ctx, GGML_TYPE_F32, shape);
}
engine::core::TensorValue make_i32(const engine::core::TensorShape & shape) {
return engine::core::make_tensor(ctx, GGML_TYPE_I32, shape);
}
void allocate_tensors() {
if (buffer != nullptr) {
return;
}
buffer = ggml_backend_alloc_ctx_tensors(ggml, backend);
if (buffer == nullptr) {
throw std::runtime_error("failed to allocate test backend tensors");
}
}
std::vector<float> run_f32(const engine::core::TensorValue & output) {
allocate_tensors();
ggml_cgraph * graph = ggml_new_graph_custom(ggml, kTestGraphNodes, false);
ggml_build_forward_expand(graph, output.tensor);
ggml_backend_graph_compute(backend, graph);
std::vector<float> values;
engine::core::read_tensor_f32_into(output.tensor, values);
return values;
}
};
using ModuleRunner = CpuModuleRunner;
std::vector<float> make_patterned_f32(size_t count, float phase, float scale) {
std::vector<float> values(count, 0.0f);
for (size_t i = 0; i < count; ++i) {
const float x = static_cast<float>(i);
values[i] = scale * (std::sin(phase + 0.173f * x) + 0.5f * std::cos(phase * 0.7f + 0.097f * x));
}
return values;
}
std::vector<float> make_global_attention_bias_for_test(int64_t frames, int64_t valid_frames) {
std::vector<float> mask(static_cast<size_t>(frames * frames), -std::numeric_limits<float>::infinity());
for (int64_t q = 0; q < valid_frames; ++q) {
for (int64_t k = 0; k < valid_frames; ++k) {
mask[static_cast<size_t>(q * frames + k)] = 0.0f;
}
}
for (int64_t q = valid_frames; q < frames; ++q) {
mask[static_cast<size_t>(q * frames + q)] = 0.0f;
}
return mask;
}
std::vector<int32_t> make_keep_mask_for_test(int64_t frames, int64_t valid_frames) {
std::vector<int32_t> mask(static_cast<size_t>(frames), 0);
for (int64_t i = 0; i < valid_frames; ++i) {
mask[static_cast<size_t>(i)] = 1;
}
return mask;
}
bool backend_is_available(engine::core::BackendType type) {
try {
ModuleRunner runner;
runner.backend_config.type = type;
if (runner.backend != nullptr) {
ggml_backend_free(runner.backend);
runner.backend = nullptr;
}
runner.backend = engine::core::init_backend(runner.backend_config);
return true;
} catch (...) {
return false;
}
}
engine::runtime::GraphOptimizationBackend graph_optimizer_backend_for_test(engine::core::BackendType type) {
switch (type) {
case engine::core::BackendType::Cpu:
return engine::runtime::GraphOptimizationBackend::Cpu;
case engine::core::BackendType::Cuda:
case engine::core::BackendType::Hip:
return engine::runtime::GraphOptimizationBackend::Gpu;
case engine::core::BackendType::Vulkan:
case engine::core::BackendType::Metal:
case engine::core::BackendType::WebGPU:
case engine::core::BackendType::BestAvailable:
return engine::runtime::GraphOptimizationBackend::Other;
}
return engine::runtime::GraphOptimizationBackend::Other;
}
void set_runner_backend(ModuleRunner & runner, engine::core::BackendType type) {
runner.backend_config.type = type;
if (runner.backend != nullptr) {
ggml_backend_free(runner.backend);
runner.backend = nullptr;
}
runner.backend = engine::core::init_backend(runner.backend_config);
}
void test_cuda_f16_lowerings_default_off() {
using namespace engine;
require(!modules::LinearConfig{}.cuda_f16_output, "FP16 Linear must be opt-in");
require(!modules::NormConfig{}.cuda_f16_output, "FP16 LayerNorm must be opt-in");
require(!modules::GroupNormConfig{}.cuda_f16_output, "FP16 GroupNorm must be opt-in");
require(!modules::Conv2dConfig{}.cuda_cudnn && !modules::Conv2dConfig{}.cuda_f16_output,
"cuDNN Conv2D and FP16 output must be opt-in");
static_assert(static_cast<int>(modules::ScaledDotProductAttentionLowering::ExplicitCpuPerHead) == 3);
(void) []() -> modules::SiluModule { return {}; }();
(void) []() -> modules::AddModule { return {}; }();
(void) []() -> modules::MulModule { return {}; }();
}
void test_mul_optional_broadcasting() {
using namespace engine;
using core::TensorShape;
struct Case {
TensorShape shape, rhs_shape;
std::vector<float> input, rhs, expected;
};
const std::vector<Case> cases = {
{TensorShape::from_dims({2, 3}), TensorShape::from_dims({2, 3}),
{1, -2, 3, 4, -5, 6}, {2, -1, .5F, 4, 2, -.5F}, {2, 2, 1.5F, 16, -10, -3}},
{TensorShape::from_dims({2, 3}), TensorShape::from_dims({1}),
{1, -2, 3, 4, -5, 6}, {-2}, {-2, 4, -6, -8, 10, -12}},
{TensorShape::from_dims({2, 3}), TensorShape::from_dims({1, 3}),
{1, -2, 3, 4, -5, 6}, {2, -1, .5F}, {2, 2, 1.5F, 8, 5, 3}},
{TensorShape::from_dims({2, 3}), TensorShape::from_dims({2, 1}),
{1, -2, 3, 4, -5, 6}, {2, -1}, {2, -4, 6, -4, 5, -6}},
{TensorShape::from_dims({2, 2, 3}), TensorShape::from_dims({1, 2, 1}),
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, {2, -1},
{2, 4, 6, -4, -5, -6, 14, 16, 18, -10, -11, -12}},
{TensorShape::from_dims({4, 2}), TensorShape::from_dims({2, 2}),
{1, 2, 3, 4, 5, 6, 7, 8}, {2, -1, 3, .5F}, {2, -2, 9, 2, 10, -6, 21, 4}},
};
for (size_t i = 0; i < cases.size(); ++i) {
const auto & c = cases[i];
CpuModuleRunner runner;
const auto input = runner.make_f32(c.shape), rhs = runner.make_f32(c.rhs_shape);
std::optional<core::TensorValue> strict;
bool rejected = false;
try {
strict = modules::MulModule().build(runner.ctx, input, rhs);
} catch (const std::runtime_error &) {
rejected = true;
}
require(rejected == (i != 0), "Mul default shape validation changed");
const auto output = modules::MulModule(true).build(runner.ctx, input, rhs);
core::validate_shape(output, c.shape, "Mul broadcast output");
require(output.type == GGML_TYPE_F32, "Mul output type changed");
auto * graph = ggml_new_graph_custom(runner.ggml, kTestGraphNodes, false);
ggml_build_forward_expand(graph, output.tensor);
for (int node = 0; node < ggml_graph_n_nodes(graph); ++node)
require(ggml_graph_node(graph, node)->op != GGML_OP_REPEAT, "Mul materialized a broadcast");
runner.allocate_tensors();
core::write_tensor_f32(input, c.input);
core::write_tensor_f32(rhs, c.rhs);
require_allclose(runner.run_f32(output), c.expected, 0, "Mul optional broadcast");
if (strict) require_allclose(runner.run_f32(*strict), c.expected, 0, "Mul unchanged default");
}
CpuModuleRunner runner;
const auto input = runner.make_f32(TensorShape::from_dims({2, 3}));
for (const auto & shape : {TensorShape::from_dims({4, 3}), TensorShape::from_dims({1, 2})}) {
bool rejected = false;
try {
(void)modules::MulModule(true).build(runner.ctx, input, runner.make_f32(shape));
} catch (const std::runtime_error &) {
rejected = true;
}
require(rejected, "Mul accepted incompatible RHS broadcasting");
}
}
void test_graph_optimizer_elides_metadata_nodes_without_changing_output() {
CpuModuleRunner runner;
auto input = runner.make_f32(engine::core::TensorShape::from_dims({2, 2}));
auto reshaped = engine::core::reshape_tensor(runner.ctx, input, engine::core::TensorShape::from_dims({4}));
auto output = engine::core::wrap_tensor(
ggml_add(runner.ctx.ggml, reshaped.tensor, reshaped.tensor),
reshaped.shape,
GGML_TYPE_F32);
ggml_cgraph * graph = ggml_new_graph_custom(runner.ggml, kTestGraphNodes, false);
ggml_build_forward_expand(graph, output.tensor);
const auto report = engine::runtime::optimize_graph(*graph);
require(report.nodes_before > report.nodes_after, "graph optimizer should reduce node count");
require(report.metadata_only_nodes_elided == 1, "graph optimizer should elide one reshape node");
runner.allocate_tensors();
engine::core::write_tensor_f32(input, {1.0f, -2.0f, 3.0f, -4.0f});
ggml_backend_graph_compute(runner.backend, graph);
std::vector<float> values;
engine::core::read_tensor_f32_into(output.tensor, values);
require_allclose(values, {2.0f, -4.0f, 6.0f, -8.0f}, 1.0e-6f, "graph optimizer output");
}
void test_graph_optimizer_two_sided_broadcast_binary_matches_repeat() {
auto run_case = [](engine::core::BackendType backend_type, const std::string & backend_label) {
ModuleRunner runner;
set_runner_backend(runner, backend_type);
auto full_shape = engine::core::TensorShape::from_dims({3, 4});
auto full_like = runner.make_f32(full_shape);
auto row = runner.make_f32(engine::core::TensorShape::from_dims({1, 4}));
auto col = runner.make_f32(engine::core::TensorShape::from_dims({3, 1}));
auto row_repeat = engine::core::wrap_tensor(
ggml_repeat(runner.ctx.ggml, row.tensor, full_like.tensor),
full_shape,
GGML_TYPE_F32);
auto output = engine::core::wrap_tensor(
ggml_add(runner.ctx.ggml, row_repeat.tensor, col.tensor),
full_shape,
GGML_TYPE_F32);
ggml_cgraph * graph = ggml_new_graph_custom(runner.ggml, kTestGraphNodes, false);
ggml_build_forward_expand(graph, output.tensor);
auto options = engine::runtime::graph_optimization_options_for_backend(
graph_optimizer_backend_for_test(backend_type),
true);
const auto report = engine::runtime::optimize_graph(*graph, options);
require(report.two_sided_broadcast_repeats_folded == 1,
"graph optimizer should fold one two-sided broadcast repeat on " + backend_label);
require(report.candidate_two_sided_broadcast_repeats == 0,
"two-sided broadcast candidate should be consumed on " + backend_label);
runner.allocate_tensors();
engine::core::write_tensor_f32(row, {10.0f, 20.0f, 30.0f, 40.0f});
engine::core::write_tensor_f32(col, {1.0f, 2.0f, 3.0f});
ggml_backend_graph_compute(runner.backend, graph);
std::vector<float> values;
engine::core::read_tensor_f32_into(output.tensor, values);
require_allclose(
values,
{
11.0f, 21.0f, 31.0f, 41.0f,
12.0f, 22.0f, 32.0f, 42.0f,
13.0f, 23.0f, 33.0f, 43.0f,
},
1.0e-6f,
"two-sided broadcast binary " + backend_label);
};
run_case(engine::core::BackendType::Cpu, "cpu");
if (backend_is_available(engine::core::BackendType::Cuda)) {
run_case(engine::core::BackendType::Cuda, "cuda");
} else {
std::cout << "encoder_module_test: skipping cuda two-sided broadcast\n";
}
}
void test_graph_optimizer_unary_broadcast_scale_matches_repeat() {
auto run_case = [](engine::core::BackendType backend_type, const std::string & backend_label) {
ModuleRunner runner;
set_runner_backend(runner, backend_type);
auto full_shape = engine::core::TensorShape::from_dims({3, 4});
auto full_like = runner.make_f32(full_shape);
auto scalar = runner.make_f32(engine::core::TensorShape::from_dims({1}));
auto scalar_repeat = engine::core::wrap_tensor(
ggml_repeat(runner.ctx.ggml, scalar.tensor, full_like.tensor),
full_shape,
GGML_TYPE_F32);
auto output = engine::core::wrap_tensor(
ggml_scale(runner.ctx.ggml, scalar_repeat.tensor, 2.5f),
full_shape,
GGML_TYPE_F32);
ggml_cgraph * graph = ggml_new_graph_custom(runner.ggml, kTestGraphNodes, false);
ggml_build_forward_expand(graph, output.tensor);
auto options = engine::runtime::graph_optimization_options_for_backend(
graph_optimizer_backend_for_test(backend_type),
true);
const auto report = engine::runtime::optimize_graph(*graph, options);
require(report.unary_broadcast_repeats_folded == 1,
"graph optimizer should fold one unary broadcast repeat on " + backend_label);
runner.allocate_tensors();
engine::core::write_tensor_f32(scalar, {4.0f});
ggml_backend_graph_compute(runner.backend, graph);
std::vector<float> values;
engine::core::read_tensor_f32_into(output.tensor, values);
require_allclose(
values,
{10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f, 10.0f},
1.0e-6f,
"unary broadcast scale " + backend_label);
};
run_case(engine::core::BackendType::Cpu, "cpu");
if (backend_is_available(engine::core::BackendType::Cuda)) {
run_case(engine::core::BackendType::Cuda, "cuda");
} else {
std::cout << "encoder_module_test: skipping cuda unary broadcast scale\n";
}
}
void test_relative_attention_fused_qkv_matches_split_and_cached_pos() {
const int64_t batch = 1;
const int64_t frames = 3;
const int64_t hidden = 4;
const int64_t heads = 2;
const int64_t pos_frames = 2 * frames - 1;
const std::vector<float> input_values = {
0.10f, 0.20f, -0.30f, 0.40f,
0.25f, -0.15f, 0.05f, 0.30f,
-0.10f, 0.35f, 0.45f, -0.20f,
};
const std::vector<float> pos_values = {
0.05f, 0.10f, -0.05f, 0.15f,
0.20f, -0.10f, 0.30f, -0.25f,
-0.15f, 0.05f, 0.25f, 0.10f,
0.12f, -0.08f, 0.18f, 0.04f,
-0.07f, 0.16f, -0.11f, 0.09f,
};
const std::vector<float> q_weight = {
0.20f, -0.10f, 0.05f, 0.30f,
-0.25f, 0.15f, 0.40f, -0.05f,
0.35f, 0.10f, -0.20f, 0.25f,
0.05f, 0.30f, 0.15f, -0.10f,
};
const std::vector<float> k_weight = {
-0.10f, 0.25f, 0.15f, 0.05f,
0.20f, 0.05f, -0.30f, 0.10f,
0.12f, -0.22f, 0.18f, 0.28f,
0.08f, 0.14f, -0.12f, 0.32f,
};
const std::vector<float> v_weight = {
0.30f, 0.05f, -0.10f, 0.20f,
-0.05f, 0.18f, 0.22f, -0.15f,
0.11f, -0.09f, 0.27f, 0.13f,
0.07f, 0.26f, -0.04f, 0.19f,
};
std::vector<float> qkv_weight;
qkv_weight.reserve(q_weight.size() + k_weight.size() + v_weight.size());
qkv_weight.insert(qkv_weight.end(), q_weight.begin(), q_weight.end());
qkv_weight.insert(qkv_weight.end(), k_weight.begin(), k_weight.end());
qkv_weight.insert(qkv_weight.end(), v_weight.begin(), v_weight.end());
const std::vector<float> out_weight = {
0.15f, 0.05f, -0.20f, 0.25f,
-0.12f, 0.18f, 0.22f, 0.08f,
0.30f, -0.05f, 0.12f, 0.14f,
0.10f, 0.16f, -0.08f, 0.20f,
};
const std::vector<float> pos_weight = {
0.14f, -0.09f, 0.21f, 0.04f,
0.07f, 0.25f, -0.11f, 0.16f,
-0.13f, 0.18f, 0.09f, 0.22f,
0.05f, 0.12f, 0.17f, -0.07f,
};
const std::vector<float> pos_bias_u = {
0.10f, -0.20f,
0.05f, 0.15f,
};
const std::vector<float> pos_bias_v = {
-0.08f, 0.12f,
0.07f, -0.03f,
};
auto make_relative_weights = [](CpuModuleRunner & runner, int64_t hidden_size, int64_t num_heads, bool fused, bool provide_projected_pos, int64_t batch_size, int64_t pos_frame_count) {
engine::modules::RelativeAttentionWeights weights{
{
runner.make_f32(engine::core::TensorShape::from_dims({hidden_size, hidden_size})),
std::nullopt,
runner.make_f32(engine::core::TensorShape::from_dims({hidden_size, hidden_size})),
std::nullopt,
runner.make_f32(engine::core::TensorShape::from_dims({hidden_size, hidden_size})),
std::nullopt,
fused ? std::optional<engine::core::TensorValue>(runner.make_f32(engine::core::TensorShape::from_dims({hidden_size * 3, hidden_size}))) : std::nullopt,
std::nullopt,
runner.make_f32(engine::core::TensorShape::from_dims({hidden_size, hidden_size})),
std::nullopt,
},
runner.make_f32(engine::core::TensorShape::from_dims({hidden_size, hidden_size})),
runner.make_f32(engine::core::TensorShape::from_dims({num_heads, hidden_size / num_heads})),
runner.make_f32(engine::core::TensorShape::from_dims({num_heads, hidden_size / num_heads})),
};
std::optional<engine::core::TensorValue> projected_pos;
if (provide_projected_pos) {
projected_pos = runner.make_f32(engine::core::TensorShape::from_dims({batch_size, pos_frame_count, hidden_size}));
}
return std::pair{weights, projected_pos};
};
auto write_relative_weights = [&](const engine::modules::RelativeAttentionWeights & weights,
const std::optional<engine::core::TensorValue> & projected_pos) {
engine::core::write_tensor_f32(weights.attention.q_weight, q_weight);
engine::core::write_tensor_f32(weights.attention.k_weight, k_weight);
engine::core::write_tensor_f32(weights.attention.v_weight, v_weight);
if (weights.attention.qkv_weight.has_value()) {
engine::core::write_tensor_f32(*weights.attention.qkv_weight, qkv_weight);
}
engine::core::write_tensor_f32(weights.attention.out_weight, out_weight);
engine::core::write_tensor_f32(weights.pos_weight, pos_weight);
engine::core::write_tensor_f32(weights.pos_bias_u, pos_bias_u);
engine::core::write_tensor_f32(weights.pos_bias_v, pos_bias_v);
if (projected_pos.has_value()) {
engine::core::write_tensor_f32(*projected_pos, project_sequence(pos_values, pos_frames, hidden, pos_weight, hidden));
}
};
CpuModuleRunner fused_runner;
auto fused_input = fused_runner.make_f32(engine::core::TensorShape::from_dims({batch, frames, hidden}));
auto fused_pos_emb = fused_runner.make_f32(engine::core::TensorShape::from_dims({batch, pos_frames, hidden}));
auto [fused_weights, fused_projected_pos] = make_relative_weights(fused_runner, hidden, heads, true, false, batch, pos_frames);
auto fused_output = engine::modules::RelativeSelfAttentionModule({hidden, heads, false, -1, -1, 0}).build(
fused_runner.ctx,
fused_input,
fused_pos_emb,
fused_weights,
std::nullopt,
std::nullopt,
fused_projected_pos);
fused_runner.allocate_tensors();
engine::core::write_tensor_f32(fused_input, input_values);
engine::core::write_tensor_f32(fused_pos_emb, pos_values);
write_relative_weights(fused_weights, fused_projected_pos);
const auto fused_values = fused_runner.run_f32(fused_output);
const std::vector<float> expected_fused_values = {
0.0190383f, 0.00988977f, 0.0295977f, 0.0188725f,
0.0186866f, 0.00997738f, 0.0296115f, 0.0186434f,
0.0197160f, 0.00805918f, 0.0315093f, 0.0185213f,
};
require_allclose(
fused_values,
expected_fused_values,
1.0e-6f,
"relative attention fused qkv golden output");
CpuModuleRunner split_runner;
auto split_input = split_runner.make_f32(engine::core::TensorShape::from_dims({batch, frames, hidden}));
auto split_pos_emb = split_runner.make_f32(engine::core::TensorShape::from_dims({batch, pos_frames, hidden}));
auto [split_weights, split_projected_pos] = make_relative_weights(split_runner, hidden, heads, false, false, batch, pos_frames);
auto split_output = engine::modules::RelativeSelfAttentionModule({hidden, heads, false, -1, -1, 0}).build(
split_runner.ctx,
split_input,
split_pos_emb,
split_weights,
std::nullopt,
std::nullopt,
split_projected_pos);
split_runner.allocate_tensors();
engine::core::write_tensor_f32(split_input, input_values);
engine::core::write_tensor_f32(split_pos_emb, pos_values);
write_relative_weights(split_weights, split_projected_pos);
const auto split_values = split_runner.run_f32(split_output);
require_allclose(fused_values, split_values, 1.0e-5f, "relative attention fused qkv vs split");
CpuModuleRunner cached_runner;
auto cached_input = cached_runner.make_f32(engine::core::TensorShape::from_dims({batch, frames, hidden}));
auto cached_pos_emb = cached_runner.make_f32(engine::core::TensorShape::from_dims({batch, pos_frames, hidden}));
auto [cached_weights, cached_projected_pos] = make_relative_weights(cached_runner, hidden, heads, true, true, batch, pos_frames);
auto cached_output = engine::modules::RelativeSelfAttentionModule({hidden, heads, false, -1, -1, 0}).build(
cached_runner.ctx,
cached_input,
cached_pos_emb,
cached_weights,
std::nullopt,
std::nullopt,
cached_projected_pos);
cached_runner.allocate_tensors();
engine::core::write_tensor_f32(cached_input, input_values);
engine::core::write_tensor_f32(cached_pos_emb, pos_values);
write_relative_weights(cached_weights, cached_projected_pos);
const auto cached_values = cached_runner.run_f32(cached_output);
require_allclose(fused_values, cached_values, 1.0e-5f, "relative attention cached projected pos");
}
void test_conformer_conv_pad_mask_ignores_dirty_padded_frames() {
const int64_t batch = 1;
const int64_t frames = 5;
const int64_t hidden = 4;
const int64_t valid_frames = 3;
const std::vector<float> clean_input = {
0.10f, -0.20f, 0.30f, 0.40f,
0.05f, 0.12f, -0.18f, 0.22f,
-0.11f, 0.07f, 0.15f, -0.09f,
0.00f, 0.00f, 0.00f, 0.00f,
0.00f, 0.00f, 0.00f, 0.00f,
};
std::vector<float> dirty_input = clean_input;
dirty_input[12] = 2.5f; dirty_input[13] = -1.7f; dirty_input[14] = 0.9f; dirty_input[15] = 3.1f;
dirty_input[16] = -2.2f; dirty_input[17] = 1.4f; dirty_input[18] = -0.6f; dirty_input[19] = 2.8f;
const auto keep_mask = make_keep_mask_for_test(frames, valid_frames);
auto make_weights = [](CpuModuleRunner & runner) {
engine::modules::ConformerConvModuleWeights weights{
{
runner.make_f32(engine::core::TensorShape::from_dims({hidden})),
runner.make_f32(engine::core::TensorShape::from_dims({hidden})),
},
{
runner.make_f32(engine::core::TensorShape::from_dims({hidden * 2, hidden})),
std::nullopt,
},
{
runner.make_f32(engine::core::TensorShape::from_dims({hidden, 1, 3})),
std::nullopt,
},
{
runner.make_f32(engine::core::TensorShape::from_dims({hidden})),
runner.make_f32(engine::core::TensorShape::from_dims({hidden})),
},
{
runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
},
};
return weights;
};
auto write_weights = [](const engine::modules::ConformerConvModuleWeights & weights) {
engine::core::write_tensor_f32(*weights.norm.weight, {1.0f, 0.9f, 1.1f, 0.95f});
engine::core::write_tensor_f32(*weights.norm.bias, {0.02f, -0.03f, 0.01f, 0.04f});
engine::core::write_tensor_f32(weights.pointwise_in.weight, {
0.20f, -0.10f, 0.05f, 0.30f,
-0.12f, 0.14f, 0.08f, -0.20f,
0.09f, 0.18f, -0.15f, 0.07f,
0.04f, -0.05f, 0.16f, 0.11f,
-0.03f, 0.17f, 0.06f, 0.12f,
0.10f, -0.08f, 0.13f, -0.04f,
0.07f, 0.15f, -0.02f, 0.09f,
0.05f, 0.01f, 0.14f, -0.06f,
});
engine::core::write_tensor_f32(weights.depthwise.weight, {
0.20f, 0.50f, -0.10f,
-0.05f, 0.40f, 0.15f,
0.10f, -0.20f, 0.30f,
0.25f, 0.05f, -0.12f,
});
engine::core::write_tensor_f32(weights.depthwise_norm.scale, {0.95f, 1.05f, 0.90f, 1.10f});
engine::core::write_tensor_f32(weights.depthwise_norm.bias, {0.01f, -0.02f, 0.03f, 0.00f});
engine::core::write_tensor_f32(weights.pointwise_out.weight, {
0.18f, -0.04f, 0.11f, 0.05f,
-0.09f, 0.16f, 0.07f, 0.12f,
0.13f, 0.02f, -0.06f, 0.14f,
0.04f, 0.15f, 0.08f, -0.10f,
});
};
CpuModuleRunner clean_runner;
auto clean_tensor = clean_runner.make_f32(engine::core::TensorShape::from_dims({batch, frames, hidden}));
auto pad_tensor_clean = clean_runner.make_i32(engine::core::TensorShape::from_dims({batch, frames}));
auto clean_weights = make_weights(clean_runner);
auto clean_output = engine::modules::ConformerConvModule({hidden, 3, false, 1.0e-5f, 0}).build(
clean_runner.ctx,
clean_tensor,
clean_weights,
pad_tensor_clean);
clean_runner.allocate_tensors();
engine::core::write_tensor_f32(clean_tensor, clean_input);
engine::core::write_tensor_i32(pad_tensor_clean, keep_mask);
write_weights(clean_weights);
const auto clean_values = clean_runner.run_f32(clean_output);
const std::vector<float> expected_clean_values = {
0.0193402f, -0.00830123f, 0.00372567f, -0.00191416f,
0.0102899f, -0.00829928f, 0.0123794f, -0.00320417f,
-0.00452878f, 0.0123428f, -0.00722798f, 0.00952792f,
-0.000882533f, 0.000231944f, -0.00204736f, -0.00295283f,
0.00297525f, -0.000970502f, -0.000458249f, -6.6002e-05f,
};
require_allclose(
clean_values,
expected_clean_values,
1.0e-6f,
"conformer conv golden output");
CpuModuleRunner dirty_runner;
auto dirty_tensor = dirty_runner.make_f32(engine::core::TensorShape::from_dims({batch, frames, hidden}));
auto pad_tensor_dirty = dirty_runner.make_i32(engine::core::TensorShape::from_dims({batch, frames}));
auto dirty_weights = make_weights(dirty_runner);
auto dirty_output = engine::modules::ConformerConvModule({hidden, 3, false, 1.0e-5f, 0}).build(
dirty_runner.ctx,
dirty_tensor,
dirty_weights,
pad_tensor_dirty);
dirty_runner.allocate_tensors();
engine::core::write_tensor_f32(dirty_tensor, dirty_input);
engine::core::write_tensor_i32(pad_tensor_dirty, keep_mask);
write_weights(dirty_weights);
const auto dirty_values = dirty_runner.run_f32(dirty_output);
require_allclose(clean_values, dirty_values, 1.0e-5f, "conformer conv pad mask invariance");
}
void test_relative_attention_specialized_flash_matches_reference_on_realistic_shapes() {
const int64_t batch = 1;
const int64_t frames = 63;
const int64_t valid_frames = 61;
const int64_t hidden = 512;
const int64_t heads = 8;
const int64_t pos_frames = 2 * frames - 1;
const auto input_values = make_patterned_f32(static_cast<size_t>(batch * frames * hidden), 0.13f, 0.08f);
const auto pos_values = make_patterned_f32(static_cast<size_t>(batch * pos_frames * hidden), -0.21f, 0.08f);
const auto q_weight = make_patterned_f32(static_cast<size_t>(hidden * hidden), 0.07f, 0.020f);
const auto k_weight = make_patterned_f32(static_cast<size_t>(hidden * hidden), -0.11f, 0.020f);
const auto v_weight = make_patterned_f32(static_cast<size_t>(hidden * hidden), 0.19f, 0.022f);
const auto out_weight = make_patterned_f32(static_cast<size_t>(hidden * hidden), -0.17f, 0.018f);
const auto pos_weight = make_patterned_f32(static_cast<size_t>(hidden * hidden), 0.23f, 0.018f);
const auto pos_bias_u = make_patterned_f32(static_cast<size_t>(heads * (hidden / heads)), 0.31f, 0.010f);
const auto pos_bias_v = make_patterned_f32(static_cast<size_t>(heads * (hidden / heads)), -0.29f, 0.010f);
const auto attention_bias = make_global_attention_bias_for_test(frames, valid_frames);
const auto keep_mask = make_keep_mask_for_test(frames, valid_frames);
const auto projected_pos_values = project_sequence(pos_values, pos_frames, hidden, pos_weight, hidden);
auto run_case = [&](engine::core::BackendType backend_type, const std::string & backend_label) {
ModuleRunner reference_runner;
reference_runner.backend_config.type = backend_type;
if (reference_runner.backend != nullptr) {
ggml_backend_free(reference_runner.backend);
reference_runner.backend = nullptr;
}
reference_runner.backend = engine::core::init_backend(reference_runner.backend_config);
auto ref_input = reference_runner.make_f32(engine::core::TensorShape::from_dims({batch, frames, hidden}));
auto ref_pos = reference_runner.make_f32(engine::core::TensorShape::from_dims({batch, pos_frames, hidden}));
auto ref_mask = reference_runner.make_f32(engine::core::TensorShape::from_dims({frames, frames}));
auto ref_keep = reference_runner.make_i32(engine::core::TensorShape::from_dims({batch, frames}));
engine::modules::RelativeAttentionWeights ref_weights{
{
reference_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
reference_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
reference_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
std::nullopt,
std::nullopt,
reference_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
},
reference_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
reference_runner.make_f32(engine::core::TensorShape::from_dims({heads, hidden / heads})),
reference_runner.make_f32(engine::core::TensorShape::from_dims({heads, hidden / heads})),
};
auto ref_output = engine::modules::RelativeSelfAttentionModule({hidden, heads, false, -1, -1, 0, false}).build(
reference_runner.ctx,
ref_input,
ref_pos,
ref_weights,
ref_mask,
ref_keep,
std::nullopt);
reference_runner.allocate_tensors();
engine::core::write_tensor_f32(ref_input, input_values);
engine::core::write_tensor_f32(ref_pos, pos_values);
engine::core::write_tensor_f32(ref_mask, attention_bias);
engine::core::write_tensor_i32(ref_keep, keep_mask);
engine::core::write_tensor_f32(ref_weights.attention.q_weight, q_weight);
engine::core::write_tensor_f32(ref_weights.attention.k_weight, k_weight);
engine::core::write_tensor_f32(ref_weights.attention.v_weight, v_weight);
engine::core::write_tensor_f32(ref_weights.attention.out_weight, out_weight);
engine::core::write_tensor_f32(ref_weights.pos_weight, pos_weight);
engine::core::write_tensor_f32(ref_weights.pos_bias_u, pos_bias_u);
engine::core::write_tensor_f32(ref_weights.pos_bias_v, pos_bias_v);
const auto reference_values = reference_runner.run_f32(ref_output);
ModuleRunner flash_runner;
flash_runner.backend_config.type = backend_type;
if (flash_runner.backend != nullptr) {
ggml_backend_free(flash_runner.backend);
flash_runner.backend = nullptr;
}
flash_runner.backend = engine::core::init_backend(flash_runner.backend_config);
auto flash_input = flash_runner.make_f32(engine::core::TensorShape::from_dims({batch, frames, hidden}));
auto flash_pos = flash_runner.make_f32(engine::core::TensorShape::from_dims({batch, pos_frames, hidden}));
auto flash_mask = flash_runner.make_f32(engine::core::TensorShape::from_dims({frames, frames}));
auto flash_keep = flash_runner.make_i32(engine::core::TensorShape::from_dims({batch, frames}));
engine::modules::RelativeAttentionWeights flash_weights{
{
flash_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
flash_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
flash_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
std::nullopt,
std::nullopt,
flash_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
std::nullopt,
},
flash_runner.make_f32(engine::core::TensorShape::from_dims({hidden, hidden})),
flash_runner.make_f32(engine::core::TensorShape::from_dims({heads, hidden / heads})),
flash_runner.make_f32(engine::core::TensorShape::from_dims({heads, hidden / heads})),
};
auto flash_projected_pos = flash_runner.make_f32(engine::core::TensorShape::from_dims({batch, pos_frames, hidden}));
auto flash_output = engine::modules::RelativeSelfAttentionModule({hidden, heads, false, -1, -1, 0, true}).build(
flash_runner.ctx,
flash_input,
flash_pos,
flash_weights,
flash_mask,
flash_keep,
flash_projected_pos);
flash_runner.allocate_tensors();
engine::core::write_tensor_f32(flash_input, input_values);
engine::core::write_tensor_f32(flash_pos, pos_values);
engine::core::write_tensor_f32(flash_mask, attention_bias);
engine::core::write_tensor_i32(flash_keep, keep_mask);
engine::core::write_tensor_f32(flash_projected_pos, projected_pos_values);
engine::core::write_tensor_f32(flash_weights.attention.q_weight, q_weight);
engine::core::write_tensor_f32(flash_weights.attention.k_weight, k_weight);
engine::core::write_tensor_f32(flash_weights.attention.v_weight, v_weight);
engine::core::write_tensor_f32(flash_weights.attention.out_weight, out_weight);
engine::core::write_tensor_f32(flash_weights.pos_weight, pos_weight);
engine::core::write_tensor_f32(flash_weights.pos_bias_u, pos_bias_u);
engine::core::write_tensor_f32(flash_weights.pos_bias_v, pos_bias_v);
const auto flash_values = flash_runner.run_f32(flash_output);
const float max_abs_tol = backend_type == engine::core::BackendType::Cuda ? 4.0e-3f : 4.0e-3f;
const double mean_abs_tol = backend_type == engine::core::BackendType::Cuda ? 7.5e-4 : 6.0e-4;
require_max_abs_diff_below(
flash_values,
reference_values,
max_abs_tol,
mean_abs_tol,
"relative attention flash parity " + backend_label);
};
run_case(engine::core::BackendType::Cpu, "cpu");
if (backend_is_available(engine::core::BackendType::Cuda)) {
run_case(engine::core::BackendType::Cuda, "cuda");
} else {
std::cout << "encoder_module_test: skipping cuda flash parity\n";
}
}
void test_glu_contiguous_gate_opt_in() {
using namespace engine;
modules::GLUModule legacy = {};
legacy = []() -> modules::GLUModule { return {}; }();
const std::vector<float> data{1, -2, 3, 4, 0, 1, -1, 2, -3, 2, 1, -1, -2, 0, 2, 1};
std::vector<float> expected;
for (size_t row = 0; row < 2; ++row) {
for (size_t col = 0; col < 4; ++col) {
expected.push_back(data[row * 8 + col] / (1.0f + std::exp(-data[row * 8 + col + 4])));
}
}
require(!modules::GLUConfig{}.contiguous_gate, "GLU layout change must be opt-in");
require(!modules::ConformerBlockConfig{}.contiguous_glu_gate, "Conformer layout change must be opt-in");
for (const auto type : {core::BackendType::Cpu, core::BackendType::Cuda}) {
if (type == core::BackendType::Cuda && !backend_is_available(type)) {
continue;
}
for (const bool opt_in : {false, true}) {
if (type == core::BackendType::Cuda && !opt_in) {
continue; // Legacy strided sigmoid is not supported on CUDA.
}
ModuleRunner runner;
set_runner_backend(runner, type);
runner.ctx.backend_type = type;
auto input = runner.make_f32(core::TensorShape::from_dims({1, 2, 8}));
const auto module = opt_in ? modules::GLUModule(modules::GLUConfig{true}) : legacy;
auto output = module.build(runner.ctx, input);
auto graph = ggml_new_graph_custom(runner.ggml, kTestGraphNodes, false);
ggml_build_forward_expand(graph, output.tensor);
bool found_sigmoid = false;
for (int i = 0; i < ggml_graph_n_nodes(graph); ++i) {
auto node = ggml_graph_node(graph, i);
if (node->op == GGML_OP_UNARY && ggml_get_unary_op(node) == GGML_UNARY_OP_SIGMOID) {
require(ggml_is_contiguous(node->src[0]) == opt_in, "GLU gate layout opt-in");
found_sigmoid = true;
}
}
require(found_sigmoid, "GLU sigmoid node");
runner.allocate_tensors();
core::write_tensor_f32(input, data);
require_allclose(runner.run_f32(output), expected, 1e-5f, "GLU output");
}
}
}
void test_depthwise_subsampling_stage_masks() {
using namespace engine;
ModuleRunner runner;
auto input = runner.make_f32(core::TensorShape::from_dims({1, 8, 4}));
auto first_mask = runner.make_i32(core::TensorShape::from_dims({1, 4}));
auto second_mask = runner.make_i32(core::TensorShape::from_dims({1, 2}));
auto kernel = runner.make_f32(core::TensorShape::from_dims({1, 1, 1, 1}));
auto bias = runner.make_f32(core::TensorShape::from_dims({1}));
auto depth_bias = runner.make_f32(core::TensorShape::from_dims({1}));
auto point_kernel = runner.make_f32(core::TensorShape::from_dims({1, 1, 1, 1}));
auto point_bias = runner.make_f32(core::TensorShape::from_dims({1}));
auto proj = runner.make_f32(core::TensorShape::from_dims({1, 1}));
auto proj_bias = runner.make_f32(core::TensorShape::from_dims({1}));
modules::DepthwiseConvSubsamplingWeights weights{
{kernel, bias}, {{{kernel, depth_bias}, {point_kernel, point_bias}}}, {proj, proj_bias}};
const modules::DepthwiseConvSubsamplingModule module({4, 1, 1, 1, 2, 0, true});
auto masked = module.build(runner.ctx, input, weights, {first_mask, second_mask});
auto unmasked = module.build(runner.ctx, input, weights);
require(masked.shape.dims[1] == 2, "two-stage subsampling output length");
bool rejected = false;
try {
module.build(runner.ctx, input, weights, {first_mask});
} catch (const std::runtime_error &) {
rejected = true;
}
require(rejected, "subsampling rejects missing stage masks");
runner.allocate_tensors();
std::vector<float> data(32);
for (size_t i = 0; i < data.size(); ++i) data[i] = static_cast<float>(i + 1);
core::write_tensor_f32(input, data);
core::write_tensor_f32(kernel, {1});
core::write_tensor_f32(bias, {1});
core::write_tensor_f32(depth_bias, {2});
core::write_tensor_f32(point_kernel, {3});
core::write_tensor_f32(point_bias, {5});
core::write_tensor_f32(proj, {2});
core::write_tensor_f32(proj_bias, {7});
core::write_tensor_i32(first_mask, {1, 1, 1, 0});
core::write_tensor_i32(second_mask, {1, 0});
require_allclose(runner.run_f32(masked), {41, 7}, 1e-6f, "subsampling tail mask");
require_allclose(runner.run_f32(unmasked), {41, 137}, 1e-6f, "subsampling unmasked");
core::write_tensor_i32(first_mask, {1, 1, 0, 0});
core::write_tensor_i32(second_mask, {1, 1});
require_allclose(runner.run_f32(masked), {41, 29}, 1e-6f, "subsampling earlier stage mask");
}
class AffineTestSource final : public engine::assets::TensorSource {
public:
std::unordered_map<std::string, std::vector<float>> values;
const std::filesystem::path & source_path() const noexcept override { return path_; }
bool has_tensor(std::string_view name) const noexcept override { return values.count(std::string(name)) != 0; }
engine::assets::TensorMetadata require_metadata(std::string_view name) const override {
return {std::string(name), "f32", {static_cast<int64_t>(values.at(std::string(name)).size())}};
}
std::vector<engine::assets::TensorMetadata> tensors() const override {
std::vector<engine::assets::TensorMetadata> result;
for (const auto & entry : values) result.push_back(require_metadata(entry.first));
return result;
}
engine::assets::RawTensorData require_tensor_data(std::string_view name) const override {
engine::assets::RawTensorData result;
result.metadata = require_metadata(name);
const auto & data = values.at(std::string(name));
result.bytes.resize(data.size() * sizeof(float));
std::memcpy(result.bytes.data(), data.data(), result.bytes.size());
return result;
}
std::vector<float> require_f32(std::string_view name,
const std::optional<std::vector<int64_t>> & shape) const override {
require(!shape || *shape == require_metadata(name).shape, "affine source shape");
return values.at(std::string(name));
}
std::optional<std::vector<float>> optional_f32(std::string_view name,
const std::optional<std::vector<int64_t>> & shape) const override {
return has_tensor(name) ? std::optional<std::vector<float>>(require_f32(name, shape)) : std::nullopt;
}
int64_t require_i64_scalar(std::string_view) const override { throw std::runtime_error("not an integer tensor"); }
private:
std::filesystem::path path_{"affine-test"};
};
void test_batch_norm_eval_binding() {
using namespace engine;
ModuleRunner runner;
core::BackendWeightStore store(runner.backend, core::BackendType::Cpu, "affine-test", 1024 * 1024);
AffineTestSource source;
source.values = {{"bn.weight", {2, -3}}, {"bn.bias", {1, 2}},
{"bn.running_mean", {4, -2}}, {"bn.running_var", {3, 8}}};
auto weights = modules::binding::batch_norm_eval_from_source(store, source, "bn", 2, 1.0f);
store.upload();
std::vector<float> scale, bias;
core::read_tensor_f32_into(weights.scale.tensor, scale);
core::read_tensor_f32_into(weights.bias.tensor, bias);
require_allclose(scale, {1, -1}, 1e-6f, "batch norm eval scale");
require_allclose(bias, {-3, 0}, 1e-6f, "batch norm eval bias");
}
void test_timestep_embedding_norm_opt_in() {
using namespace engine;
ModuleRunner runner;
auto timestep = runner.make_f32(core::TensorShape::from_dims({1, 1}));
auto freqs = runner.make_f32(core::TensorShape::from_dims({2}));
auto identity = runner.make_f32(core::TensorShape::from_dims({4, 4}));
auto zeros = runner.make_f32(core::TensorShape::from_dims({4}));
auto alpha = runner.make_f32(core::TensorShape::from_dims({4}));
modules::TimestepEmbeddingWeights weights{freqs, {identity, zeros}, {identity, zeros}, alpha};
modules::TimestepEmbeddingConfig config{4, 4, 1e-5f};
require(config.norm == modules::TimestepEmbeddingNorm::VarianceRms, "default norm remains the variance RMS");
auto legacy = modules::TimestepEmbeddingModule(config).build(runner.ctx, timestep, weights);
config.norm = modules::TimestepEmbeddingNorm::Rms;
auto rms = modules::TimestepEmbeddingModule(config).build(runner.ctx, timestep, weights);
runner.allocate_tensors();
const float t = 0.7f;
const std::vector<float> freq_values{1.0f, 0.25f};
const std::vector<float> alpha_values{1.0f, 2.0f, 0.5f, -1.0f};
core::write_tensor_f32(timestep, {t});
core::write_tensor_f32(freqs, freq_values);
core::write_tensor_f32(identity, {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1});
core::write_tensor_f32(zeros, {0, 0, 0, 0});
core::write_tensor_f32(alpha, alpha_values);
std::vector<float> x;
for (float f : freq_values) x.push_back(std::cos(t * f));
for (float f : freq_values) x.push_back(std::sin(t * f));
for (float & v : x) v = v / (1.0f + std::exp(-v));
float mean = 0.0f, mean_square = 0.0f;
for (float v : x) {
mean += v / 4.0f;
mean_square += v * v / 4.0f;
}
float variance = 0.0f;
for (float v : x) variance += (v - mean) * (v - mean) / 3.0f;
std::vector<float> expected_legacy, expected_rms;
for (size_t i = 0; i < x.size(); ++i) {
expected_legacy.push_back(x[i] * alpha_values[i] / std::sqrt(variance + 1e-5f));
expected_rms.push_back(x[i] * alpha_values[i] / std::sqrt(mean_square + 1e-5f));
}
require_allclose(runner.run_f32(legacy), expected_legacy, 1e-4f, "legacy variance RMS time embedding");
require_allclose(runner.run_f32(rms), expected_rms, 1e-5f, "opt-in plain RMS time embedding");
}
void test_feed_forward_activation_opt_in() {
using namespace engine;
ModuleRunner runner;
auto input = runner.make_f32(core::TensorShape::from_dims({1, 1, 4}));
auto identity = runner.make_f32(core::TensorShape::from_dims({4, 4}));
modules::FeedForwardWeights weights{identity, std::nullopt, identity, std::nullopt};
modules::FeedForwardConfig config{4, 4, false};
require(config.activation == modules::FeedForwardActivation::Gelu, "default activation remains GELU");
auto legacy = modules::FeedForwardModule(config).build(runner.ctx, input, weights);
config.activation = modules::FeedForwardActivation::Relu;
auto relu = modules::FeedForwardModule(config).build(runner.ctx, input, weights);
runner.allocate_tensors();
const std::vector<float> data{-2, -1, 1, 2};
core::write_tensor_f32(input, data);
core::write_tensor_f32(identity, {1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1});
std::vector<float> expected;
for (float v : data) expected.push_back(0.5f * v * (1.0f + std::erf(v / std::sqrt(2.0f))));
require_allclose(runner.run_f32(legacy), expected, 1e-6f, "legacy GELU");
require_allclose(runner.run_f32(relu), {0,0,1,2}, 1e-6f, "opt-in ReLU");
}
void test_cached_decoder_block_matches_composition(bool flash_cross) {
using namespace engine;
using core::TensorShape;
constexpr int64_t hidden = 32;
ModuleRunner runner;
std::vector<std::pair<core::TensorValue, std::vector<float>>> initializers;
auto param = [&](const TensorShape & shape) {
auto tensor = runner.make_f32(shape);
initializers.emplace_back(tensor, make_patterned_f32(shape.num_elements(), 0.37f, 0.07f));
return tensor;
};
auto norm = [&]() -> modules::NormWeights {
auto weight = param(TensorShape::from_dims({hidden}));
initializers.back().second.assign(hidden, 1.0f);
return {weight, param(TensorShape::from_dims({hidden}))};
};
modules::TransformerDecoderBlockWeights w;
w.norm1 = norm(); w.norm2 = norm(); w.norm3 = norm();
w.self_attention.qkv_weight = param(TensorShape::from_dims({3 * hidden, hidden}));
w.self_attention.qkv_bias = param(TensorShape::from_dims({3 * hidden}));
w.self_attention.out_weight = param(TensorShape::from_dims({hidden, hidden}));
w.self_attention.out_bias = param(TensorShape::from_dims({hidden}));
w.cross_attention.q_weight = param(TensorShape::from_dims({hidden, hidden}));
w.cross_attention.q_bias = param(TensorShape::from_dims({hidden}));
w.cross_attention.out_weight = param(TensorShape::from_dims({hidden, hidden}));
w.cross_attention.out_bias = param(TensorShape::from_dims({hidden}));
w.feed_forward = {param(TensorShape::from_dims({64, hidden})), param(TensorShape::from_dims({64})),
param(TensorShape::from_dims({hidden, 64})), param(TensorShape::from_dims({hidden}))};
auto input = runner.make_f32(TensorShape::from_dims({1, 1, hidden}));
auto slot = runner.make_i32(TensorShape::from_dims({1}));
auto mask = core::make_tensor(runner.ctx, GGML_TYPE_F16, TensorShape::from_dims({1, 8}));
auto memory_mask = runner.make_i32(TensorShape::from_dims({1, 3}));
auto flash_mask = core::make_tensor(runner.ctx, GGML_TYPE_F16, TensorShape::from_dims({1, 1, 1, 3}));
modules::CrossAttentionKeyValue memory{param(TensorShape::from_dims({1, 1, 3, hidden})),
param(TensorShape::from_dims({1, 1, 3, hidden}))};
std::vector<core::TensorValue> caches;
for (int i = 0; i < 4; ++i) caches.push_back(core::make_tensor(runner.ctx, GGML_TYPE_F16,
TensorShape::from_dims({1, 8, 1, hidden})));
modules::TransformerDecoderBlockConfig config{hidden, 1, 64};
require(!config.use_flash_cross_attention, "cached cross flash remains opt-in");
config.use_flash_cross_attention = flash_cross;
config.activation = modules::FeedForwardActivation::Relu;
config.use_packed_qkv = true;
config.use_packed_kv = true;
auto actual = modules::TransformerDecoderBlockModule(config).build_cached_tail(runner.ctx, input, w,
caches[0], caches[1], slot, mask, memory, flash_cross ? flash_mask : memory_mask);
modules::AttentionConfig attention{hidden, 1, true};
attention.causal = true;
attention.use_packed_qkv = true;
auto y = modules::LayerNormModule({hidden}).build(runner.ctx, input, w.norm1);
y = modules::SelfAttentionModule(attention).build_cached_tail(runner.ctx, y, w.self_attention,
caches[2], caches[3], slot, mask).output;
auto expected = modules::AddModule().build(runner.ctx, input, y);
y = modules::LayerNormModule({hidden}).build(runner.ctx, expected, w.norm2);
modules::AttentionConfig cross_attention{hidden, 1, true};
cross_attention.use_packed_kv = true;
y = modules::CrossAttentionModule(cross_attention).build_cached(runner.ctx, y, memory, w.cross_attention, memory_mask);
expected = modules::AddModule().build(runner.ctx, expected, y);
y = modules::LayerNormModule({hidden}).build(runner.ctx, expected, w.norm3);
y = modules::LinearModule({hidden, 64, true}).build(runner.ctx, y, {w.feed_forward.fc1_weight, w.feed_forward.fc1_bias});
y = modules::ReluModule().build(runner.ctx, y);
y = modules::LinearModule({64, hidden, true}).build(runner.ctx, y, {w.feed_forward.fc2_weight, w.feed_forward.fc2_bias});
expected = modules::AddModule().build(runner.ctx, expected, y);
runner.allocate_tensors();
for (const auto & item : initializers) core::write_tensor_f32(item.first, item.second);
for (const auto & cache : caches) core::write_tensor_f16(cache, std::vector<float>(8 * hidden, 0));
core::write_tensor_i32(memory_mask, {1, 1, 0});
core::write_tensor_f16(flash_mask, {0, 0, -std::numeric_limits<float>::infinity()});
for (int32_t step = 0; step < 3; ++step) {
core::write_tensor_f32(input, make_patterned_f32(hidden, 0.4f + step, 0.3f));
core::write_tensor_i32(slot, &step, 1);
std::vector<float> causal(8, -std::numeric_limits<float>::infinity());
std::fill_n(causal.begin(), step + 1, 0.0f);
core::write_tensor_f16(mask, causal);
require_allclose(runner.run_f32(actual), runner.run_f32(expected), 1e-5f, "cached decoder composition");
}
}
void test_cached_cross_flash_masks() {
using namespace engine;
using core::TensorShape;
ModuleRunner runner;
modules::AttentionConfig config{64, 2, false};
config.use_packed_kv = true;
const modules::CrossAttentionModule module(config);
auto query = runner.make_f32(TensorShape::from_dims({2, 2, 64}));
auto weight = runner.make_f32(TensorShape::from_dims({64, 64}));
modules::AttentionWeights weights;
weights.q_weight = weight;
weights.out_weight = weight;
modules::CrossAttentionKeyValue kv{
runner.make_f32(TensorShape::from_dims({2, 2, 3, 32})),
runner.make_f32(TensorShape::from_dims({2, 2, 3, 32}))};
auto keep = runner.make_i32(TensorShape::from_dims({2, 3}));
auto mask = core::make_tensor(runner.ctx, GGML_TYPE_F16, TensorShape::from_dims({2, 1, 2, 3}));
auto shared_mask = core::make_tensor(runner.ctx, GGML_TYPE_F16, TensorShape::from_dims({1, 1, 2, 3}));
auto per_head_mask = core::make_tensor(runner.ctx, GGML_TYPE_F16, TensorShape::from_dims({2, 2, 2, 3}));
auto expected = module.build_cached(runner.ctx, query, kv, weights, keep);
auto actual = module.build_cached_flash(runner.ctx, query, kv, weights, mask);
auto shared = module.build_cached_flash(runner.ctx, query, kv, weights, shared_mask);
auto per_head = module.build_cached_flash(runner.ctx, query, kv, weights, per_head_mask);
for (const auto & invalid : {keep,
runner.make_f32(TensorShape::from_dims({2, 1, 2, 3})),
core::make_tensor(runner.ctx, GGML_TYPE_F16, TensorShape::from_dims({2, 1, 1, 3})),
core::make_tensor(runner.ctx, GGML_TYPE_F16, TensorShape::from_dims({2, 1, 2, 4}))}) {
bool rejected = false;
try { module.build_cached_flash(runner.ctx, query, kv, weights, invalid); }
catch (const std::runtime_error &) { rejected = true; }
require(rejected, "cached flash rejects invalid mask contracts");
}
runner.allocate_tensors();
std::vector<float> identity(64 * 64, 0);
for (size_t i = 0; i < 64; ++i) identity[i * 64 + i] = 1;
core::write_tensor_f32(weight, identity);
core::write_tensor_f32(query, make_patterned_f32(256, 0.2f, 0.3f));
core::write_tensor_f32(kv.key, make_patterned_f32(384, 0.4f, 0.2f));
core::write_tensor_f32(kv.value, make_patterned_f32(384, 0.7f, 0.1f));
const float excluded = -std::numeric_limits<float>::infinity();
core::write_tensor_i32(keep, {1, 1, 0, 1, 0, 1});
core::write_tensor_f16(mask, {0, 0, excluded, 0, 0, excluded, 0, excluded, 0, 0, excluded, 0});
require_allclose(runner.run_f32(actual), runner.run_f32(expected), 1e-5f, "batched cached flash padding");
core::write_tensor_i32(keep, {1, 1, 0, 1, 1, 0});
core::write_tensor_f16(shared_mask, {0, 0, excluded, 0, 0, excluded});
std::vector<float> per_head_values(24);
for (size_t i = 0; i < per_head_values.size(); ++i) per_head_values[i] = i % 3 == 2 ? excluded : 0;
core::write_tensor_f16(per_head_mask, per_head_values);
require_allclose(runner.run_f32(shared), runner.run_f32(expected), 1e-5f, "shared cached flash mask");
require_allclose(runner.run_f32(per_head), runner.run_f32(expected), 1e-5f, "per-head cached flash mask");
}
void test_weighted_statistics_and_scale_bias() {
using namespace engine;
CpuModuleRunner runner;
const auto shape = core::TensorShape::from_dims({1, 2, 3});
const auto input = runner.make_f32(shape);
const auto weights = runner.make_f32(shape);
const auto moments = modules::WeightedStatisticsModule({2, 1e-4f, 1e4f}).build(runner.ctx, input, weights);
const auto affine = modules::ScaleBiasModule({2.0f, -1.0f}).build(runner.ctx, input);
runner.allocate_tensors();
core::write_tensor_f32(input, {1, 2, 3, 2, 2, 2});
core::write_tensor_f32(weights, {0.25f, 0.5f, 0.25f, 0.25f, 0.5f, 0.25f});
require_allclose(runner.run_f32(moments.mean), {2, 2}, 0, "weighted mean");
require_allclose(runner.run_f32(moments.stddev), {std::sqrt(0.5f), 0.01f}, 1e-7f, "weighted stddev and floor");
require_allclose(runner.run_f32(affine), {1, 3, 5, 3, 3, 3}, 0, "scalar affine");
}
void test_convnext_default_matches_composition() {
using namespace engine;
using core::TensorShape;
CpuModuleRunner runner;
std::vector<std::pair<core::TensorValue, std::vector<float>>> parameters;
const auto parameter = [&](const TensorShape & shape) {
const auto tensor = runner.make_f32(shape);
parameters.emplace_back(tensor, make_patterned_f32(shape.num_elements(), 0.4f, 0.2f));
return tensor;
};
const auto input = parameter(TensorShape::from_dims({1, 7, 4}));
modules::ConvNeXt1dWeights weights;
weights.depthwise = {parameter(TensorShape::from_dims({4, 1, 3})), parameter(TensorShape::from_dims({4}))};
weights.norm = {parameter(TensorShape::from_dims({4})), parameter(TensorShape::from_dims({4}))};
weights.expansion = {parameter(TensorShape::from_dims({8, 4})), parameter(TensorShape::from_dims({8}))};
weights.projection = {parameter(TensorShape::from_dims({4, 8})), parameter(TensorShape::from_dims({4}))};
weights.gamma = parameter(TensorShape::from_dims({4}));
const auto actual = modules::build_convnext1d(runner.ctx, input, weights);
auto expected = modules::TransposeModule({{0, 2, 1, 3}, 3}).build(runner.ctx, input);
expected = modules::DepthwiseConv1dModule({4, 3, 1, 1, 1, true}).build(runner.ctx, expected, weights.depthwise);
expected = modules::TransposeModule({{0, 2, 1, 3}, 3}).build(runner.ctx, expected);
expected = modules::LayerNormModule({4, 1e-6f, true, true}).build(runner.ctx, expected, weights.norm);
expected = modules::LinearModule({4, 8, true}).build(runner.ctx, expected, weights.expansion);
expected = modules::GeluModule({modules::GeluApproximation::ExactErf}).build(runner.ctx, expected);
expected = modules::LinearModule({8, 4, true}).build(runner.ctx, expected, weights.projection);
const auto gamma = core::reshape_tensor(runner.ctx, weights.gamma, TensorShape::from_dims({1, 1, 4}));
expected = modules::MulModule().build(runner.ctx, expected, modules::RepeatModule({expected.shape}).build(runner.ctx, gamma));
expected = modules::AddModule().build(runner.ctx, input, expected);
runner.allocate_tensors();
for (const auto & parameter : parameters) core::write_tensor_f32(parameter.first, parameter.second);
require_allclose(runner.run_f32(actual), runner.run_f32(expected), 0, "ConvNeXt default composition");
}
void test_prelu_broadcasting() {
using namespace engine;
for (size_t axis = 0; axis < 3; ++axis) {
for (bool scalar : {false, true}) {
CpuModuleRunner runner;
const auto shape = core::TensorShape::from_dims({2, 3, 4});
const auto input = runner.make_f32(shape);
const auto count = scalar ? 1 : shape.dims[axis];
const auto slope = runner.make_f32(core::TensorShape::from_dims({count}));
const auto output = modules::PReluModule({axis}).build(runner.ctx, input, slope);
runner.allocate_tensors();
std::vector<float> values(24), slopes(count), expected(24);
for (int64_t c = 0; c < count; ++c) slopes[c] = (c - 1) * 0.25f;
int64_t stride = 1;
for (size_t dim = axis + 1; dim < 3; ++dim) stride *= shape.dims[dim];
for (size_t i = 0; i < values.size(); ++i) {
values[i] = static_cast<float>(i % 9) - 4.0f;
const auto channel = scalar ? 0 : (i / stride) % count;
expected[i] = values[i] < 0 ? values[i] * slopes[channel] : values[i];
}
core::write_tensor_f32(input, values);
core::write_tensor_f32(slope, slopes);
require_allclose(runner.run_f32(output), expected, 0.0f, "PReLU scalar/channel broadcasting");
}
}
}
void test_interpolate_align_corners() {
using namespace engine;
CpuModuleRunner runner;
const auto input=runner.make_f32(core::TensorShape::from_dims({1,1,3}));
const auto legacy=modules::Interpolate1dModule({5,modules::Interpolate1dMode::Linear}).build(runner.ctx,input);
const auto aligned=modules::Interpolate1dModule({5,modules::Interpolate1dMode::Linear,true}).build(runner.ctx,input);
const auto single=modules::Interpolate1dModule({1,modules::Interpolate1dMode::Linear,true}).build(runner.ctx,input);
runner.allocate_tensors();
core::write_tensor_f32(input,{0,1,4});
require_allclose(runner.run_f32(legacy),{0,.4F,1,2.8F,4},1.e-6F,"interpolation unchanged default");
require_allclose(runner.run_f32(aligned),{0,.5F,1,2.5F,4},1.e-6F,"interpolation aligned corners");
require_allclose(runner.run_f32(single),{0},1.e-6F,"interpolation aligned singleton");
}
void test_batched_rope_matches_independent_rows() {
using namespace engine;
for (const int64_t batch : {1, 2, 5}) {
for (const int64_t steps : {1, 3}) {
for (const int mode : {GGML_ROPE_TYPE_NORMAL, GGML_ROPE_TYPE_NEOX}) {
for (const bool half : {false, true}) {
for (const bool use_factors : {false, true}) {
CpuModuleRunner runner;
const auto shape = core::TensorShape::from_dims({batch, steps, 3, 8});
const auto source = runner.make_f32(shape);
const auto input = half ? core::wrap_tensor(ggml_cast(runner.ggml, source.tensor, GGML_TYPE_F16),
shape, GGML_TYPE_F16) : source;
const auto positions = runner.make_i32(core::TensorShape::from_dims({batch, steps}));
const auto factors = runner.make_f32(core::TensorShape::from_dims({4}));
const modules::RoPEModule rope({8, mode, 10000.0F});
const auto actual = rope.build_batched(runner.ctx, input, positions, use_factors ? &factors : nullptr);
std::vector<core::TensorValue> independent;
for (int64_t row = 0; row < batch; ++row) {
const auto x = modules::SliceModule({0, row, 1}).build(runner.ctx, input);
auto p = modules::SliceModule({0, row, 1}).build(runner.ctx, positions);
p = core::reshape_tensor(runner.ctx, p, core::TensorShape::from_dims({steps}));
independent.push_back(rope.build(runner.ctx, x, p, use_factors ? &factors : nullptr));
}
auto expected = independent.front();
for (size_t row = 1; row < independent.size(); ++row)
expected = modules::ConcatModule({0}).build(runner.ctx, expected, independent[row]);
const auto actual_f32 = core::wrap_tensor(ggml_cast(runner.ggml, actual.tensor, GGML_TYPE_F32), shape, GGML_TYPE_F32);
const auto expected_f32 = core::wrap_tensor(ggml_cast(runner.ggml, expected.tensor, GGML_TYPE_F32), shape, GGML_TYPE_F32);
runner.allocate_tensors();
core::write_tensor_f32(source, make_patterned_f32(shape.num_elements(), 0.3F, 0.8F));
core::write_tensor_f32(factors, {1.0F, 1.5F, 2.0F, 0.5F});
for (int round = 0; round < 2; ++round) {
std::vector<int32_t> ids(batch * steps);
for (int64_t row = 0; row < batch; ++row)
for (int64_t t = 0; t < steps; ++t) ids[row * steps + t] = row * 137 + t * 3 + round * 4096;
core::write_tensor_i32(positions, ids);
require_allclose(runner.run_f32(actual_f32), runner.run_f32(expected_f32), 0.0F, "Independent batched RoPE");
}
}
}
}
}
}
}
} // namespace
int main(int argc, char ** argv) {
const auto started = std::chrono::steady_clock::now();
try {
if (argc == 3 && std::string(argv[1]) == "--log")
engine::debug::configure_logging({true, argv[2]});
else if (argc != 1)
throw std::runtime_error("Usage: encoder_module_test [--log FILE]");
test_batched_rope_matches_independent_rows();
test_cuda_f16_lowerings_default_off();
test_mul_optional_broadcasting();
test_weighted_statistics_and_scale_bias();
test_convnext_default_matches_composition();
test_prelu_broadcasting();
test_interpolate_align_corners();
test_glu_contiguous_gate_opt_in();
test_depthwise_subsampling_stage_masks();
test_batch_norm_eval_binding();
test_feed_forward_activation_opt_in();
test_timestep_embedding_norm_opt_in();
test_cached_decoder_block_matches_composition(false);
test_cached_decoder_block_matches_composition(true);
test_cached_cross_flash_masks();
test_graph_optimizer_elides_metadata_nodes_without_changing_output();
test_graph_optimizer_two_sided_broadcast_binary_matches_repeat();
test_graph_optimizer_unary_broadcast_scale_matches_repeat();
test_relative_attention_fused_qkv_matches_split_and_cached_pos();
test_relative_attention_specialized_flash_matches_reference_on_realistic_shapes();
test_conformer_conv_pad_mask_ignores_dirty_padded_frames();
std::cout << "encoder_module_test: ok\n";
engine::debug::timing_log_scalar("session.wall_ms", engine::debug::elapsed_ms(started));
return 0;
} catch (const std::exception & ex) {
std::cerr << "encoder_module_test: failed: " << ex.what() << "\n";
engine::debug::timing_log_scalar("session.wall_ms", engine::debug::elapsed_ms(started));
return 1;
}
}