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
428 lines
16 KiB
C++
428 lines
16 KiB
C++
#include "engine/framework/core/backend.h"
|
|
#include "engine/framework/io/json.h"
|
|
#include "engine/framework/modules/speech_encoders/sanm.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <exception>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
constexpr size_t kGraphBytes = 64 * 1024 * 1024;
|
|
constexpr size_t kGraphNodes = 4096;
|
|
constexpr float kCpuAbsoluteTolerance = 2.0e-4F;
|
|
constexpr float kCudaAbsoluteTolerance = 1.0e-3F;
|
|
constexpr float kRelativeTolerance = 2.0e-4F;
|
|
constexpr const char *kTransformersCommit =
|
|
"48e7f65fb274172e15aa88875d780c67c37606c7";
|
|
|
|
struct Arguments {
|
|
engine::core::BackendType backend = engine::core::BackendType::Cpu;
|
|
std::filesystem::path reference;
|
|
std::filesystem::path data;
|
|
};
|
|
|
|
Arguments parse_arguments(int argc, char **argv) {
|
|
Arguments arguments;
|
|
for (int index = 1; index < argc; ++index) {
|
|
const std::string option(argv[index]);
|
|
if (index + 1 >= argc) {
|
|
throw std::runtime_error("missing value for " + option);
|
|
}
|
|
const std::string value(argv[++index]);
|
|
if (option == "--backend") {
|
|
if (value == "cpu") {
|
|
arguments.backend = engine::core::BackendType::Cpu;
|
|
} else if (value == "cuda") {
|
|
arguments.backend = engine::core::BackendType::Cuda;
|
|
} else {
|
|
throw std::runtime_error("unsupported SAN-M probe backend: " + value);
|
|
}
|
|
} else if (option == "--reference") {
|
|
arguments.reference = value;
|
|
} else if (option == "--data") {
|
|
arguments.data = value;
|
|
} else {
|
|
throw std::runtime_error("unknown SAN-M probe option: " + option);
|
|
}
|
|
}
|
|
if (arguments.reference.empty() || arguments.data.empty()) {
|
|
throw std::runtime_error("--reference and --data are required");
|
|
}
|
|
return arguments;
|
|
}
|
|
|
|
std::vector<float> load_f32_slice(const std::filesystem::path &data_path,
|
|
const engine::io::json::Value &descriptor) {
|
|
const int64_t signed_offset = descriptor.require("offset_f32").as_i64();
|
|
const int64_t signed_count = descriptor.require("count").as_i64();
|
|
if (signed_offset < 0 || signed_count < 0 ||
|
|
static_cast<uint64_t>(signed_count) >
|
|
std::numeric_limits<size_t>::max()) {
|
|
throw std::runtime_error("invalid SAN-M reference data slice");
|
|
}
|
|
const uint64_t offset = static_cast<uint64_t>(signed_offset);
|
|
const size_t count = static_cast<size_t>(signed_count);
|
|
if (offset >
|
|
static_cast<uint64_t>(std::numeric_limits<std::streamoff>::max()) / 4) {
|
|
throw std::runtime_error("SAN-M reference data offset is too large");
|
|
}
|
|
std::ifstream input(data_path, std::ios::binary);
|
|
input.seekg(static_cast<std::streamoff>(offset * 4));
|
|
if (!input) {
|
|
throw std::runtime_error("could not seek in SAN-M reference data");
|
|
}
|
|
std::vector<float> values(count);
|
|
for (float &value : values) {
|
|
uint8_t bytes[4]{};
|
|
input.read(reinterpret_cast<char *>(bytes), 4);
|
|
if (!input) {
|
|
throw std::runtime_error("truncated SAN-M reference data");
|
|
}
|
|
const uint32_t bits = static_cast<uint32_t>(bytes[0]) |
|
|
(static_cast<uint32_t>(bytes[1]) << 8) |
|
|
(static_cast<uint32_t>(bytes[2]) << 16) |
|
|
(static_cast<uint32_t>(bytes[3]) << 24);
|
|
static_assert(sizeof(value) == sizeof(bits));
|
|
std::memcpy(&value, &bits, sizeof(value));
|
|
}
|
|
return values;
|
|
}
|
|
|
|
engine::core::TensorShape
|
|
descriptor_shape(const engine::io::json::Value &descriptor) {
|
|
const auto dimensions =
|
|
engine::io::json::require_i64_array(descriptor, "shape");
|
|
if (dimensions.empty() || dimensions.size() > engine::core::kMaxTensorRank) {
|
|
throw std::runtime_error("SAN-M reference tensor rank is invalid");
|
|
}
|
|
engine::core::TensorShape shape;
|
|
shape.rank = dimensions.size();
|
|
for (size_t axis = 0; axis < dimensions.size(); ++axis) {
|
|
if (dimensions[axis] <= 0) {
|
|
throw std::runtime_error(
|
|
"SAN-M reference tensor dimension must be positive");
|
|
}
|
|
shape.dims[axis] = dimensions[axis];
|
|
}
|
|
return shape;
|
|
}
|
|
|
|
class GraphRunner {
|
|
public:
|
|
explicit GraphRunner(engine::core::BackendType backend_type)
|
|
: backend_(engine::core::init_backend({backend_type, 0, 4})) {
|
|
if (backend_ == nullptr) {
|
|
throw std::runtime_error("failed to initialize SAN-M probe backend");
|
|
}
|
|
engine::core::set_backend_threads(backend_, 4);
|
|
ggml_init_params parameters{};
|
|
parameters.mem_size = kGraphBytes;
|
|
parameters.mem_buffer = nullptr;
|
|
parameters.no_alloc = true;
|
|
ggml_ = ggml_init(parameters);
|
|
if (ggml_ == nullptr) {
|
|
throw std::runtime_error(
|
|
"failed to initialize SAN-M probe graph context");
|
|
}
|
|
context_.ggml = ggml_;
|
|
context_.module_instance_name = "fun_asr_nano_sanm_probe";
|
|
context_.backend_type = backend_type;
|
|
}
|
|
|
|
~GraphRunner() {
|
|
if (buffer_ != nullptr) {
|
|
ggml_backend_buffer_free(buffer_);
|
|
}
|
|
if (ggml_ != nullptr) {
|
|
ggml_free(ggml_);
|
|
}
|
|
if (backend_ != nullptr) {
|
|
ggml_backend_free(backend_);
|
|
}
|
|
}
|
|
|
|
GraphRunner(const GraphRunner &) = delete;
|
|
GraphRunner &operator=(const GraphRunner &) = delete;
|
|
|
|
engine::core::ModuleBuildContext &context() { return context_; }
|
|
|
|
engine::core::TensorValue make_f32(const engine::core::TensorShape &shape,
|
|
std::vector<float> values) {
|
|
if (static_cast<int64_t>(values.size()) != shape.num_elements()) {
|
|
throw std::runtime_error("SAN-M tensor data does not match its shape");
|
|
}
|
|
auto tensor = engine::core::make_tensor(context_, GGML_TYPE_F32, shape);
|
|
pending_.push_back({tensor, std::move(values)});
|
|
return tensor;
|
|
}
|
|
|
|
std::vector<float> run(const engine::core::TensorValue &output) {
|
|
buffer_ = ggml_backend_alloc_ctx_tensors(ggml_, backend_);
|
|
if (buffer_ == nullptr) {
|
|
throw std::runtime_error("failed to allocate SAN-M probe tensors");
|
|
}
|
|
for (const auto &pending : pending_) {
|
|
engine::core::write_tensor_f32(pending.tensor, pending.values);
|
|
}
|
|
ggml_cgraph *graph = ggml_new_graph_custom(ggml_, kGraphNodes, false);
|
|
ggml_build_forward_expand(graph, output.tensor);
|
|
engine::core::validate_backend_graph_supported(backend_, graph,
|
|
"SAN-M probe");
|
|
const auto status = engine::core::compute_backend_graph(
|
|
backend_, graph, nullptr, "SAN-M probe");
|
|
if (status != GGML_STATUS_SUCCESS) {
|
|
throw std::runtime_error("SAN-M probe graph execution failed");
|
|
}
|
|
return engine::core::read_tensor_f32(output.tensor);
|
|
}
|
|
|
|
private:
|
|
struct PendingTensor {
|
|
engine::core::TensorValue tensor;
|
|
std::vector<float> values;
|
|
};
|
|
|
|
ggml_backend_t backend_ = nullptr;
|
|
ggml_backend_buffer_t buffer_ = nullptr;
|
|
ggml_context *ggml_ = nullptr;
|
|
engine::core::ModuleBuildContext context_{};
|
|
std::vector<PendingTensor> pending_;
|
|
};
|
|
|
|
class TensorLoader {
|
|
public:
|
|
TensorLoader(GraphRunner &runner, std::filesystem::path data_path)
|
|
: runner_(runner), data_path_(std::move(data_path)) {}
|
|
|
|
engine::core::TensorValue load(const engine::io::json::Value &descriptor) {
|
|
return runner_.make_f32(descriptor_shape(descriptor),
|
|
load_f32_slice(data_path_, descriptor));
|
|
}
|
|
|
|
engine::core::TensorValue load_named(const engine::io::json::Value &weights,
|
|
const std::string &name) {
|
|
return load(weights.require(name));
|
|
}
|
|
|
|
engine::modules::LinearWeights
|
|
load_linear(const engine::io::json::Value &weights,
|
|
const std::string &prefix) {
|
|
return {
|
|
load_named(weights, prefix + ".weight"),
|
|
load_named(weights, prefix + ".bias"),
|
|
};
|
|
}
|
|
|
|
engine::modules::NormWeights load_norm(const engine::io::json::Value &weights,
|
|
const std::string &prefix) {
|
|
return {
|
|
load_named(weights, prefix + ".weight"),
|
|
load_named(weights, prefix + ".bias"),
|
|
};
|
|
}
|
|
|
|
private:
|
|
GraphRunner &runner_;
|
|
std::filesystem::path data_path_;
|
|
};
|
|
|
|
engine::modules::SanmBlockWeightsView
|
|
load_weights(TensorLoader &loader, const engine::io::json::Value &weights) {
|
|
return {
|
|
loader.load_norm(weights, "self_attn_layer_norm"),
|
|
loader.load_linear(weights, "self_attn.q_proj"),
|
|
loader.load_linear(weights, "self_attn.k_proj"),
|
|
loader.load_linear(weights, "self_attn.v_proj"),
|
|
loader.load_linear(weights, "self_attn.out_proj"),
|
|
loader.load_named(weights, "feedforward_sequential_memory.conv.weight"),
|
|
loader.load_norm(weights, "final_layer_norm"),
|
|
loader.load_linear(weights, "fc1"),
|
|
loader.load_linear(weights, "fc2"),
|
|
};
|
|
}
|
|
|
|
void require_close(const std::string &label, const std::vector<float> &actual,
|
|
const std::vector<float> &expected,
|
|
engine::core::BackendType backend_type);
|
|
|
|
void run_layer_norm(const std::string &name,
|
|
const engine::io::json::Value &block,
|
|
const engine::io::json::Value &config_json,
|
|
const std::filesystem::path &data_path,
|
|
engine::core::BackendType backend_type) {
|
|
GraphRunner runner(backend_type);
|
|
TensorLoader loader(runner, data_path);
|
|
const auto input = loader.load(block.require("input"));
|
|
const auto weights =
|
|
loader.load_norm(block.require("weights"), "self_attn_layer_norm");
|
|
const auto output = engine::modules::sanm_layer_norm(
|
|
runner.context(), input, weights,
|
|
engine::io::json::require_f32(config_json, "layer_norm_eps"));
|
|
const auto actual = runner.run(output);
|
|
const auto expected = load_f32_slice(
|
|
data_path, block.require("checkpoints").require("self_attn_layer_norm"));
|
|
require_close(name + ".self_attn_layer_norm", actual, expected, backend_type);
|
|
}
|
|
|
|
void require_close(const std::string &label, const std::vector<float> &actual,
|
|
const std::vector<float> &expected,
|
|
engine::core::BackendType backend_type) {
|
|
if (actual.size() != expected.size()) {
|
|
throw std::runtime_error(label + " output size mismatch");
|
|
}
|
|
float maximum_absolute_error = 0.0F;
|
|
float maximum_relative_error = 0.0F;
|
|
size_t worst_index = 0;
|
|
for (size_t index = 0; index < actual.size(); ++index) {
|
|
const float absolute_error = std::abs(actual[index] - expected[index]);
|
|
const float relative_error =
|
|
absolute_error / std::max(std::abs(expected[index]), 1.0e-12F);
|
|
if (absolute_error > maximum_absolute_error) {
|
|
maximum_absolute_error = absolute_error;
|
|
worst_index = index;
|
|
}
|
|
maximum_relative_error = std::max(maximum_relative_error, relative_error);
|
|
const float absolute_tolerance =
|
|
backend_type == engine::core::BackendType::Cuda ? kCudaAbsoluteTolerance
|
|
: kCpuAbsoluteTolerance;
|
|
const float limit =
|
|
absolute_tolerance + kRelativeTolerance * std::abs(expected[index]);
|
|
if (absolute_error > limit) {
|
|
throw std::runtime_error(
|
|
label + " mismatch at " + std::to_string(index) + ": expected " +
|
|
std::to_string(expected[index]) + ", got " +
|
|
std::to_string(actual[index]) + ", absolute error " +
|
|
std::to_string(absolute_error) + ", limit " + std::to_string(limit));
|
|
}
|
|
}
|
|
std::cout << label << ": count=" << actual.size()
|
|
<< ", max_abs=" << maximum_absolute_error
|
|
<< ", max_rel=" << maximum_relative_error
|
|
<< ", worst_index=" << worst_index << '\n';
|
|
}
|
|
|
|
void run_block(const std::string &name, const engine::io::json::Value &block,
|
|
const engine::io::json::Value &config_json,
|
|
const std::filesystem::path &data_path,
|
|
engine::core::BackendType backend_type) {
|
|
GraphRunner runner(backend_type);
|
|
TensorLoader loader(runner, data_path);
|
|
const auto input = loader.load(block.require("input"));
|
|
const auto weights = load_weights(loader, block.require("weights"));
|
|
engine::modules::SanmBlockConfig config;
|
|
config.input_size =
|
|
name == "projection"
|
|
? engine::io::json::require_i64(config_json, "input_size")
|
|
: engine::io::json::require_i64(config_json, "model_size");
|
|
config.model_size = engine::io::json::require_i64(config_json, "model_size");
|
|
config.num_heads = engine::io::json::require_i64(config_json, "num_heads");
|
|
config.ffn_size = engine::io::json::require_i64(config_json, "ffn_size");
|
|
config.fsmn_kernel_size =
|
|
engine::io::json::require_i64(config_json, "fsmn_kernel_size");
|
|
config.layer_norm_eps =
|
|
engine::io::json::require_f32(config_json, "layer_norm_eps");
|
|
config.attention_lowering =
|
|
engine::modules::ScaledDotProductAttentionLowering::Explicit;
|
|
|
|
const auto output = name == "projection"
|
|
? engine::modules::sanm_projection_block(
|
|
runner.context(), input, weights, config)
|
|
: engine::modules::sanm_residual_block(
|
|
runner.context(), input, weights, config);
|
|
const auto actual = runner.run(output);
|
|
const auto expected = load_f32_slice(data_path, block.require("output"));
|
|
require_close(name, actual, expected, backend_type);
|
|
}
|
|
|
|
void require_runtime_error(const std::string &label,
|
|
const std::string &expected_message,
|
|
const std::function<void()> &operation) {
|
|
try {
|
|
operation();
|
|
} catch (const std::runtime_error &error) {
|
|
if (std::string(error.what()).find(expected_message) != std::string::npos) {
|
|
return;
|
|
}
|
|
throw std::runtime_error(label +
|
|
" returned an unexpected error: " + error.what());
|
|
}
|
|
throw std::runtime_error(label + " did not reject the invalid configuration");
|
|
}
|
|
|
|
void run_config_contracts() {
|
|
GraphRunner runner(engine::core::BackendType::Cpu);
|
|
const auto input8 =
|
|
runner.make_f32(engine::core::TensorShape::from_dims({1, 2, 8}),
|
|
std::vector<float>(16, 0.0F));
|
|
const auto input7 =
|
|
runner.make_f32(engine::core::TensorShape::from_dims({1, 2, 7}),
|
|
std::vector<float>(14, 0.0F));
|
|
const engine::modules::SanmBlockWeightsView weights;
|
|
engine::modules::SanmBlockConfig config{
|
|
8, 8, 2, 16, 3,
|
|
};
|
|
|
|
auto bad_heads = config;
|
|
bad_heads.num_heads = 3;
|
|
require_runtime_error("SAN-M head divisibility", "divisible", [&] {
|
|
static_cast<void>(engine::modules::sanm_projection_block(
|
|
runner.context(), input8, weights, bad_heads));
|
|
});
|
|
|
|
auto even_kernel = config;
|
|
even_kernel.fsmn_kernel_size = 4;
|
|
require_runtime_error("SAN-M FSMN kernel", "odd", [&] {
|
|
static_cast<void>(engine::modules::sanm_projection_block(
|
|
runner.context(), input8, weights, even_kernel));
|
|
});
|
|
|
|
auto mismatched_residual = config;
|
|
mismatched_residual.input_size = 7;
|
|
require_runtime_error(
|
|
"SAN-M residual dimensions", "input_size == model_size", [&] {
|
|
static_cast<void>(engine::modules::sanm_residual_block(
|
|
runner.context(), input7, weights, mismatched_residual));
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char **argv) try {
|
|
const auto arguments = parse_arguments(argc, argv);
|
|
const auto reference = engine::io::json::parse_file(arguments.reference);
|
|
if (reference.require("schema_version").as_i64() != 1 ||
|
|
reference.require("transformers_commit").as_string() !=
|
|
kTransformersCommit ||
|
|
reference.require("data_format").as_string() != "little-endian-float32") {
|
|
throw std::runtime_error("unexpected SAN-M reference metadata");
|
|
}
|
|
run_config_contracts();
|
|
const auto &blocks = reference.require("blocks");
|
|
const auto &config = reference.require("config");
|
|
run_layer_norm("projection", blocks.require("projection"), config,
|
|
arguments.data, arguments.backend);
|
|
run_layer_norm("residual", blocks.require("residual"), config, arguments.data,
|
|
arguments.backend);
|
|
run_block("projection", blocks.require("projection"), config, arguments.data,
|
|
arguments.backend);
|
|
run_block("residual", blocks.require("residual"), config, arguments.data,
|
|
arguments.backend);
|
|
return 0;
|
|
} catch (const std::exception &error) {
|
|
std::cerr << "fun_asr_nano_sanm_probe failed: " << error.what() << '\n';
|
|
return 1;
|
|
}
|