mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-25 15:37:25 -05:00
which resulted in different greedy transcripts for 4.5% of English and 6.5% of Japanese
test utterances. In Japanese, some differences changed entire words.
This change:
* uses `log(x + 2^-24)` instead of clamping to the log floor
* uses a symmetric Hann window, equivalent to `torch.hann_window(periodic=False)`
* adds the normalization epsilon to the standard deviation instead of inside the square root
Only the `lfm2a` preprocessor opts into these behaviors. Other audio preprocessors are unchanged.
Tested on top of 84e76d8 using `llama-server` with CUDA and `temperature=0`, compared against
http://github.com/Liquid4All/liquid-audio fp32.
Test set:
* 200 LibriSpeech `test-clean` utterances (EN)
* 200 Common Voice `ja` test utterances (JP)
* identical 16 kHz audio passed to both implementations
| Greedy transcript identical to `liquid-audio` | Without fix | With fix |
| --------------------------------------------- | ----------: | ----------: |
| EN F16 | 191/200 | 200/200 |
| JP F32 | 187/200 | 200/200 |
| JP F16 | 187/200 | 199/200 |
The remaining JP F16 difference is a comma and matches the reference implementation's own bf16
output.
Mel relative L2 error versus `liquid-audio`:
* EN: 3.2% -> ~2e-6 median
* JP: 3.9% -> ~2e-6 median
209 lines
6.6 KiB
C++
209 lines
6.6 KiB
C++
#include "testing.h"
|
|
|
|
#include "mtmd-audio.h"
|
|
#include "mtmd-image.h"
|
|
#include "mtmd-internal.h"
|
|
|
|
#include <cmath>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
// this test file contains:
|
|
// 1. test cases for mtmd helpers
|
|
// 2. test cases for internal mtmd components
|
|
// internal headers can be included here
|
|
|
|
struct test_registry {
|
|
using fn_t = void (*)(testing &);
|
|
|
|
struct entry {
|
|
std::string name;
|
|
fn_t fn;
|
|
};
|
|
|
|
static std::vector<entry> & all() {
|
|
static std::vector<entry> entries;
|
|
return entries;
|
|
}
|
|
|
|
test_registry(const char * name, fn_t fn) {
|
|
all().push_back({ name, fn });
|
|
}
|
|
};
|
|
|
|
#define MAKE_TEST(name) \
|
|
static void name(testing & t); \
|
|
static const test_registry test_registry_ ## name(#name, &name); \
|
|
static void name(testing & t)
|
|
|
|
|
|
//
|
|
// mtmd_image
|
|
//
|
|
|
|
MAKE_TEST(test_image_preprocessor_lfm2) {
|
|
clip_hparams hparams;
|
|
hparams.patch_size = 16;
|
|
hparams.n_merge = 2;
|
|
hparams.set_limit_image_tokens(64, 256);
|
|
|
|
// { image size, expected tiling }
|
|
const std::vector<std::pair<clip_image_size, bool>> cases = {
|
|
{ { 704, 704 }, false },
|
|
// 720 / (patch_size * n_merge) is exactly 22.5, so this only matches HF
|
|
// if round_by_factor rounds half to even (22) instead of away from zero (23)
|
|
{ { 720, 720 }, false },
|
|
{ { 736, 736 }, true },
|
|
{ { 1024, 977 }, true },
|
|
{ { 1056, 384 }, false },
|
|
};
|
|
|
|
for (const auto & [size, expected] : cases) {
|
|
const bool actual = mtmd_image_preprocessor_lfm2::should_tile(hparams, size);
|
|
|
|
t.assert_equal(
|
|
"tiling for " + std::to_string(size.width) + "x" + std::to_string(size.height),
|
|
std::string(expected ? "tiled" : "single"),
|
|
std::string(actual ? "tiled" : "single"));
|
|
}
|
|
}
|
|
|
|
//
|
|
// mtmd temporal merge
|
|
//
|
|
|
|
MAKE_TEST(test_temporal_merge_grouping) {
|
|
std::vector<mtmd::bitmap_ptr> pool; // keeps the bitmaps alive until the end of the test
|
|
|
|
// spec chars:
|
|
// v = video frame, w = video frame of another size, a = audio, i = plain image, t = text
|
|
auto make_parts = [&pool](const std::string & spec) {
|
|
std::vector<mtmd_internal_part> parts;
|
|
for (char c : spec) {
|
|
if (c == 't') {
|
|
parts.push_back({ "hello", nullptr });
|
|
continue;
|
|
}
|
|
mtmd_bitmap * bm = nullptr;
|
|
switch (c) {
|
|
case 'v': bm = mtmd_bitmap_init(100, 100, nullptr); break;
|
|
case 'w': bm = mtmd_bitmap_init(200, 200, nullptr); break;
|
|
case 'a': bm = mtmd_bitmap_init_from_audio(100, nullptr); break;
|
|
case 'i': bm = mtmd_bitmap_init(100, 100, nullptr); break;
|
|
default: throw std::runtime_error(std::string("unknown spec char: ") + c);
|
|
}
|
|
mtmd_bitmap_set_mergeable(bm, c != 'i');
|
|
pool.emplace_back(bm);
|
|
parts.push_back({ "", bm });
|
|
}
|
|
return parts;
|
|
};
|
|
|
|
// { parts, n_merge, expected size of each group }
|
|
const std::vector<std::tuple<std::string, int, std::string>> cases = {
|
|
{ "vv", 2, "2" },
|
|
{ "vvv", 2, "21" },
|
|
{ "vvvv", 2, "22" },
|
|
{ "vvi", 2, "21" },
|
|
{ "tvvt", 2, "2" },
|
|
{ "vtv", 2, "11" }, // text in between breaks the merge
|
|
{ "vw", 2, "11" }, // different sizes cannot be merged
|
|
{ "aa", 2, "11" }, // audio is never merged
|
|
{ "ii", 2, "11" }, // two unrelated images must stay separated
|
|
{ "iv", 2, "11" },
|
|
{ "vi", 2, "11" },
|
|
{ "vv", 1, "11" }, // model without temporal merge
|
|
};
|
|
|
|
for (const auto & [spec, n_merge, expected] : cases) {
|
|
auto parts = make_parts(spec);
|
|
auto groups = mtmd_group_mergeable_bitmaps(parts, n_merge);
|
|
|
|
std::string actual;
|
|
for (const auto & group : groups) {
|
|
actual += std::to_string(group.size());
|
|
}
|
|
|
|
const std::string name = "\"" + spec + "\" with n_merge=" + std::to_string(n_merge);
|
|
t.assert_equal("groups for " + name, expected, actual);
|
|
|
|
size_t n_bitmap_parts = 0;
|
|
for (const auto & p : parts) {
|
|
n_bitmap_parts += p.bitmap != nullptr ? 1 : 0;
|
|
}
|
|
t.assert_equal("remaining bitmap parts for " + name, groups.size(), n_bitmap_parts);
|
|
}
|
|
}
|
|
|
|
//
|
|
// mtmd_audio
|
|
//
|
|
|
|
MAKE_TEST(test_audio_preprocessor_conformer) {
|
|
clip_hparams hparams;
|
|
hparams.n_mel_bins = 128;
|
|
hparams.audio_sample_rate = 16000;
|
|
hparams.audio_n_fft = 512;
|
|
hparams.audio_window_len = 400;
|
|
hparams.audio_hop_len = 160;
|
|
|
|
// 0.4 s of tones, 0.2 s of silence, 0.4 s of a quiet tone
|
|
const double pi = 3.14159265358979323846;
|
|
const int sr = hparams.audio_sample_rate;
|
|
std::vector<float> samples(sr, 0.0f);
|
|
for (int i = 0; i < sr; i++) {
|
|
const double ts = (double) i / sr;
|
|
if (i < 0.4 * sr) {
|
|
samples[i] = (float) (0.3 * std::sin(2 * pi * 300 * ts) + 0.2 * std::sin(2 * pi * 1200 * ts) + 0.1 * std::sin(2 * pi * 3500 * ts));
|
|
} else if (i >= 0.6 * sr) {
|
|
samples[i] = (float) (1e-3 * std::sin(2 * pi * 800 * ts));
|
|
}
|
|
}
|
|
|
|
mtmd_audio_preprocessor_conformer preproc(hparams);
|
|
preproc.initialize();
|
|
std::vector<mtmd_audio_mel> mels;
|
|
if (!t.assert_true("preprocess", preproc.preprocess(samples.data(), samples.size(), mels))) {
|
|
return;
|
|
}
|
|
const mtmd_audio_mel & mel = mels[0];
|
|
t.assert_equal("n_len", (int64_t) 101, mel.n_len);
|
|
|
|
// reference values from NeMo AudioToMelSpectrogramPreprocessor (liquid-audio 1.3.0)
|
|
// { mel bin, frame, value }
|
|
const std::vector<std::tuple<int, int, float>> cases = {
|
|
{ 8, 0, 3.6104f }, // tones
|
|
{ 24, 50, -0.6371f }, // silence
|
|
{ 24, 60, -0.0476f }, // silence -> quiet tone
|
|
{ 100, 20, -0.0913f }, // tones, high bin
|
|
};
|
|
for (const auto & [bin, frame, expected] : cases) {
|
|
const float actual = mel.data[bin * mel.n_len + frame];
|
|
t.assert_true("mel[" + std::to_string(bin) + "][" + std::to_string(frame) + "] = " + std::to_string(actual) + ", expected " + std::to_string(expected), std::fabs(actual - expected) < 1e-3f);
|
|
}
|
|
}
|
|
|
|
//
|
|
// main
|
|
//
|
|
|
|
int main(int argc, char ** argv) {
|
|
testing t(std::cout);
|
|
t.verbose = true;
|
|
|
|
// usage: test-mtmd-impl [filter_regex]
|
|
for (int i = 1; i < argc; i++) {
|
|
t.set_filter(argv[i]);
|
|
}
|
|
|
|
for (const auto & e : test_registry::all()) {
|
|
t.test(e.name, e.fn);
|
|
}
|
|
|
|
return t.summary();
|
|
}
|