Skip to content
Closed
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
37 changes: 25 additions & 12 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,31 @@ option(CED_GGML_METAL "Forward GGML_METAL" OFF)
option(CED_GGML_VULKAN "Forward GGML_VULKAN" OFF)
option(CED_GGML_HIP "Forward GGML_HIP (ROCm)" OFF)

set(GGML_CUDA ${CED_GGML_CUDA} CACHE BOOL "" FORCE)
set(GGML_METAL ${CED_GGML_METAL} CACHE BOOL "" FORCE)
set(GGML_VULKAN ${CED_GGML_VULKAN} CACHE BOOL "" FORCE)
set(GGML_HIP ${CED_GGML_HIP} CACHE BOOL "" FORCE)
set(CED_TOP_LEVEL OFF)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
set(CED_TOP_LEVEL ON)
endif()

option(CED_EXTERNAL_DR_WAV "Use the embedding project's dr_wav implementation" OFF)

if(CED_TOP_LEVEL)
set(GGML_CUDA ${CED_GGML_CUDA} CACHE BOOL "" FORCE)
set(GGML_METAL ${CED_GGML_METAL} CACHE BOOL "" FORCE)
set(GGML_VULKAN ${CED_GGML_VULKAN} CACHE BOOL "" FORCE)
set(GGML_HIP ${CED_GGML_HIP} CACHE BOOL "" FORCE)

# Free perf wins (per parakeet.cpp / rt-detr.cpp): -march=native + tinyBLAS.
if(NOT DEFINED GGML_NATIVE)
set(GGML_NATIVE ON CACHE BOOL "ggml: optimize for the current system" FORCE)
# Free perf wins (per parakeet.cpp / rt-detr.cpp): -march=native + tinyBLAS.
if(NOT DEFINED GGML_NATIVE)
set(GGML_NATIVE ON CACHE BOOL "ggml: optimize for the current system" FORCE)
endif()
if(NOT DEFINED GGML_LLAMAFILE)
set(GGML_LLAMAFILE ON CACHE BOOL "ggml: use LLAMAFILE (tinyBLAS SGEMM)" FORCE)
endif()
endif()
if(NOT DEFINED GGML_LLAMAFILE)
set(GGML_LLAMAFILE ON CACHE BOOL "ggml: use LLAMAFILE (tinyBLAS SGEMM)" FORCE)
if(NOT TARGET ggml)
add_subdirectory(third_party/ggml)
endif()

add_subdirectory(third_party/ggml)

