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

214 lines
11 KiB
C++

#include "engine/framework/assets/weight_adapter.h"
#include "engine/framework/core/backend.h"
#include "engine/framework/debug/trace.h"
#include "engine/framework/io/safetensors.h"
#include "test_assert.h"
#include <cstring>
#include <filesystem>
#include <iostream>
namespace {
using namespace engine;
using test::require;
template<class T>
std::vector<unsigned char> bytes(const std::vector<T> & values) {
std::vector<unsigned char> out(values.size() * sizeof(T));
std::memcpy(out.data(), values.data(), out.size());
return out;
}
template<class F>
void rejects(F fn) {
bool threw = false;
try { fn(); } catch (const std::runtime_error &) { threw = true; }
require(threw, "invalid overlay was accepted");
}
void check_upload(const assets::TensorSource & source, const std::string & name,
const std::vector<int64_t> & shape, assets::TensorStorageType type,
const assets::TensorSource * reference = nullptr) {
auto backend = core::init_backend({core::BackendType::Cpu, 0, 1});
auto ctx = ggml_init({ggml_tensor_overhead() * 2, nullptr, true});
auto tensor = ggml_new_tensor_2d(ctx, assets::ggml_type_for_tensor_storage(type), shape[1], shape[0]);
auto buffer = ggml_backend_alloc_ctx_tensors(ctx, backend);
require(buffer != nullptr, "test backend allocation failed");
source.set_backend_tensor(tensor, name, type, shape);
std::vector<std::byte> actual(ggml_nbytes(tensor));
ggml_backend_tensor_get(tensor, actual.data(), 0, actual.size());
const auto expected = (reference ? *reference : source).require_tensor(name, type, shape);
ggml_backend_buffer_free(buffer);
ggml_free(ctx);
ggml_backend_free(backend);
require(actual == expected.bytes, "backend upload differs from raw export conversion");
}
class CountingSource final : public assets::TensorSource {
public:
explicit CountingSource(std::shared_ptr<const assets::TensorSource> source) : source_(std::move(source)) {}
const std::filesystem::path & source_path() const noexcept override { return source_->source_path(); }
bool has_tensor(std::string_view name) const noexcept override { return source_->has_tensor(name); }
assets::TensorMetadata require_metadata(std::string_view name) const override {
return source_->require_metadata(name);
}
std::vector<assets::TensorMetadata> tensors() const override { return source_->tensors(); }
void release_storage() const override { source_->release_storage(); }
assets::RawTensorData require_tensor_data(std::string_view name) const override {
return source_->require_tensor_data(name);
}
std::vector<float> require_f32(std::string_view name,
const std::optional<std::vector<int64_t>> & shape) const override {
++reads;
return source_->require_f32(name, shape);
}
std::optional<std::vector<float>> optional_f32(std::string_view name,
const std::optional<std::vector<int64_t>> & shape) const override {
return source_->optional_f32(name, shape);
}
int64_t require_i64_scalar(std::string_view name) const override { return source_->require_i64_scalar(name); }
mutable size_t reads = 0;
private:
std::shared_ptr<const assets::TensorSource> source_;
};
void run(const std::filesystem::path & root) {
std::vector<float> weights(64), a(64), b = {1.0F, 1.0F, -0.75F, 0.25F};
for (size_t i = 0; i < weights.size(); ++i) {
weights[i] = static_cast<float>(i) / 9.0F;
a[i] = static_cast<float>(static_cast<int>(i % 11) - 5) / 7.0F;
}
// Rank-wise accumulation must not silently become a summed delta + base.
weights[0] = 100000000.0F;
a[0] = a[32] = 4.0F;
io::write_safetensors_file(root / "base.safetensors", {
{"weight", "F32", {2, 32}, bytes(weights)},
{"untouched", "F32", {2, 32}, bytes(weights)},
{"step", "I64", {1}, bytes(std::vector<int64_t>{42})}
});
const auto base = assets::open_tensor_source(root / "base.safetensors");
require(assets::make_weight_adapter_source(base, {}).get() == base.get(), "empty overlay must be identity");
assets::LowRankWeightUpdate delta{a, b, 2, 32, 2};
assets::WeightUpdate update{delta, 1.0F, assets::WeightMergeMode::RankWiseF32};
auto overlay = assets::make_weight_adapter_source(base, {{{"weight", update}}, {}});
auto expected = weights;
for (int o = 0; o < 2; ++o) {
for (int k = 0; k < 2; ++k) {
const float scaled_b = update.scale * b[o * 2 + k];
for (int i = 0; i < 32; ++i) expected[o * 32 + i] += scaled_b * a[k * 32 + i];
}
}
require(expected[0] == weights[0], "rounding fixture must retain original rank-wise result");
require(overlay->require_f32("weight") == expected, "merge arithmetic changed");
require(overlay->require_f32("weight") == expected, "repeated read applies adapter twice");
require(base->require_f32("weight") == weights, "overlay changed base storage");
auto rounded_delta = update;
rounded_delta.merge_mode = assets::WeightMergeMode::RoundedBF16Delta;
auto rounded = assets::make_weight_adapter_source(base, {{{"weight", rounded_delta}}, {}});
auto rounded_expected = weights;
for (int o = 0; o < 2; ++o) {
for (int i = 0; i < 32; ++i) {
float product = 0.0F;
for (int k = 0; k < 2; ++k) product += b[o * 2 + k] * a[k * 32 + i];
const float update = ggml_bf16_to_fp32(ggml_fp32_to_bf16(product));
rounded_expected[o * 32 + i] = ggml_bf16_to_fp32(
ggml_fp32_to_bf16(weights[o * 32 + i] + update));
}
}
require(rounded_expected != expected, "merge modes need distinct rounding fixtures");
require(rounded->require_f32("weight") == rounded_expected, "BF16 delta merge differs");
require(overlay->require_tensor_data("untouched").bytes == base->require_tensor_data("untouched").bytes,
"unadapted tensor changed");
require(overlay->require_metadata("weight").dtype == base->require_metadata("weight").dtype,
"storage metadata changed");
require(overlay->source_path() == base->source_path(), "base path changed");
require(overlay->tensors().size() == base->tensors().size(), "tensor inventory changed");
require(overlay->require_i64_scalar("step") == 42, "scalar passthrough failed");
require(!overlay->optional_f32("missing"), "missing optional tensor should remain absent");
require(overlay->optional_f32("weight").value() == expected, "optional merge differs");
rejects([&] { (void)overlay->require_f32("weight", {32, 2}); });
auto bad = delta;
bad.a.pop_back();
rejects([&] { assets::make_weight_adapter_source(base, {{{"weight", {bad, 1.0F, assets::WeightMergeMode::RankWiseF32}}}, {}}); });
const std::vector<float> replacement(64, 0.75F);
auto overridden = assets::make_weight_adapter_source(base, {{{"weight", update}},
{{"weight", {{2, 32}, replacement}}}});
require(overridden->require_f32("weight") == replacement, "full override must win over delta");
rejects([&] { (void)overridden->require_f32("weight", {32, 2}); });
rejects([&] { assets::make_weight_adapter_source(base, {{}, {{"weight", {{2, 32}, {1.0F}}}}}); });
rejects([&] { assets::make_weight_adapter_source(base, {{}, {{"missing", {{2, 32}, replacement}}}}); });
for (auto type : {assets::TensorStorageType::F32, assets::TensorStorageType::F16,
assets::TensorStorageType::BF16, assets::TensorStorageType::Q8_0,
assets::TensorStorageType::Q4_0}) {
check_upload(*overlay, "weight", {2, 32}, type);
check_upload(*rounded, "weight", {2, 32}, type);
check_upload(*overlay, "untouched", {2, 32}, type);
check_upload(*overridden, "weight", {2, 32}, type);
}
overlay->release_storage();
require(overlay->require_f32("weight") == expected, "release/reopen changed merge");
for (auto mode : {assets::WeightMergeMode::RankWiseF32, assets::WeightMergeMode::RoundedBF16Delta}) {
auto counted = std::make_shared<CountingSource>(base);
auto cached_delta = update;
cached_delta.merge_mode = mode;
auto reference = assets::make_weight_adapter_source(base, {{{"weight", cached_delta}}, {}});
auto cached = assets::make_weight_adapter_source(counted, {{{"weight", cached_delta}}, {}}, "cached", true);
size_t expected_reads = 0;
for (auto type : {assets::TensorStorageType::F32, assets::TensorStorageType::F16,
assets::TensorStorageType::BF16, assets::TensorStorageType::Q8_0,
assets::TensorStorageType::Q4_0, assets::TensorStorageType::Q8_0,
assets::TensorStorageType::Q4_0}) {
check_upload(*cached, "weight", {2, 32}, type, reference.get());
require(counted->reads == ++expected_reads, "dtype replacement did not merge from original base");
cached->release_storage();
for (int repeat = 0; repeat < 3; ++repeat) {
check_upload(*cached, "weight", {2, 32}, type, reference.get());
require(counted->reads == expected_reads, "cached upload re-read/re-merged weights");
}
}
auto uncached = assets::make_weight_adapter_source(counted, {{{"weight", cached_delta}}, {}});
for (int repeat = 0; repeat < 2; ++repeat) {
check_upload(*uncached, "weight", {2, 32}, assets::TensorStorageType::Q8_0, reference.get());
require(counted->reads == ++expected_reads, "default overlay unexpectedly caches weights");
}
}
// Exercise retained bytes in the parallel F16/BF16 conversion paths too.
std::vector<float> large_weights(1024 * 1024);
for (size_t i = 0; i < large_weights.size(); ++i) large_weights[i] = float(i % 997) / 997.0F;
io::write_safetensors_file(root / "large.safetensors", {
{"weight", "F32", {1024, 1024}, bytes(large_weights)}
});
auto large_base = assets::open_tensor_source(root / "large.safetensors");
assets::LowRankWeightUpdate large_delta;
large_delta.in = large_delta.out = 1024;
large_delta.r = 1;
large_delta.a.assign(1024, 0.123F);
large_delta.b.assign(1024, -0.456F);
assets::WeightUpdate large_update{large_delta, 1.0F, assets::WeightMergeMode::RankWiseF32};
auto large_cached = assets::make_weight_adapter_source(large_base, {{{"weight", large_update}}, {}}, "large", true);
auto large_reference = assets::make_weight_adapter_source(large_base, {{{"weight", large_update}}, {}});
for (auto type : {assets::TensorStorageType::F16, assets::TensorStorageType::BF16}) {
check_upload(*large_reference, "weight", {1024, 1024}, type);
check_upload(*large_cached, "weight", {1024, 1024}, type, large_reference.get());
check_upload(*large_cached, "weight", {1024, 1024}, type, large_reference.get());
}
}
} // namespace
int main(int argc, char ** argv) {
const auto root = std::filesystem::temp_directory_path() / "audiocpp_weight_adapter_source_test";
try {
debug::configure_logging({argc > 1 && std::string(argv[1]) == "--log", std::nullopt});
std::filesystem::create_directories(root);
run(root);
std::filesystem::remove_all(root);
std::cout << "weight_adapter_source_test passed\n";
} catch (const std::exception & e) {
std::filesystem::remove_all(root);
std::cerr << e.what() << '\n';
return 1;
}
}