720 lines
34 KiB
Plaintext
720 lines
34 KiB
Plaintext
// gpu-bench: times every model mcap runs on the GPU (each OCR model, the label model's image encoder,
|
|
// segmentation) over a fixed corpus, so machines can be compared. The corpus (gpu-bench-corpus beside
|
|
// this program, from tools/gpu_bench/corpus) is checked file by file against corpus.txt's SHA-256s
|
|
// before anything runs, and each model's files and models.config entry are fingerprinted, so a
|
|
// comparison of two results is known to be like for like. mcap's GPU Benchmark window runs this and
|
|
// lays the results side by side.
|
|
//
|
|
// gpu-bench [-only ocr,labels,segment] [-models a,b] [-repeats N] [-warmup N] [-cpu | -cpu-only]
|
|
// [-corpus FOLDER] [-out FOLDER] [-note TEXT] [-progress]
|
|
//
|
|
// -only the kinds of model to run (default all)
|
|
// -models the models to run, by their models.config names (default every one)
|
|
// -repeats timed passes over the corpus; the median is reported (default 3)
|
|
// -warmup untimed passes first, so every image size has been seen (default 1)
|
|
// -cpu the CPU too, after the GPU (slow: minutes for segmentation); also times the label
|
|
// model's text encoder, which always runs on the CPU, as a like: search uses it
|
|
// -cpu-only the CPU only
|
|
// -corpus the corpus folder (default gpu-bench-corpus beside this program)
|
|
// -out where the results file goes (default gpu-bench-results beside this program)
|
|
// -note a few words kept with the results, such as "plugged in, max performance"
|
|
// -progress also print @progress lines, for the GPU Benchmark window
|
|
// -ocr-workers N OCR's worker threads on the GPU, in place of Ocr_Options' (for investigating memory)
|
|
// -ort-log LEVEL onnxruntime's console log: verbose, info (each GPU arena extension), warning (default)
|
|
//
|
|
// Build: jai first.jai - release (it is built with mcap).
|
|
|
|
USAGE :: "Usage: gpu-bench [-only ocr,labels,segment] [-models a,b] [-repeats N] [-warmup N] [-cpu | -cpu-only] [-corpus FOLDER] [-out FOLDER] [-note TEXT] [-progress] [-ocr-workers N] [-ort-log LEVEL]\n";
|
|
|
|
options: struct {
|
|
only: [..] string;
|
|
models: [..] string;
|
|
repeats := 3;
|
|
warmup := 1;
|
|
gpu := true;
|
|
cpu := false;
|
|
progress := false;
|
|
ocr_workers: s32; // 0: Ocr_Options' default
|
|
corpus_folder, out_folder, note: string;
|
|
};
|
|
|
|
main :: () {
|
|
runtime_support_headless = true; // a command-line tool: a failure is reported on the console and exits with 1, no message box
|
|
args := get_command_line_arguments();
|
|
exe_folder := path_strip_filename(get_path_of_running_executable());
|
|
options.corpus_folder = sprint("%gpu-bench-corpus", exe_folder);
|
|
options.out_folder = sprint("%gpu-bench-results", exe_folder);
|
|
i := 1;
|
|
while i < args.count {
|
|
arg := args[i];
|
|
value := ifx i + 1 < args.count then args[i + 1] else "";
|
|
if arg == {
|
|
case "-only"; for split(value, ",") array_add(*options.only, trim(it)); i += 2;
|
|
case "-models"; for split(value, ",") array_add(*options.models, trim(it)); i += 2;
|
|
case "-repeats"; options.repeats = max(1, string_to_int(value)); i += 2;
|
|
case "-warmup"; options.warmup = max(0, string_to_int(value)); i += 2;
|
|
case "-cpu"; options.cpu = true; i += 1;
|
|
case "-cpu-only"; options.cpu = true; options.gpu = false; i += 1;
|
|
case "-corpus"; options.corpus_folder = value; i += 2;
|
|
case "-out"; options.out_folder = value; i += 2;
|
|
case "-note"; options.note = value; i += 2;
|
|
case "-progress"; options.progress = true; i += 1;
|
|
case "-ocr-workers"; options.ocr_workers = cast(s32) max(1, string_to_int(value)); i += 2;
|
|
case "-ort-log";
|
|
if value == {
|
|
case "verbose"; ort.log_level = .ORT_LOGGING_LEVEL_VERBOSE;
|
|
case "info"; ort.log_level = .ORT_LOGGING_LEVEL_INFO;
|
|
case "warning"; ort.log_level = .ORT_LOGGING_LEVEL_WARNING;
|
|
case; print("-ort-log takes verbose, info or warning.\n"); exit(2);
|
|
}
|
|
i += 2;
|
|
case "-help"; print(USAGE); exit(0);
|
|
case; print("Unknown option %.\n%", arg, USAGE); exit(2);
|
|
}
|
|
}
|
|
make_directory_if_it_does_not_exist(options.out_folder, recursive = true);
|
|
started_at := current_time_consensus();
|
|
|
|
// The machine.
|
|
info := system_info();
|
|
ort_ok, ort_error := ort_load();
|
|
print("gpu-bench, mcap % (%, %)\n", VERSION, GIT_HASH, GIT_BRANCH);
|
|
print("machine: %, %\n", info.machine, info.os);
|
|
print("cpu: % (% threads), % GB of memory\n", info.cpu, info.logical_cores, FF(info.ram_gb, 1));
|
|
if info.gpu print("gpu: %, % GB, driver % (CUDA %), power limit % W, max clocks % / % MHz\n", info.gpu, FF(info.vram_gb, 1), info.driver, info.cuda_driver, FF(info.power_limit_w, 0), info.max_sm_clock_mhz, info.max_mem_clock_mhz);
|
|
else print("gpu: none (%)\n", info.gpu_error);
|
|
if info.power_source print("power: %\n", info.power_source);
|
|
if !ort_ok { print("%\n", ort_error); exit(1); }
|
|
print("onnxruntime: % (%)\n", ort.version, ort.library_path);
|
|
|
|
// The corpus: every file as corpus.txt pins it, or nothing runs.
|
|
progress("corpus", "-", "checking", 0, 0);
|
|
corpus, corpus_error := corpus_load(options.corpus_folder);
|
|
if corpus_error { print("\nThe corpus cannot be used: %\n", corpus_error); exit(1); }
|
|
print("corpus: % images, % megapixels, fingerprint % (decoded in % s)\n\n", corpus.images.count, FF(corpus.megapixels, 1), short(corpus.fingerprint), FF(corpus.decode_s, 2));
|
|
|
|
// The models, and what each is made of.
|
|
tasks := list_tasks(exe_folder);
|
|
if !tasks.count { print("No models to run: none matched -only and -models, or none are beside this program (run jai first.jai).\n"); exit(1); }
|
|
hash_cache_load();
|
|
for * tasks fingerprint_task(it);
|
|
hash_cache_save();
|
|
|
|
runs: [..] Run;
|
|
for * task: tasks {
|
|
providers: [..] string;
|
|
providers.allocator = temp;
|
|
if task.kind == .LABELS_TEXT { if options.cpu array_add(*providers, "CPU"); }
|
|
else {
|
|
if options.gpu array_add(*providers, "CUDA");
|
|
if options.cpu array_add(*providers, "CPU");
|
|
}
|
|
for provider: providers {
|
|
run := run_task(task, provider == "CUDA", *corpus);
|
|
array_add(*runs, run);
|
|
if !run.ok { print("== % on %: %\n\n", task.name, provider, run.error); continue; }
|
|
print_run(task, run);
|
|
}
|
|
}
|
|
|
|
cudart, cudnn := cuda_library_versions();
|
|
path := write_results(info, started_at, *corpus, tasks, runs, cudart, cudnn);
|
|
print("Results: %\n", path);
|
|
if options.progress print("@results %\n", path);
|
|
}
|
|
|
|
Task_Kind :: enum { OCR; LABELS; LABELS_TEXT; SEGMENT; }
|
|
|
|
Task :: struct {
|
|
kind: Task_Kind;
|
|
model: string; // as models.config names it
|
|
name: string; // kind/model: ocr/ppocr-v5-mobile
|
|
models_folder: string;
|
|
files: [..] string; // the model's files, in models_folder
|
|
config: string; // its models.config entry, as compared
|
|
fingerprint: string; // of files and config
|
|
pinned: string; // "yes" when every file is one a fetch script pins, "no" when one differs, "-" when none are pinned
|
|
}
|
|
|
|
Run :: struct {
|
|
task: *Task;
|
|
ok: bool;
|
|
error: string;
|
|
provider: string; // what it really ran on: a GPU run can fall back to the CPU
|
|
items: s64; // per pass
|
|
unit: string; // image or phrase
|
|
megapixels: float64; // per pass
|
|
load_s, first_s: float64;
|
|
pass_s: [..] float64;
|
|
output: string; // what the models found, in words without spaces: lines:123,chars:4567
|
|
digest: string; // of what they found, to see whether two machines' models agree
|
|
stages: string; // where the time went, per item
|
|
batch: s64;
|
|
gpu: Gpu_Stats;
|
|
budget: string; // OCR on the GPU: what its memory budget measured (modules/OCR/budget.jai), or ""
|
|
trace: [..] string; // the GPU's memory above the baseline, MB, after each step: load:812 first:1020 warm1:... pass1:.../peak...
|
|
explanation: string; // why a GPU run is on the CPU
|
|
}
|
|
|
|
Gpu_Stats :: struct {
|
|
sampled: bool;
|
|
baseline_mb, peak_mb: float64; // device memory before the engine, and the most during the timed passes
|
|
pass_peak_mb: float64; // the most during the current timed pass
|
|
util_sum, power_sum, clock_sum: float64;
|
|
temperature_max: float64;
|
|
samples: s64;
|
|
}
|
|
|
|
// The corpus, decoded.
|
|
Corpus :: struct {
|
|
fingerprint: string;
|
|
images: [..] Corpus_Image;
|
|
megapixels: float64;
|
|
decode_s: float64;
|
|
}
|
|
|
|
Corpus_Image :: struct { name: string; pixels: *u8; width, height: s32; }
|
|
|
|
// Every file corpus.txt names, checked against its hash; the error says what is wrong.
|
|
corpus_load :: (folder: string) -> Corpus, error: string {
|
|
corpus: Corpus;
|
|
manifest_path := tprint("%/corpus.txt", folder);
|
|
manifest, ok := read_entire_file(manifest_path, log_errors = false);
|
|
if !ok return corpus, tprint("% cannot be read. The build copies it, with the images, from tools/gpu_bench.", manifest_path);
|
|
fingerprint: Sha256;
|
|
bad: [..] string;
|
|
bad.allocator = temp;
|
|
start := current_time_monotonic();
|
|
for raw: split(manifest, "\n") {
|
|
line := trim(raw);
|
|
if !line.count || line[0] == #char "#" continue;
|
|
space := find_index_from_left(line, #char " ");
|
|
if space < 0 continue;
|
|
expected := slice(line, 0, space);
|
|
name := trim(advance(line, space));
|
|
path := tprint("%/%", folder, name);
|
|
data, read_ok := read_entire_file(path, log_errors = false);
|
|
if !read_ok { array_add(*bad, tprint("% is missing", name)); continue; }
|
|
hash := sha256_of_string(data,, temp);
|
|
if hash != expected { array_add(*bad, tprint("% is not the file corpus.txt pins (SHA-256 %)", name, hash)); free(data); continue; }
|
|
sha256_update(*fingerprint, tprint("% %\n", name, hash));
|
|
progress("corpus", "-", "decoding", corpus.images.count + 1, 0);
|
|
image, decoded, decode_error := image_decode(cast([] u8) data);
|
|
free(data);
|
|
if !decoded { array_add(*bad, tprint("% cannot be decoded (%)", name, decode_error)); continue; }
|
|
array_add(*corpus.images, .{copy_string(name), image.pixels, image.width, image.height});
|
|
corpus.megapixels += cast(float64) image.width * image.height / 1_000_000;
|
|
reset_temporary_storage();
|
|
}
|
|
corpus.decode_s = seconds_since(start);
|
|
if bad.count return corpus, join(..bad, "; ");
|
|
if !corpus.images.count return corpus, tprint("% names no images.", manifest_path);
|
|
corpus.fingerprint = sha256_finish(*fingerprint);
|
|
return corpus, "";
|
|
}
|
|
|
|
// The models beside this program, as -only and -models choose.
|
|
list_tasks :: (exe_folder: string) -> [..] Task {
|
|
tasks: [..] Task;
|
|
add :: (tasks: *[..] Task, kind: Task_Kind, model: string, folder: string, files: .. string) {
|
|
prefix := ifx kind == .OCR then "ocr" else ifx kind == .LABELS then "labels" else ifx kind == .LABELS_TEXT then "labels-text" else "segment";
|
|
if options.only.count && !array_find(options.only, ifx kind == .LABELS_TEXT then "labels" else prefix) return;
|
|
if options.models.count && !array_find(options.models, model) return;
|
|
task := array_add(tasks);
|
|
task.* = .{kind = kind, model = model, name = sprint("%/%", prefix, model), models_folder = folder};
|
|
for files if it array_add(*task.files, it);
|
|
task.config = config_entry(tprint("%/models.config", folder), model);
|
|
}
|
|
ocr_folder := sprint("%ocr-models", exe_folder);
|
|
ocr_models, ocr_error := ocr_list_models(ocr_folder);
|
|
if ocr_error print("OCR: %\n", ocr_error);
|
|
for ocr_models add(*tasks, .OCR, it.name, ocr_folder, it.detector, it.recognizer, it.dictionary, it.textline_ori);
|
|
labels_folder := sprint("%label-models", exe_folder);
|
|
labels_models, labels_error := labels_list_models(labels_folder);
|
|
if labels_error print("Labels: %\n", labels_error);
|
|
for labels_models add(*tasks, .LABELS, it.name, labels_folder, it.vision, it.text, it.tokenizer);
|
|
for labels_models add(*tasks, .LABELS_TEXT, it.name, labels_folder, it.vision, it.text, it.tokenizer);
|
|
segment_folder := sprint("%segment-models", exe_folder);
|
|
segment_models, segment_error := segment_list_models(segment_folder);
|
|
if segment_error print("Segmentation: %\n", segment_error);
|
|
for segment_models add(*tasks, .SEGMENT, it.name, segment_folder, it.model, it.classes);
|
|
return tasks;
|
|
}
|
|
|
|
// A model's [model NAME] entry, comments and spacing taken out, so only a change that matters
|
|
// changes the fingerprint.
|
|
config_entry :: (path: string, model: string) -> string {
|
|
text, ok := read_entire_file(path, log_errors = false,, temp);
|
|
if !ok return "";
|
|
builder: String_Builder;
|
|
inside := false;
|
|
for raw: split(text, "\n",, temp) {
|
|
line := raw;
|
|
hash := find_index_from_left(line, #char "#");
|
|
if hash >= 0 line = slice(line, 0, hash);
|
|
line = trim(line);
|
|
if !line.count continue;
|
|
if begins_with(line, "[model ") { inside = line == tprint("[model %]", model); continue; }
|
|
if !inside continue;
|
|
colon := find_index_from_left(line, #char ":");
|
|
if colon >= 0 line = tprint("%: %", trim(slice(line, 0, colon)), trim(advance(line, colon + 1)));
|
|
print_to_builder(*builder, "%\n", line);
|
|
}
|
|
return builder_to_string(*builder);
|
|
}
|
|
|
|
// The files the fetch scripts pin (modules/OCR/fetch_models.jai, modules/Labels/fetch_models.jai).
|
|
// Segmentation's model is made on each machine by tools/export_mask2former.py, so it has no pin:
|
|
// copy the .onnx from one machine to the others to be sure they run the same one.
|
|
Pin :: struct { name: string; sha256: string; }
|
|
PINNED :: Pin.[
|
|
.{"ch_PP-OCRv5_det_mobile.onnx", "4d97c44a20d30a81aad087d6a396b08f786c4635742afc391f6621f5c6ae78ae"},
|
|
.{"ch_PP-OCRv5_rec_mobile.onnx", "5825fc7ebf84ae7a412be049820b4d86d77620f204a041697b0494669b1742c5"},
|
|
.{"ppocrv5_dict.txt", "d1979e9f794c464c0d2e0b70a7fe14dd978e9dc644c0e71f14158cdf8342af1b"},
|
|
.{"ch_PP-LCNet_x0_25_textline_ori_cls_mobile.onnx", "54379ae5174d026780215fc748a7f31910dee36818e63d49e17dc598ecc82df7"},
|
|
.{"ch_PP-OCRv5_det_server.onnx", "0f8846b1d4bba223a2a2f9d9b44022fbc22cc019051a602b41a7fda9667e4cad"},
|
|
.{"ch_PP-OCRv5_rec_server.onnx", "e09385400eaaaef34ceff54aeb7c4f0f1fe014c27fa8b9905d4709b65746562a"},
|
|
.{"siglip2-base-patch16-384-vision.onnx", "49ae4958b1098ca995e929d646f7be05a69c65e6344beae07d58c6598ffc5210"},
|
|
.{"siglip2-base-patch16-384-text-int8.onnx", "30a94b43b9ddc39c04ab67f80cba4b808262682b2fa1a2a8b924bf1e7bd84c61"},
|
|
.{"siglip2-tokenizer.json", "cb9140fae3ac5122c972d37adf83e1248471a38147ad76f8215c8872c6fd8322"},
|
|
];
|
|
|
|
fingerprint_task :: (task: *Task) {
|
|
h: Sha256;
|
|
pinned, differs := 0, 0;
|
|
for task.files {
|
|
progress("hashing", task.name, it, it_index + 1, task.files.count);
|
|
hash := hash_file_cached(tprint("%/%", task.models_folder, it));
|
|
if !hash hash = "missing";
|
|
sha256_update(*h, tprint("% %\n", it, hash));
|
|
for pin: PINNED if pin.name == it { if pin.sha256 == hash pinned += 1; else differs += 1; }
|
|
}
|
|
sha256_update(*h, task.config);
|
|
task.fingerprint = sha256_finish(*h);
|
|
task.pinned = ifx differs then "no" else ifx pinned then "yes" else "-";
|
|
}
|
|
|
|
run_task :: (task: *Task, prefer_gpu: bool, corpus: *Corpus) -> Run {
|
|
run: Run;
|
|
run.task = task;
|
|
run.provider = ifx prefer_gpu then "CUDA" else "CPU";
|
|
run.unit = ifx task.kind == .LABELS_TEXT then "phrase" else "image";
|
|
images := corpus.images;
|
|
run.items = ifx task.kind == .LABELS_TEXT then PHRASES.count else images.count;
|
|
if task.kind != .LABELS_TEXT run.megapixels = corpus.megapixels;
|
|
|
|
sampler_start(*run.gpu); // the baseline: the device's memory before the engine
|
|
sampler_pause();
|
|
|
|
// The engine.
|
|
progress(task.name, run.provider, "loading", 0, 0);
|
|
load_start := current_time_monotonic();
|
|
engine: struct { ocr: *Ocr_Engine; labels: *Labels_Engine; segment: *Segment_Engine; };
|
|
ok: bool;
|
|
error: string;
|
|
phrase_vectors: [] float;
|
|
if task.kind == {
|
|
case .OCR;
|
|
ocr_options: Ocr_Options;
|
|
if options.ocr_workers ocr_options.gpu_workers = options.ocr_workers;
|
|
engine.ocr, ok, error = ocr_create(task.models_folder, task.model, prefer_gpu, ocr_options);
|
|
if ok {
|
|
ocr_gpu, ocr_why := ocr_gpu_status(engine.ocr);
|
|
if prefer_gpu && !ocr_gpu run.explanation = copy_string(ocr_why);
|
|
run.provider = sprint("%", ocr_provider(engine.ocr));
|
|
run.batch = engine.ocr.rec_batch;
|
|
b := engine.ocr.budget;
|
|
if b.enabled run.budget = sprint("detector %+% MB/MP, recognizer %+% MB per 1000 line-px", FF(b.det_fixed / MEGABYTE, 0), FF(b.det_per_pixel * 1_000_000 / MEGABYTE, 0),
|
|
FF(b.rec_fixed / MEGABYTE, 0), FF(b.rec_per_column * 1000 / MEGABYTE, 1));
|
|
else if engine.ocr.provider == .CUDA run.budget = "off (the arena could not be measured)";
|
|
}
|
|
case .LABELS; #through;
|
|
case .LABELS_TEXT;
|
|
engine.labels, ok, error = labels_create(task.models_folder, task.model, prefer_gpu && task.kind == .LABELS);
|
|
if ok {
|
|
labels_gpu, labels_why := labels_gpu_status(engine.labels);
|
|
if prefer_gpu && !labels_gpu run.explanation = copy_string(labels_why);
|
|
run.provider = ifx task.kind == .LABELS_TEXT then "CPU" else sprint("%", engine.labels.provider);
|
|
run.batch = ifx task.kind == .LABELS_TEXT then 1 else engine.labels.model.image_batch;
|
|
phrases := PHRASES;
|
|
phrase_vectors = labels_embed_texts(engine.labels, phrases);
|
|
}
|
|
case .SEGMENT;
|
|
engine.segment, ok, error = segment_create(task.models_folder, task.model, prefer_gpu);
|
|
if ok {
|
|
if prefer_gpu && engine.segment.provider != .CUDA run.explanation = copy_string(engine.segment.gpu_explanation);
|
|
run.provider = sprint("%", engine.segment.provider);
|
|
run.batch = ifx engine.segment.provider == .CUDA then engine.segment.model.batch_gpu else engine.segment.model.batch_cpu;
|
|
}
|
|
}
|
|
run.load_s = seconds_since(load_start);
|
|
trace(*run, "load");
|
|
if !ok { sampler_stop(); run.error = copy_string(error); return run; }
|
|
defer {
|
|
ocr_destroy(engine.ocr);
|
|
labels_destroy(engine.labels);
|
|
segment_destroy(engine.segment);
|
|
array_free(phrase_vectors);
|
|
}
|
|
if prefer_gpu && run.provider != "CUDA" {
|
|
// Said, and not timed: the CPU run, when asked for, is the same thing.
|
|
sampler_stop();
|
|
run.error = sprint("not on the GPU: %", run.explanation);
|
|
return run;
|
|
}
|
|
|
|
// One pass over the corpus, or over its first image alone; what it found, and its digest.
|
|
pass :: (first_only: bool) -> output: string, digest: string #expand {
|
|
h: Sha256;
|
|
builder: String_Builder;
|
|
builder.allocator = temp;
|
|
count := ifx first_only then 1 else `run.items;
|
|
if `task.kind == {
|
|
case .OCR;
|
|
lines, characters: s64;
|
|
done := 0;
|
|
while done < count {
|
|
n := min(OCR_BATCH, count - done);
|
|
batch := NewArray(n, Ocr_Image,, temp);
|
|
for 0..n - 1 { image := `images[done + it]; batch[it] = .{image.pixels, image.width, image.height, image.width * 4}; }
|
|
results := ocr_process(`engine.ocr, batch,, temp);
|
|
for results {
|
|
if it.failed { sha256_update(*h, "failed\n"); continue; }
|
|
lines += it.lines.count;
|
|
for line: it.lines { characters += line.text.count; sha256_update(*h, line.text); sha256_update(*h, "\n"); }
|
|
}
|
|
done += n;
|
|
}
|
|
print_to_builder(*builder, "lines:%,chars:%", lines, characters);
|
|
case .LABELS;
|
|
input := NewArray(count, Labels_Image,, temp);
|
|
for 0..count - 1 { image := `images[it]; input[it] = .{image.pixels, image.width, image.height, image.width * 4}; }
|
|
vectors, error := labels_embed_images(`engine.labels, input,, temp);
|
|
if error sha256_update(*h, "failed\n");
|
|
// Each image's best phrase: what the label list would make of it.
|
|
dims := `engine.labels.dimensions;
|
|
best_count: [PHRASES.count] s64;
|
|
for image: 0..count - 1 {
|
|
best, best_score := -1, -1.0;
|
|
for phrase: 0..PHRASES.count - 1 {
|
|
score := labels_score(`engine.labels, vectors.data + image * dims, `phrase_vectors.data + phrase * dims);
|
|
if score > best_score { best, best_score = phrase, score; }
|
|
}
|
|
sha256_update(*h, tprint("%\n", best));
|
|
best_count[best] += 1;
|
|
}
|
|
distinct := 0;
|
|
for best_count if it distinct += 1;
|
|
print_to_builder(*builder, "best_phrases:%", distinct);
|
|
case .LABELS_TEXT;
|
|
for 0..count - 1 {
|
|
one: [1] string;
|
|
one[0] = PHRASES[it];
|
|
vectors, error := labels_embed_texts(`engine.labels, one,, temp); // one at a time, as a search does
|
|
sha256_update(*h, ifx error then "failed\n" else "ok\n");
|
|
}
|
|
print_to_builder(*builder, "phrases:%", count);
|
|
case .SEGMENT;
|
|
input := NewArray(count, Segment_Image,, temp);
|
|
for 0..count - 1 { image := `images[it]; input[it] = .{image.pixels, image.width, image.height, image.width * 4}; }
|
|
results := segment_images(`engine.segment, input,, temp);
|
|
segments: s64;
|
|
for results {
|
|
if it.failed { sha256_update(*h, "failed\n"); continue; }
|
|
segments += it.segments.count;
|
|
for segment: it.segments sha256_update(*h, tprint("% ", segment.class));
|
|
sha256_update(*h, "\n");
|
|
}
|
|
print_to_builder(*builder, "segments:%", segments);
|
|
}
|
|
return builder_to_string(*builder,, temp), sha256_finish(*h,, temp);
|
|
}
|
|
|
|
// The first image alone: the first run pays for choosing kernels and setting up memory.
|
|
progress(task.name, run.provider, "first", 0, 0);
|
|
first_start := current_time_monotonic();
|
|
pass(true);
|
|
run.first_s = seconds_since(first_start);
|
|
trace(*run, "first");
|
|
reset_temporary_storage();
|
|
for 1..options.warmup {
|
|
progress(task.name, run.provider, "warm-up", it, options.warmup);
|
|
pass(false);
|
|
trace(*run, tprint("warm%", it));
|
|
reset_temporary_storage();
|
|
}
|
|
if task.kind == .OCR ocr_reset_stats(engine.ocr);
|
|
if task.kind == .SEGMENT engine.segment.stats = .{};
|
|
|
|
sampler_resume();
|
|
for 1..options.repeats {
|
|
progress(task.name, run.provider, "timing", it, options.repeats);
|
|
start := current_time_monotonic();
|
|
sampler_pass_start();
|
|
output, digest := pass(false);
|
|
array_add(*run.pass_s, seconds_since(start));
|
|
trace(*run, tprint("pass%", it), sampler_pass_peak());
|
|
if it == 1 { run.output = copy_string(output); run.digest = copy_string(short(digest)); }
|
|
reset_temporary_storage();
|
|
}
|
|
sampler_stop();
|
|
|
|
per := cast(float64) max(run.items * options.repeats, 1) * 1000;
|
|
if task.kind == .OCR {
|
|
s := ocr_stats(engine.ocr);
|
|
run.stages = sprint("detect:%,orient:%,recognize:%,budget_waits:%", FF(s.detect_us / per, 2), FF(s.orientation_us / per, 2), FF(s.recognize_us / per, 2), s.budget_waits);
|
|
if engine.ocr.budget.enabled run.budget = sprint("%; limit % MB at the end", run.budget, FF(engine.ocr.budget.limit / MEGABYTE, 0));
|
|
} else if task.kind == .SEGMENT {
|
|
s := engine.segment.stats;
|
|
run.stages = sprint("prepare:%,model:%,post:%", FF(s.prepare_us / per, 2), FF(s.model_us / per, 2), FF(s.post_us / per, 2));
|
|
}
|
|
run.ok = true;
|
|
return run;
|
|
}
|
|
|
|
OCR_BATCH :: 8; // images per ocr_process call, as the app's scan gives them
|
|
|
|
// Phrases for the label model: its image vectors are scored against them (the output's digest), and
|
|
// the text encoder is timed on them, one at a time, as a like: search runs it.
|
|
PHRASES :: string.[
|
|
"a screenshot of a code editor showing source code", "a screenshot of a terminal window",
|
|
"a spreadsheet with rows and columns of numbers", "a document with paragraphs of text",
|
|
"a chat conversation on a phone", "a dialog box asking to save changes",
|
|
"a photo of a living room", "a photo of people playing tennis",
|
|
"a photo of food on a plate", "a photo of a street with cars",
|
|
"a photo of an animal", "a photo of a person", "an electronic circuit schematic diagram",
|
|
"a printed circuit board", "a graph with several colored lines", "a web page in a browser",
|
|
];
|
|
|
|
Median :: struct { median, best, worst: float64; }
|
|
|
|
median_of :: (values: [] float64) -> Median {
|
|
sorted := array_copy(values,, temp);
|
|
quick_sort(sorted, (a: float64, b: float64) -> s64 { return ifx a < b then -1 else ifx a > b then 1 else 0; });
|
|
m: Median;
|
|
if !sorted.count return m;
|
|
m.median = ifx sorted.count % 2 then sorted[sorted.count / 2] else (sorted[sorted.count / 2 - 1] + sorted[sorted.count / 2]) / 2;
|
|
m.best, m.worst = sorted[0], sorted[sorted.count - 1];
|
|
return m;
|
|
}
|
|
|
|
print_run :: (task: *Task, run: Run) {
|
|
m := median_of(run.pass_s);
|
|
print("== % on % (batch %)\n", task.name, run.provider, run.batch);
|
|
print(" load % s, first % % s\n", FF(run.load_s, 2), run.unit, FF(run.first_s, 3));
|
|
print(" % ms per % (median of %; best %, worst %), % %s/s", FF(1000 * m.median / run.items, 2), run.unit, run.pass_s.count,
|
|
FF(1000 * m.best / run.items, 2), FF(1000 * m.worst / run.items, 2), FF(run.items / m.median, 1), run.unit);
|
|
if run.megapixels print(", % megapixels/s", FF(run.megapixels / m.median, 1));
|
|
print("\n");
|
|
if run.stages print(" per %, summed over threads (ms): %\n", run.unit, run.stages);
|
|
if run.budget print(" GPU memory budget: %\n", run.budget);
|
|
if run.gpu.samples print(" GPU: % MB more memory at the peak, % percent busy, % W, % MHz, up to % C\n",
|
|
FF(run.gpu.peak_mb - run.gpu.baseline_mb, 0), FF(run.gpu.util_sum / run.gpu.samples, 0),
|
|
FF(run.gpu.power_sum / run.gpu.samples, 0), FF(run.gpu.clock_sum / run.gpu.samples, 0), FF(run.gpu.temperature_max, 0));
|
|
if run.trace.count print(" GPU memory over the baseline (MB), after each step [and its peak]: %\n", join(..run.trace, " ",, temp));
|
|
print(" found % (digest %)\n\n", run.output, run.digest);
|
|
}
|
|
|
|
write_results :: (info: System_Info, started_at: Apollo_Time, corpus: *Corpus, tasks: [] Task, runs: [] Run, cudart: string, cudnn: string) -> path: string {
|
|
b: String_Builder;
|
|
print_to_builder(*b, "%\n", BENCH_RESULTS_FORMAT);
|
|
date := to_calendar(started_at, .LOCAL);
|
|
print_to_builder(*b, "date: %-%-% %:%:%\n", date.year, pad2(date.month_starting_at_0 + 1), pad2(date.day_of_month_starting_at_0 + 1), pad2(date.hour), pad2(date.minute), pad2(date.second));
|
|
print_to_builder(*b, "machine: %\n", one_line(info.machine));
|
|
if options.note print_to_builder(*b, "note: %\n", one_line(options.note));
|
|
print_to_builder(*b, "os: %\n", info.os);
|
|
print_to_builder(*b, "cpu: % (% threads)\n", one_line(info.cpu), info.logical_cores);
|
|
print_to_builder(*b, "ram_gb: %\n", FF(info.ram_gb, 1));
|
|
print_to_builder(*b, "gpu: %\n", ifx info.gpu then one_line(info.gpu) else tprint("none (%)", info.gpu_error));
|
|
if info.gpu {
|
|
print_to_builder(*b, "vram_gb: %\n", FF(info.vram_gb, 1));
|
|
print_to_builder(*b, "driver: % (CUDA %)\n", info.driver, info.cuda_driver);
|
|
print_to_builder(*b, "power_limit_w: %\n", FF(info.power_limit_w, 0));
|
|
print_to_builder(*b, "max_clocks_mhz: % / %\n", info.max_sm_clock_mhz, info.max_mem_clock_mhz);
|
|
}
|
|
if info.power_source print_to_builder(*b, "power: %\n", info.power_source);
|
|
print_to_builder(*b, "onnxruntime: %\n", ort.version);
|
|
if cudart print_to_builder(*b, "cudart: %\n", cudart);
|
|
if cudnn print_to_builder(*b, "cudnn: %\n", cudnn);
|
|
print_to_builder(*b, "mcap: % (%, %)\n", VERSION, GIT_HASH, GIT_BRANCH);
|
|
print_to_builder(*b, "passes: % timed, % warm-up\n", options.repeats, options.warmup);
|
|
print_to_builder(*b, "corpus: %\n", corpus.fingerprint);
|
|
print_to_builder(*b, "corpus_size: % images, % megapixels\n", corpus.images.count, FF(corpus.megapixels, 1));
|
|
for tasks {
|
|
print_to_builder(*b, "model: % %\n", it.name, it.fingerprint);
|
|
print_to_builder(*b, "pinned %: %\n", it.name, it.pinned);
|
|
}
|
|
for run: runs {
|
|
task := run.task;
|
|
if !run.ok { print_to_builder(*b, "error % %: %\n", task.name, run.provider, one_line(run.error)); continue; }
|
|
m := median_of(run.pass_s);
|
|
print_to_builder(*b, "result: % % unit=% items=% batch=% ms=% best_ms=% worst_ms=% per_s=% load_s=% first_s=%",
|
|
task.name, run.provider, run.unit, run.items, run.batch, FF(1000 * m.median / run.items, 3), FF(1000 * m.best / run.items, 3),
|
|
FF(1000 * m.worst / run.items, 3), FF(run.items / m.median, 2), FF(run.load_s, 2), FF(run.first_s, 3));
|
|
if run.megapixels print_to_builder(*b, " mp_per_s=%", FF(run.megapixels / m.median, 2));
|
|
if run.gpu.samples {
|
|
g := run.gpu;
|
|
print_to_builder(*b, " vram_mb=% gpu_util=% power_w=% sm_mhz=% temp_c=%", FF(g.peak_mb - g.baseline_mb, 0), FF(g.util_sum / g.samples, 0),
|
|
FF(g.power_sum / g.samples, 0), FF(g.clock_sum / g.samples, 0), FF(g.temperature_max, 0));
|
|
}
|
|
if run.stages print_to_builder(*b, " stages=%", run.stages);
|
|
if run.trace.count print_to_builder(*b, " trace=%", join(..run.trace, ",",, temp));
|
|
print_to_builder(*b, " output=% digest=%\n", run.output, run.digest);
|
|
}
|
|
name := tprint("%_%-%-%_%-%-%.txt", file_safe(info.machine), date.year, pad2(date.month_starting_at_0 + 1), pad2(date.day_of_month_starting_at_0 + 1),
|
|
pad2(date.hour), pad2(date.minute), pad2(date.second));
|
|
path := tprint("%/%", options.out_folder, name);
|
|
if !write_entire_file(path, *b) print("Could not write %.\n", path);
|
|
return path;
|
|
}
|
|
|
|
// For the GPU Benchmark window: where the run is. total 0: no count.
|
|
progress :: (task: string, provider: string, phase: string, done: s64, total: s64) {
|
|
if !options.progress return;
|
|
print("@progress % % % % %\n", task, provider, phase, done, total);
|
|
}
|
|
|
|
// The GPU, sampled every SAMPLE_MS on a thread of its own while the timed passes run.
|
|
SAMPLE_MS :: 100;
|
|
|
|
sampler: struct {
|
|
thread: Thread;
|
|
stats: *Gpu_Stats;
|
|
running: bool; // the thread is up
|
|
paused: bool; // it is up, and not sampling
|
|
mutex: Mutex;
|
|
};
|
|
|
|
sampler_start :: (stats: *Gpu_Stats) {
|
|
s, ok := gpu_sample();
|
|
if !ok return;
|
|
stats.sampled = true;
|
|
stats.baseline_mb = s.memory_used_mb;
|
|
sampler.stats = stats;
|
|
init(*sampler.mutex);
|
|
sampler.running = true;
|
|
sampler.paused = false;
|
|
thread_init(*sampler.thread, (thread: *Thread) -> s64 {
|
|
while sampler.running {
|
|
sleep_milliseconds(SAMPLE_MS);
|
|
s, ok := gpu_sample();
|
|
lock(*sampler.mutex);
|
|
if ok && !sampler.paused && sampler.running {
|
|
g := sampler.stats;
|
|
g.peak_mb = max(g.peak_mb, s.memory_used_mb);
|
|
g.pass_peak_mb = max(g.pass_peak_mb, s.memory_used_mb);
|
|
g.util_sum += s.util_percent;
|
|
g.power_sum += s.power_w;
|
|
g.clock_sum += s.sm_clock_mhz;
|
|
g.temperature_max = max(g.temperature_max, s.temperature_c);
|
|
g.samples += 1;
|
|
}
|
|
unlock(*sampler.mutex);
|
|
}
|
|
return 0;
|
|
});
|
|
thread_start(*sampler.thread);
|
|
}
|
|
|
|
// The GPU's memory now, over the run's baseline, as a step of its trace; with the step's peak when known.
|
|
trace :: (run: *Run, step: string, peak: float64 = -1) {
|
|
if !run.gpu.sampled return;
|
|
s, ok := gpu_sample();
|
|
if !ok return;
|
|
now := s.memory_used_mb - run.gpu.baseline_mb;
|
|
if peak >= 0 array_add(*run.trace, sprint("%:%[%]", step, FF(now, 0), FF(peak - run.gpu.baseline_mb, 0)));
|
|
else array_add(*run.trace, sprint("%:%", step, FF(now, 0)));
|
|
}
|
|
|
|
// The peak of one timed pass: reset as it starts, read as it ends.
|
|
sampler_pass_start :: () { if !sampler.running return; lock(*sampler.mutex); sampler.stats.pass_peak_mb = 0; unlock(*sampler.mutex); }
|
|
sampler_pass_peak :: () -> float64 {
|
|
if !sampler.running return -1;
|
|
lock(*sampler.mutex);
|
|
peak := sampler.stats.pass_peak_mb;
|
|
unlock(*sampler.mutex);
|
|
return peak;
|
|
}
|
|
|
|
sampler_pause :: () { if !sampler.running return; lock(*sampler.mutex); sampler.paused = true; unlock(*sampler.mutex); }
|
|
sampler_resume :: () { if !sampler.running return; lock(*sampler.mutex); sampler.paused = false; unlock(*sampler.mutex); }
|
|
|
|
sampler_stop :: () {
|
|
if !sampler.running return;
|
|
lock(*sampler.mutex);
|
|
sampler.running = false;
|
|
unlock(*sampler.mutex);
|
|
thread_is_done(*sampler.thread, -1);
|
|
thread_deinit(*sampler.thread);
|
|
destroy(*sampler.mutex);
|
|
}
|
|
|
|
// Model files are hundreds of MB, so their hashes are kept in the results folder, by path, size and
|
|
// time: hashed again only when one changes.
|
|
hash_cache: [..] struct { key: string; hash: string; };
|
|
|
|
hash_cache_load :: () {
|
|
text, ok := read_entire_file(tprint("%/.hash-cache", options.out_folder), log_errors = false);
|
|
if !ok return;
|
|
for split(text, "\n") {
|
|
tab := find_index_from_left(it, #char "\t");
|
|
if tab > 0 array_add(*hash_cache, .{trim(advance(it, tab + 1)), slice(it, 0, tab)});
|
|
}
|
|
}
|
|
|
|
hash_cache_save :: () {
|
|
b: String_Builder;
|
|
for hash_cache print_to_builder(*b, "%\t%\n", it.hash, it.key);
|
|
write_entire_file(tprint("%/.hash-cache", options.out_folder), *b);
|
|
}
|
|
|
|
hash_file_cached :: (path: string) -> string {
|
|
modtime, size, ok := file_modtime_and_size(path);
|
|
if !ok return "";
|
|
key := sprint("% % % %", size, modtime.low, modtime.high, path);
|
|
for hash_cache if it.key == key return it.hash;
|
|
hash := sha256_of_file(path);
|
|
if hash array_add(*hash_cache, .{key, hash});
|
|
return hash;
|
|
}
|
|
|
|
MEGABYTE :: 1024.0 * 1024;
|
|
|
|
seconds_since :: (start: Apollo_Time) -> float64 {
|
|
return to_float64_seconds(current_time_monotonic() - start);
|
|
}
|
|
|
|
short :: (hash: string) -> string { return slice(hash, 0, min(hash.count, 12)); }
|
|
|
|
pad2 :: (n: s64) -> string { return tprint("%", formatInt(n, minimum_digits = 2)); }
|
|
|
|
one_line :: (text: string) -> string {
|
|
return replace(replace(trim(text), "\r", " ",, temp), "\n", " ",, temp);
|
|
}
|
|
|
|
file_safe :: (text: string) -> string {
|
|
result := copy_string(text,, temp);
|
|
for 0..result.count - 1 {
|
|
c := result[it];
|
|
if !(is_alnum(c) || c == #char "-") result[it] = #char "_";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#load "sha256.jai";
|
|
#load "system_info.jai";
|
|
#load "results.jai";
|
|
|
|
#import "Basic";
|
|
#import "String";
|
|
#import "File";
|
|
#import "File_Utilities";
|
|
#import "Sort";
|
|
#import "System";
|
|
#import "Thread";
|
|
#import "Image_Decode";
|
|
#import "onnxruntime";
|
|
#import "OCR";
|
|
#import "Labels";
|
|
#import "Segment";
|