set(CED_SRC
src/model_loader.cpp
src/ced_runner.cpp
Expand All @@ -45,7 +55,10 @@ if(CED_SHARED)
else()
add_library(ced STATIC ${CED_SRC})
endif()
target_include_directories(ced PUBLIC include PRIVATE src ${CMAKE_SOURCE_DIR}/third_party)
target_include_directories(ced PUBLIC include PRIVATE src ${CMAKE_CURRENT_SOURCE_DIR}/third_party)
if(CED_EXTERNAL_DR_WAV)
target_compile_definitions(ced PRIVATE CED_EXTERNAL_DR_WAV)
endif()
target_compile_definitions(ced PUBLIC $<$<CXX_COMPILER_ID:MSVC>:_USE_MATH_DEFINES>)
target_link_libraries(ced PUBLIC ggml)

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,12 @@ cmake --build build-shared -j
| `CED_GGML_METAL` | OFF | Forward GGML_METAL to the submodule |
| `CED_GGML_VULKAN` | OFF | Forward GGML_VULKAN to the submodule |
| `CED_GGML_HIP` | OFF | Forward GGML_HIP (ROCm) to the submodule |
| `CED_EXTERNAL_DR_WAV` | OFF | Do not compile dr_wav; the embedding project provides it |

To build for a GPU backend, forward its flag, e.g. `cmake -B build -DCED_GGML_METAL=ON`.

When ced.cpp is added with `add_subdirectory` by a project that already has a `ggml` target, it reuses that ggml.

At runtime a GPU build picks the first GPU it finds and falls back to CPU. Set `CED_DEVICE` to choose: `CED_DEVICE=cpu` forces the CPU, and a device name such as `CUDA0`, `Vulkan0` or `MTL0` selects that device (`ced-cli info` prints the one in use). The weights are uploaded to the device once at load. If the device has no kernel for an op, that graph runs through ggml's scheduler with a CPU fallback.

---
Expand Down Expand Up @@ -158,7 +161,7 @@ if (json) { printf("%s\n", json); ced_capi_free_string(json); }
ced_capi_free(ctx);
```

The per-PCM entry points take an arbitrary mono window, so a realtime consumer can call them on a sliding buffer for live recognition. There is also a struct-array variant (`ced_capi_classify_pcm`) and a WAV-path variant (`ced_capi_classify_path_json`). See `include/ced_capi.h` for the full API.
The per-PCM entry points take an arbitrary mono window, so a realtime consumer can call them on a sliding buffer for live recognition. There is also a struct-array variant (`ced_capi_classify_pcm`), a WAV-path variant (`ced_capi_classify_path_json`), and `ced_capi_classify_pcm_probs`, which writes every class score in class-index order (no sorting, no allocation) for callers that want the raw distribution. See `include/ced_capi.h` for the full API.

---

Expand Down
10 changes: 8 additions & 2 deletions examples/cli/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
add_executable(ced-cli main.cpp)
if(CED_EXTERNAL_DR_WAV)
# libced.a was built without a dr_wav implementation (the embedding host
# supplies one for its own binary), but ced-cli is a separate executable
# that still needs the real symbols.
target_sources(ced-cli PRIVATE dr_wav_impl.cpp)
endif()
target_link_libraries(ced-cli PRIVATE ced)
target_include_directories(ced-cli PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/third_party)
${PROJECT_SOURCE_DIR}/src
${PROJECT_SOURCE_DIR}/third_party)
8 changes: 8 additions & 0 deletions examples/cli/dr_wav_impl.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// ced-cli is its own executable, never the embedding host's binary, so it
// needs a real dr_wav implementation even when the `ced` library is built
// with CED_EXTERNAL_DR_WAV (which only promises that *the host's* binary
// supplies one). Compiled into ced-cli only when that option is set; the
// non-embedded build already gets the implementation from src/audio_io.cpp
// via libced.a.
#define DR_WAV_IMPLEMENTATION
#include "dr_wav.h"
7 changes: 7 additions & 0 deletions include/ced_capi.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ typedef struct {
int ced_capi_classify_pcm(ced_ctx* ctx, const float* samples, int n_samples, int sample_rate,
ced_tag* out, int max_tags);

// All class scores for mono float PCM, in class-index order (no sorting, no
// allocation). Writes min(n_out, ced_capi_num_classes(ctx)) floats into `out`
// and returns that count, or -1 on error (see ced_capi_last_error). Resamples
// like ced_capi_classify_pcm when `sample_rate` differs from the model rate.
int ced_capi_classify_pcm_probs(ced_ctx* ctx, const float* samples, int n_samples,
int sample_rate, float* out, int n_out);

// Free a string returned by a *_json function. Safe on NULL.
void ced_capi_free_string(char* s);

Expand Down
4 changes: 3 additions & 1 deletion src/audio_io.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#define DR_WAV_IMPLEMENTATION
#ifndef CED_EXTERNAL_DR_WAV
#define DR_WAV_IMPLEMENTATION // an embedding project may provide it instead
#endif
#include "dr_wav.h"

#include "audio_io.hpp"
Expand Down
11 changes: 11 additions & 0 deletions src/ced_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ int ced_capi_classify_pcm(ced_ctx* ctx, const float* samples, int n_samples, int
return n;
}

int ced_capi_classify_pcm_probs(ced_ctx* ctx, const float* samples, int n_samples,
int sample_rate, float* out, int n_out) {
auto* c = reinterpret_cast<CedContext*>(ctx);
if (!c || !samples || !out || n_out <= 0) return -1;
std::vector<float> probs;
if (!do_classify(c, samples, n_samples, sample_rate, probs)) return -1;
const int n = std::min(n_out, (int)probs.size());
std::memcpy(out, probs.data(), (size_t)n * sizeof(float));
return n;
}

void ced_capi_free_string(char* s) { std::free(s); }

} // extern "C"
18 changes: 9 additions & 9 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
set(CED_MODEL_GGUF "${CMAKE_SOURCE_DIR}/models/ced-base-f32.gguf")
set(CED_BASELINE_GGUF "${CMAKE_SOURCE_DIR}/tests/fixtures/ced-base.baseline.gguf")
set(CED_MODEL_GGUF "${PROJECT_SOURCE_DIR}/models/ced-base-f32.gguf")
set(CED_BASELINE_GGUF "${PROJECT_SOURCE_DIR}/tests/fixtures/ced-base.baseline.gguf")

foreach(t blocks frontend quant capi)
add_executable(test_${t} test_${t}.cpp)
target_link_libraries(test_${t} PRIVATE ced)
target_include_directories(test_${t} PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/third_party/ggml/include)
${PROJECT_SOURCE_DIR}/src
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/third_party/ggml/include)
endforeach()
add_test(NAME capi COMMAND test_capi "${CED_MODEL_GGUF}" "${CED_BASELINE_GGUF}")

Expand All @@ -20,9 +20,9 @@ add_test(NAME frontend COMMAND test_frontend "${CED_MODEL_GGUF}" "${CED_BASELINE
# python scripts/gen_ced_baseline.py --model mispeech/ced-base \
# --out tests/fixtures/ced-base-short.baseline.gguf --n-samples 64000
add_test(NAME frontend_short COMMAND test_frontend "${CED_MODEL_GGUF}"
"${CMAKE_SOURCE_DIR}/tests/fixtures/ced-base-short.baseline.gguf")
"${PROJECT_SOURCE_DIR}/tests/fixtures/ced-base-short.baseline.gguf")

# End-to-end quantization parity (probs close + top-5 labels unchanged).
add_test(NAME e2e_f32 COMMAND test_quant "${CMAKE_SOURCE_DIR}/models/ced-base-f32.gguf" "${CED_BASELINE_GGUF}" 1e-3)
add_test(NAME e2e_f16 COMMAND test_quant "${CMAKE_SOURCE_DIR}/models/ced-base-f16.gguf" "${CED_BASELINE_GGUF}" 5e-3)
add_test(NAME e2e_q8 COMMAND test_quant "${CMAKE_SOURCE_DIR}/models/ced-base-q8_0.gguf" "${CED_BASELINE_GGUF}" 3e-2)
add_test(NAME e2e_f32 COMMAND test_quant "${PROJECT_SOURCE_DIR}/models/ced-base-f32.gguf" "${CED_BASELINE_GGUF}" 1e-3)
add_test(NAME e2e_f16 COMMAND test_quant "${PROJECT_SOURCE_DIR}/models/ced-base-f16.gguf" "${CED_BASELINE_GGUF}" 5e-3)
add_test(NAME e2e_q8 COMMAND test_quant "${PROJECT_SOURCE_DIR}/models/ced-base-q8_0.gguf" "${CED_BASELINE_GGUF}" 3e-2)
24 changes: 24 additions & 0 deletions tests/test_capi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,30 @@ int main(int argc, char** argv) {
ced_capi_free_string(json);
}

// all-scores path: same numbers as the sorted top-k path, in index order
{
const int nc = ced_capi_num_classes(ctx);
std::vector<float> all(nc, -1.0f);
int w = ced_capi_classify_pcm_probs(ctx, wav.data(), (int)wav.size(), 16000,
all.data(), nc);
ok &= (w == nc);
std::vector<ced_tag> sorted(nc);
int ns = ced_capi_classify_pcm(ctx, wav.data(), (int)wav.size(), 16000,
sorted.data(), nc);
ok &= (ns == nc);
for (int i = 0; i < ns; ++i) ok &= (all[sorted[i].index] == sorted[i].score);
// short buffer: only the first n_out classes
float three[3];
ok &= (ced_capi_classify_pcm_probs(ctx, wav.data(), (int)wav.size(), 16000,
three, 3) == 3);
ok &= (three[0] == all[0] && three[2] == all[2]);
// errors
ok &= (ced_capi_classify_pcm_probs(ctx, nullptr, 10, 16000, three, 3) == -1);
ok &= (ced_capi_classify_pcm_probs(ctx, wav.data(), (int)wav.size(), 16000,
nullptr, 3) == -1);
std::fprintf(stderr, "classify_pcm_probs: %s\n", ok ? "ok" : "FAIL");
}

ced_capi_free(ctx);
std::fprintf(stderr, "%s\n", ok ? "PASS" : "FAIL");
return ok ? 0 : 1;
Expand Down
Loading