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
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ if (ENGINE_ENABLE_NEON)
add_library(engine_neon OBJECT
src/framework/neon/tensor.cpp
src/framework/neon/operators.cpp
src/framework/neon/tensor_operators.cpp
src/framework/neon/execution.cpp
src/framework/neon/worker_team.cpp
src/framework/neon/blocks.cpp
Expand Down Expand Up @@ -971,6 +972,9 @@ audiocpp_add_model(micro_wake_word
LOADERS
engine::models::micro_wake_word::make_micro_wake_word_loader
)
if(ENGINE_ENABLE_NEON)
target_sources(engine_model_micro_wake_word PRIVATE src/models/micro_wake_word/neon_network.cpp)
endif()

audiocpp_add_model(sherpa_kws
SOURCES
Expand Down Expand Up @@ -3382,6 +3386,9 @@ if (ENGINE_BUILD_TESTS OR ENGINE_BUILD_EXTENDED_TESTS OR ENGINE_BUILD_MODEL_TEST
target_include_directories(neon_library_test PRIVATE tests/unittests)
target_link_libraries(neon_library_test PRIVATE engine_neon)
add_test(NAME neon_library_test COMMAND neon_library_test)
add_executable(neon_tensor_ops_test tests/unittests/test_neon_tensor_ops.cpp)
target_link_libraries(neon_tensor_ops_test PRIVATE engine_neon)
add_test(NAME neon_tensor_ops_test COMMAND neon_tensor_ops_test)
endif()
if (sanotts IN_LIST AUDIOCPP_LINKED_MODELS)
add_engine_unittest(sanotts_neon_decoder_test tests/unittests/test_sanotts_neon_decoder.cpp)
Expand Down
21 changes: 21 additions & 0 deletions docs/models/micro_wake_word.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,24 @@ build/debug/bin/audiocpp_cli --family micro_wake_word --task wake \
Detections are returned as speech segments. `text` contains the wake phrase, `confidence` contains the smoothed probability, and the sample span identifies the feature block that triggered the detection. Request options `threshold` and `sliding_window_size` override the values copied from the model manifest.

Use `--mode streaming` for incremental audio.

## Optional NEON network

The default network remains the existing GGML implementation on the session
backend. On AArch64, build with `-DENGINE_ENABLE_NEON=ON` and select the native
stateful CNN with:

```bash
--session-option 'micro_wake_word.exec_plan={"network":{"impl":"neon"}}'
```

The NEON network runs on CPU, uses the session thread count, and works in both
offline and streaming modes. The fixed-point frontend, input quantization,
activation quantization boundaries, recurrent-state updates, probability
smoothing and detection thresholds retain their existing behavior. The native
network supports batch-one MixedNet manifests with unit spatial width and
depthwise multiplier one.

`impl: "ggml"` or `impl: "default"` retains the original path. The component
accepts no artifacts or params. Existing GGUFs can use the new session option
without reconversion, including when loading their embedded spec.
36 changes: 35 additions & 1 deletion include/engine/framework/neon/operators.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@ class Conv2d : public Operator {
class DepthwiseConv1d : public Operator {
public:
DepthwiseConv1d(int channels, int kernel_size, const std::vector<float> &weight, const std::vector<float> &bias);
DepthwiseConv1d(Conv1dConfig config, const std::vector<float> &weight, const std::vector<float> &bias);
};

class LayerNorm : public Operator {
public:
LayerNorm(const std::vector<float> &weight, const std::vector<float> &bias, float epsilon);
};

enum class ActivationKind { SiLU, GELU, LeakyReLU };
enum class ActivationKind { SiLU, GELU, LeakyReLU, Sigmoid };

class Activation : public Operator {
public:
Expand Down Expand Up @@ -121,6 +122,39 @@ class ConcatChannels : public Operator {
ConcatChannels();
};

// Logical-axis operations on contiguous channel-first tensors.
class Concat : public Operator {
public:
explicit Concat(int axis);
};

class Slice : public Operator {
public:
Slice(int axis, int start, int length);
};

class Reshape : public Operator {
public:
explicit Reshape(Shape shape);
};

// Matrix transpose: [channels, frames] becomes [frames, channels].
class Transpose2d : public Operator {
public:
Transpose2d();
};

class Clamp : public Operator {
public:
Clamp(float minimum, float maximum);
};

// Round ties away from zero, clamp to integer bounds, then dequantize.
class QuantizeDequantize : public Operator {
public:
QuantizeDequantize(float scale, int zero_point, int minimum, int maximum);
};

// Spatial dimensions follow the input; values do not depend on its contents.
class ConstantChannels : public Operator {
public:
Expand Down
25 changes: 25 additions & 0 deletions include/engine/models/micro_wake_word/neon_network.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include "engine/models/micro_wake_word/runtime.h"
#include "engine/framework/neon/execution.h"

namespace engine::models::micro_wake_word {

class NeonNetwork {
public:
NeonNetwork(const assets::TensorSource &source, const io::json::Value &config,
const std::vector<NetworkTensorInfo> &tensors, int threads);
float infer(const std::vector<float> &input);
void reset();

private:
neon::ExecutionPlan plan_;
neon::Shape input_shape_{1};
int threads_;
float output_scale_ = 1.0F;
std::vector<neon::Tensor> states_;
std::vector<neon::ExecutionPlan::Value> state_outputs_;
std::vector<neon::TensorView> inputs_;
};

} // namespace engine::models::micro_wake_word
11 changes: 10 additions & 1 deletion include/engine/models/micro_wake_word/runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>

Expand All @@ -20,11 +21,19 @@ struct ModelConfig {
std::string wake_phrase;
};

struct NetworkTensorInfo {
std::vector<int64_t> tflite_shape;
std::string type;
std::optional<float> scale;
int64_t zero_point = 0;
bool per_channel_quantized = false;
};

class Runtime {
public:
Runtime(std::shared_ptr<const assets::TensorSource> source,
const io::json::Value & config,
core::ExecutionContext & execution);
core::ExecutionContext & execution, bool use_neon = false);
~Runtime();

Runtime(const Runtime &) = delete;
Expand Down
10 changes: 9 additions & 1 deletion model_specs/micro_wake_word.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,15 @@
"min": 1
}
],
"session": [],
"session": [
{
"name": "exec_plan",
"type": "string",
"description": "JSON component execution plans. Component network accepts impl ggml/neon (default ggml). GGML retains the session backend; neon runs the stateful CNN on CPU and requires ENGINE_ENABLE_NEON=ON and AArch64 NEON. Frontend and detection logic are unchanged. No artifacts or params are used.",
"required": false,
"default": "{}"
}
],
"load": []
},
"package_defaults": {
Expand Down
30 changes: 30 additions & 0 deletions src/framework/neon/kernels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,36 @@ bool available() noexcept {
#endif
}

void quantize_dequantize(const float *input, float *output, size_t count, float scale,
float minimum, float maximum) noexcept {
const float inverse = 1.0F / scale;
size_t i = 0;
#if defined(__aarch64__) && defined(__ARM_NEON)
for (; i + 4 <= count; i += 4) {
auto value = vrndaq_f32(vmulq_n_f32(vld1q_f32(input + i), inverse));
value = vmaxq_f32(vdupq_n_f32(minimum), vminq_f32(vdupq_n_f32(maximum), value));
vst1q_f32(output + i, vmulq_n_f32(value, scale));
}
#endif
for (; i < count; ++i) output[i] = std::clamp(std::round(input[i] * inverse), minimum, maximum) * scale;
}

void depthwise_conv1d(const float *input, const float *weight, const float *bias, float *output,
const Conv1dConfig &config, int frames, int output_frames, WorkerContext &worker) noexcept {
int begin, end;
split_range(config.input_channels, worker.index, worker.count, begin, end);
for (int channel = begin; channel < end; ++channel) {
for (int frame = 0; frame < output_frames; ++frame) {
float value = bias ? bias[channel] : 0.0F;
for (int tap = 0; tap < config.kernel_size; ++tap) {
const int at = frame * config.stride - config.padding_left + tap * config.dilation;
if (at >= 0 && at < frames) value += input[channel * frames + at] * weight[channel * config.kernel_size + tap];
}
output[channel * output_frames + frame] = value;
}
}
}

PackedConvWeights pack_conv_weights(const float *w, const float *b, int cin, int cout, int kernel) {
if (!w || cin <= 0 || cout <= 0 || cout > std::numeric_limits<int>::max() - 3 || kernel <= 0 ||
cin > std::numeric_limits<int>::max() / kernel) {
Expand Down
5 changes: 5 additions & 0 deletions src/framework/neon/kernels.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@

namespace engine::neon::detail {

void quantize_dequantize(const float *input, float *output, size_t count, float scale,
float minimum, float maximum) noexcept;
void depthwise_conv1d(const float *input, const float *weight, const float *bias, float *output,
const Conv1dConfig &config, int frames, int output_frames, WorkerContext &worker) noexcept;

bool available() noexcept;

struct PackedConvWeights {
Expand Down
42 changes: 41 additions & 1 deletion src/framework/neon/operators.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,41 @@ class DepthwiseConv1dKernel final : public detail::Kernel {
std::vector<float> weight_, bias_;
};

class ConfiguredDepthwiseConv1dKernel final : public detail::Kernel {
public:
ConfiguredDepthwiseConv1dKernel(Conv1dConfig config, std::vector<float> weight, std::vector<float> bias)
: config_(config), weight_(std::move(weight)), bias_(std::move(bias)) {
if (config.input_channels <= 0 || config.input_channels != config.output_channels || config.kernel_size <= 0 ||
config.stride <= 0 || config.dilation <= 0 || config.padding_left < 0 || config.padding_right < 0 ||
weight_.size() != size_t(config.input_channels) * config.kernel_size ||
(!bias_.empty() && bias_.size() != size_t(config.input_channels))) {
throw std::invalid_argument("NEON depthwise convolution configuration invalid");
}
}
Shape output_shape(const std::vector<Shape> &inputs) const override {
auto shape = unary_shape(inputs, 2);
const auto &c = config_;
if (shape.rank != 2 || shape.dimensions[0] != c.input_channels) {
throw std::invalid_argument("NEON depthwise convolution input shape invalid");
}
const int64_t span = int64_t(shape.dimensions[1]) + c.padding_left + c.padding_right -
int64_t(c.dilation) * (c.kernel_size - 1) - 1;
if (span < 0 || span / c.stride >= std::numeric_limits<int>::max()) {
throw std::invalid_argument("NEON depthwise convolution output extent invalid");
}
shape.dimensions[1] = int(span / c.stride + 1);
return shape;
}
void compute(const std::vector<detail::BufferView> &inputs, float *output, const Shape &shape,
detail::Workspace &, detail::WorkerContext &worker) const noexcept override {
detail::depthwise_conv1d(inputs[0].data, weight_.data(), bias_.empty() ? nullptr : bias_.data(), output,
config_, inputs[0].shape->dimensions[1], shape.dimensions[1], worker);
}
private:
Conv1dConfig config_;
std::vector<float> weight_, bias_;
};

class LayerNormKernel final : public detail::Kernel {
public:
LayerNormKernel(std::vector<float> weight, std::vector<float> bias, float epsilon)
Expand Down Expand Up @@ -320,7 +355,8 @@ class LayerNormKernel final : public detail::Kernel {
class ActivationKernel final : public detail::Kernel {
public:
ActivationKernel(ActivationKind kind, float slope) : kind_(kind), slope_(slope) {
if ((kind != ActivationKind::SiLU && kind != ActivationKind::GELU && kind != ActivationKind::LeakyReLU) ||
if ((kind != ActivationKind::SiLU && kind != ActivationKind::GELU && kind != ActivationKind::LeakyReLU &&
kind != ActivationKind::Sigmoid) ||
!std::isfinite(slope)) {
throw std::invalid_argument("ARM activation configuration invalid");
}
Expand All @@ -347,6 +383,8 @@ class ActivationKernel final : public detail::Kernel {
detail::apply_gelu(output, begin, end);
} else if (kind_ == ActivationKind::SiLU) {
detail::apply_silu(output, begin, end);
} else if (kind_ == ActivationKind::Sigmoid) {
for (int i = begin; i < end; ++i) output[i] = 1.0F / (1.0F + std::exp(-output[i]));
} else {
detail::apply_leaky_relu_clamp(output, begin, end, slope_, -std::numeric_limits<float>::infinity());
}
Expand Down Expand Up @@ -678,6 +716,8 @@ DepthwiseConv1d::DepthwiseConv1d(int channels, int kernel, const std::vector<flo
: Operator(std::make_shared<DepthwiseConv1dKernel>(channels, kernel, weight, bias)) {}
LayerNorm::LayerNorm(const std::vector<float> &weight, const std::vector<float> &bias, float epsilon)
: Operator(std::make_shared<LayerNormKernel>(weight, bias, epsilon)) {}
DepthwiseConv1d::DepthwiseConv1d(Conv1dConfig config, const std::vector<float> &weight, const std::vector<float> &bias)
: Operator(std::make_shared<ConfiguredDepthwiseConv1dKernel>(config, weight, bias)) {}
Activation::Activation(ActivationKind kind, float slope) : Operator(std::make_shared<ActivationKernel>(kind, slope)) {}
ClampMin::ClampMin(float minimum) : Operator(std::make_shared<ClampMinKernel>(minimum)) {}
MaxPool2d::MaxPool2d(int kh, int kw, int sh, int sw) : Operator(std::make_shared<MaxPool2dKernel>(kh, kw, sh, sw)) {}
Expand Down
Loading
Loading