mcap/src/test/labels_tests.jai

134 lines
5.5 KiB
Plaintext

// Labels (modules/Labels): the tokenizer's ids for known phrases (from Hugging Face's tokenizers on
// the same tokenizer.json), unit vectors from both encoders, and the model ranking a drawn text image
// as text rather than a photo. Needs the label models beside the program (the build copies them);
// without them, the suite says so and passes.
#scope_file
checks_run := 0;
checks_failed := 0;
MAX_GROUPS :: 8;
group_names : [MAX_GROUPS] string;
group_checks : [MAX_GROUPS] int;
group_fails : [MAX_GROUPS] int;
group_count := 0;
output_builder: *String_Builder;
emit :: (format_string: string, args: ..Any) {
if output_builder print_to_builder(output_builder, format_string, ..args);
else print(format_string, ..args);
} @PrintLike
group :: (name: string) {
assert(group_count < MAX_GROUPS, "raise MAX_GROUPS");
group_names[group_count] = name;
group_count += 1;
emit("\n-- % --\n", name);
}
expect :: (condition: bool, format: string, args: ..Any, loc := #caller_location) {
checks_run += 1;
if group_count group_checks[group_count-1] += 1;
if condition return;
checks_failed += 1;
if group_count group_fails[group_count-1] += 1;
emit(" FAIL [%:%] %\n", group_names[max(group_count-1, 0)], loc.line_number, tprint(format, ..args));
}
report :: () -> bool {
emit("\n");
for 0..group_count-1 {
if group_fails[it] emit(" % -- % checks, % FAILED\n", group_names[it], group_checks[it], group_fails[it]);
else emit(" % -- % checks\n", group_names[it], group_checks[it]);
}
if checks_failed emit("\n% OF % LABELS CHECKS FAILED\n", checks_failed, checks_run);
else emit("\nALL LABELS TESTS PASSED (% checks over % groups)\n", checks_run, group_count);
return checks_failed == 0;
}
// From Hugging Face's tokenizers 0.23.2 on the same tokenizer.json, lowercased, cut and padded to 64.
Known_Tokens :: struct { text: string; ids: [] s64; }
KNOWN :: Known_Tokens.[
.{"code editor", .[2564, 10476, 1]},
.{"A screenshot of a terminal", .[235250, 13703, 576, 476, 17145, 1]},
.{"", .[1]},
];
// labels_float_to_half is a copy of the Float16 module's float_to_half_fast3 marked #no_aoc: the
// same bits for every value (here in a debug build, where the module's own is not stopped).
half_tests :: () {
group("half floats");
values: [..] float;
values.allocator = temp;
array_add(*values, ..float.[0, -0.0, 1, -1, 0.5, 65504, 65520, 1000000, -1000000, cast(float) 0.00006103515625, cast(float) 0.0000001, FLOAT32_INFINITY, -FLOAT32_INFINITY]);
x: float = 0.00001;
for 1..10_000 { array_add(*values, x, -x * 0.37); x *= 1.0027; }
differ := 0;
for values if labels_float_to_half(it) != cast(u16) float32_to_float16(it) differ += 1;
expect(differ == 0, "% of % values convert to different bits", differ, values.count);
}
labels_tests :: () {
half_tests();
folder := labels_models_folder();
if !file_exists(tprint("%/models.config", folder)) {
emit(" (skipped: no label models in %)\n", folder);
return;
}
group("engine");
engine, created, error := labels_create(folder, LABELS_MODEL);
expect(created, "the label engine starts: %", error);
if !created return;
defer labels_destroy(engine);
emit(" images on %; % dimensions\n", ifx engine.provider == .CUDA then "CUDA" else "the CPU", engine.dimensions);
group("tokenizer");
for KNOWN {
ids := tokenizer_encode(engine.tokenizer, it.text, 64);
same := ids.count == 64;
for known, i: it.ids if !same || ids[i] != known same = false;
for it.ids.count..63 if same && ids[it] != 0 same = false; // <pad>
expect(same, "\"%\" tokenizes as Hugging Face's does: % (got %)", it.text, it.ids, array_view(ids, 0, it.ids.count + 1));
}
group("vectors");
prompts := string.["a screenshot of text on a white background", "a photo of a cat", "a photo of a mountain landscape"];
texts, text_error := labels_embed_texts(engine, prompts,, temp);
expect(!text_error && texts.count == prompts.count * engine.dimensions, "the prompts' vectors are made (%)", text_error);
if text_error || texts.count != prompts.count * engine.dimensions return;
for 0..prompts.count - 1 expect(abs(vector_length(array_view(texts, it * engine.dimensions, engine.dimensions)) - 1) < 0.001, "the vector of \"%\" is a unit vector", prompts[it]);
drawn, drawn_ok := test_drawn_text();
if !drawn_ok {
emit(" (the text image is drawn with GDI, on Windows only: skipped)\n");
return;
}
image := Labels_Image.{drawn.pixels, drawn.width, drawn.height, drawn.stride};
vector, image_error := labels_embed_images(engine, .[image],, temp);
expect(!image_error && vector.count == engine.dimensions, "the image's vector is made (%)", image_error);
if image_error || vector.count != engine.dimensions return;
expect(abs(vector_length(vector) - 1) < 0.001, "the image's vector is a unit vector");
scores: [3] float;
for 0..2 scores[it] = labels_score(engine, vector.data, texts.data + it * engine.dimensions);
expect(scores[0] > scores[1] && scores[0] > scores[2], "drawn text scores as text (%) above a cat (%) and a landscape (%)", scores[0], scores[1], scores[2]);
}
vector_length :: (v: [] float) -> float {
sum: float;
for v sum += it * it;
return sqrt(sum);
}
#scope_export
run_labels_tests :: (builder: *String_Builder = null) -> bool {
output_builder = builder;
emit("=== labels test suite ===\n");
push_allocator(context.default_allocator);
labels_tests();
return report();
}