Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ cache/
# Test files
test_*.wav
test_*.py
*_test.cpp

# Large archives
*.zip
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion include/eddy/core/model_configs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};
Expand Down
43 changes: 43 additions & 0 deletions src/utils/openvino_utils.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// OpenVINO utility functions for model compilation and configuration

#include "eddy/utils/openvino_utils.hpp"
#include "openvino_utils_detail.hpp"

#include <openvino/op/log_softmax.hpp>

#include <algorithm>
#include <cctype>
Expand Down Expand Up @@ -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<ov::op::v5::LogSoftmax>(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");
Expand All @@ -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);
}
Expand Down
11 changes: 11 additions & 0 deletions src/utils/openvino_utils_detail.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include <openvino/core/model.hpp>

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
5 changes: 5 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
112 changes: 112 additions & 0 deletions tests/openvino_utils_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include "utils/openvino_utils_detail.hpp"

#include <openvino/openvino.hpp>
#include <openvino/op/log_softmax.hpp>
#include <openvino/op/parameter.hpp>
#include <openvino/op/relu.hpp>

#include <algorithm>
#include <cmath>
#include <iostream>
#include <memory>
#include <string_view>
#include <vector>

namespace {

bool expect(bool condition, std::string_view message) {
if (condition) return true;
std::cerr << "FAILED: " << message << "\n";
return false;
}

std::shared_ptr<ov::Model> make_log_softmax_model(const ov::PartialShape& shape,
int64_t axis,
std::shared_ptr<ov::op::v5::LogSoftmax>& op) {
auto input = std::make_shared<ov::op::v0::Parameter>(ov::element::f32, shape);
op = std::make_shared<ov::op::v5::LogSoftmax>(input, axis);
return std::make_shared<ov::Model>(ov::OutputVector{op}, ov::ParameterVector{input});
}

std::vector<float> infer_cpu(const std::shared_ptr<ov::Model>& model,
const ov::Shape& shape,
const std::vector<float>& 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<float>());
request.set_input_tensor(input);
request.infer();

const auto output = request.get_output_tensor();
return {output.data<const float>(), output.data<const float>() + output.get_size()};
}

bool test_last_axis_normalization_preserves_output() {
const ov::Shape shape{1, 2, 2, 3};
std::shared_ptr<ov::op::v5::LogSoftmax> op;
auto model = make_log_softmax_model(shape, -1, op);

const std::vector<float> 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<ov::op::v5::LogSoftmax> 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<ov::op::v5::LogSoftmax> 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<ov::op::v5::LogSoftmax> 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::op::v0::Parameter>(ov::element::f32, ov::Shape{1, 4});
auto relu = std::make_shared<ov::op::v0::Relu>(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;
}
Loading