diff --git a/.gitignore b/.gitignore index d52b81f..dcaa977 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,6 @@ cache/ # Test files test_*.wav test_*.py -*_test.cpp # Large archives *.zip diff --git a/README.md b/README.md index c300125..2fa7eb8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ For Apple platforms (macOS/iOS), use [FluidAudio](https://github.com/FluidInfere **Model Cards:** - [Parakeet V2 (English)](https://huggingface.co/FluidInference/parakeet-tdt-0.6b-v2-ov) -- [Parakeet V3 (Multilingual)](https://huggingface.co/FluidInference/parakeet-tdt-1.1b-v3-ov) +- [Parakeet V3 (Multilingual)](https://huggingface.co/FluidInference/parakeet-tdt-0.6b-v3-ov) - [Whisper large-v3-turbo](https://huggingface.co/FluidInference/whisper-large-v3-turbo-fp16-ov-npu) ## Building diff --git a/include/eddy/core/model_configs.hpp b/include/eddy/core/model_configs.hpp index d98dd01..62c493d 100644 --- a/include/eddy/core/model_configs.hpp +++ b/include/eddy/core/model_configs.hpp @@ -39,7 +39,7 @@ namespace model_configs { }; inline const ModelConfig PARAKEET_V3 = { - .repo_id = "FluidInference/parakeet-tdt-1.1b-v3-ov", + .repo_id = "FluidInference/parakeet-tdt-0.6b-v3-ov", .required_files = PARAKEET_STANDARD_FILES, .cache_subdir = "parakeet-v3" }; diff --git a/src/utils/openvino_utils.cpp b/src/utils/openvino_utils.cpp index e01e98b..f1cf9cf 100644 --- a/src/utils/openvino_utils.cpp +++ b/src/utils/openvino_utils.cpp @@ -1,6 +1,9 @@ // OpenVINO utility functions for model compilation and configuration #include "eddy/utils/openvino_utils.hpp" +#include "openvino_utils_detail.hpp" + +#include #include #include @@ -73,6 +76,36 @@ std::string to_upper(const std::string& s) { } // anonymous namespace +namespace detail { + +bool normalize_negative_log_softmax_axes(ov::Model& model) { + bool changed = false; + + for (const auto& node : model.get_ordered_ops()) { + const auto log_softmax = ov::as_type_ptr(node); + if (!log_softmax || log_softmax->get_axis() >= 0) continue; + + const auto rank = log_softmax->get_input_partial_shape(0).rank(); + if (rank.is_dynamic()) continue; + + const int64_t original_axis = log_softmax->get_axis(); + const int64_t normalized_axis = original_axis + rank.get_length(); + if (normalized_axis < 0) continue; + + log_softmax->set_axis(normalized_axis); + changed = true; + if (is_debug_enabled()) { + std::cerr << "[DEBUG] Normalized NPU LogSoftmax axis " << original_axis << " to " + << normalized_axis << " for " << log_softmax->get_friendly_name() << "\n"; + } + } + + if (changed) model.validate_nodes_and_infer_types(); + return changed; +} + +} // namespace detail + ov::CompiledModel compile_component(ov::Core& core, const ModelFile& file, const std::string& device) { if (file.path.empty()) { throw std::invalid_argument("Parakeet component path is empty"); @@ -92,6 +125,16 @@ ov::CompiledModel compile_component(ov::Core& core, const ModelFile& file, const return core.import_model(blob_stream, device); } + // Intel's NPU compiler rejects valid negative LogSoftmax axes in its + // AlignDimensionsForDPU pass. Canonicalize static-rank axes in memory; the + // model files on disk and every non-NPU path remain unchanged. + if (to_upper(device) == "NPU") { + auto model = core.read_model(file.path); + detail::normalize_negative_log_softmax_axes(*model); + if (!cfg.empty()) return core.compile_model(model, device, cfg); + return core.compile_model(model, device); + } + if (!cfg.empty()) return core.compile_model(file.path, device, cfg); return core.compile_model(file.path, device); } diff --git a/src/utils/openvino_utils_detail.hpp b/src/utils/openvino_utils_detail.hpp new file mode 100644 index 0000000..2bb3a3e --- /dev/null +++ b/src/utils/openvino_utils_detail.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace eddy::parakeet::detail { + +// Canonicalize valid negative opset5 LogSoftmax axes when the input rank is +// static. Returns true when at least one node changed. +bool normalize_negative_log_softmax_axes(ov::Model& model); + +} // namespace eddy::parakeet::detail diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..aacd752 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(openvino_utils_test openvino_utils_test.cpp) +target_include_directories(openvino_utils_test PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_link_libraries(openvino_utils_test PRIVATE eddy) + +add_test(NAME openvino_utils_test COMMAND openvino_utils_test) diff --git a/tests/openvino_utils_test.cpp b/tests/openvino_utils_test.cpp new file mode 100644 index 0000000..27b268a --- /dev/null +++ b/tests/openvino_utils_test.cpp @@ -0,0 +1,112 @@ +#include "utils/openvino_utils_detail.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +bool expect(bool condition, std::string_view message) { + if (condition) return true; + std::cerr << "FAILED: " << message << "\n"; + return false; +} + +std::shared_ptr make_log_softmax_model(const ov::PartialShape& shape, + int64_t axis, + std::shared_ptr& op) { + auto input = std::make_shared(ov::element::f32, shape); + op = std::make_shared(input, axis); + return std::make_shared(ov::OutputVector{op}, ov::ParameterVector{input}); +} + +std::vector infer_cpu(const std::shared_ptr& model, + const ov::Shape& shape, + const std::vector& values) { + ov::Core core; + auto compiled = core.compile_model(model, "CPU"); + auto request = compiled.create_infer_request(); + ov::Tensor input(ov::element::f32, shape); + std::copy(values.begin(), values.end(), input.data()); + request.set_input_tensor(input); + request.infer(); + + const auto output = request.get_output_tensor(); + return {output.data(), output.data() + output.get_size()}; +} + +bool test_last_axis_normalization_preserves_output() { + const ov::Shape shape{1, 2, 2, 3}; + std::shared_ptr op; + auto model = make_log_softmax_model(shape, -1, op); + + const std::vector values{ + -1.0f, 0.0f, 1.0f, 2.0f, -2.0f, 0.5f, + 4.0f, 3.0f, 2.0f, -3.0f, 1.0f, 5.0f, + }; + const auto before = infer_cpu(model, shape, values); + + const bool changed = eddy::parakeet::detail::normalize_negative_log_softmax_axes(*model); + const auto after = infer_cpu(model, shape, values); + + bool ok = expect(changed, "axis -1 reports a change"); + ok &= expect(op->get_axis() == 3, "rank-4 axis -1 normalizes to 3"); + ok &= expect(before.size() == after.size(), "output sizes match"); + for (size_t i = 0; i < before.size() && i < after.size(); ++i) { + ok &= expect(std::abs(before[i] - after[i]) < 1e-6f, + "normalization preserves LogSoftmax output"); + } + return ok; +} + +bool test_first_axis_normalization() { + std::shared_ptr op; + auto model = make_log_softmax_model(ov::Shape{1, 2, 3, 4}, -4, op); + const bool changed = eddy::parakeet::detail::normalize_negative_log_softmax_axes(*model); + return expect(changed, "axis -4 reports a change") && + expect(op->get_axis() == 0, "rank-4 axis -4 normalizes to 0"); +} + +bool test_positive_axis_is_unchanged() { + std::shared_ptr op; + auto model = make_log_softmax_model(ov::Shape{1, 2, 3, 4}, 2, op); + const bool changed = eddy::parakeet::detail::normalize_negative_log_softmax_axes(*model); + return expect(!changed, "positive axis reports no change") && + expect(op->get_axis() == 2, "positive axis remains unchanged"); +} + +bool test_dynamic_rank_is_unchanged() { + std::shared_ptr op; + auto model = make_log_softmax_model(ov::PartialShape::dynamic(), -1, op); + const bool changed = eddy::parakeet::detail::normalize_negative_log_softmax_axes(*model); + return expect(!changed, "dynamic-rank axis reports no change") && + expect(op->get_axis() == -1, "dynamic-rank axis remains negative"); +} + +bool test_non_log_softmax_is_untouched() { + auto input = std::make_shared(ov::element::f32, ov::Shape{1, 4}); + auto relu = std::make_shared(input); + ov::Model model(ov::OutputVector{relu}, ov::ParameterVector{input}); + return expect(!eddy::parakeet::detail::normalize_negative_log_softmax_axes(model), + "model without LogSoftmax reports no change"); +} + +} // namespace + +int main() { + bool ok = true; + ok &= test_last_axis_normalization_preserves_output(); + ok &= test_first_axis_normalization(); + ok &= test_positive_axis_is_unchanged(); + ok &= test_dynamic_rank_is_unchanged(); + ok &= test_non_log_softmax_is_untouched(); + return ok ? 0 : 1; +}