// sa3-ggml-test.cpp: parity test CLI for the StableStep GGML SA3 modules. // // Runs GGML ports of the SA3 conditioning modules against golden vectors // dumped from the validated ONNX graphs (see tools/onnx-export/), and reports // cosine similarity + max abs diff per component. // // Usage: // sa3-ggml-test --models --goldens // [--component text_enc|seconds|same_enc|same_dec|dit|all] // // Components: // text_enc: sa3-text-enc-BF16.gguf, T5Gemma encoder + learned padding // substitution. Inputs input_ids [1,S] i64 + attention_mask [1,S] // u8, expected embeddings [1,S,768] f32. // seconds: sa3-dit-BF16.gguf (embedder tensors only), NumberConditioner // expo Fourier embedder. Input [1] f32, expected [1,768] f32. // same_enc: sa3-same-enc-F16.gguf, SAME-L encoder. Input audio // [1,2,524288] f32, expected latents [1,256,128] f32. // same_dec: sa3-same-dec-F16.gguf, SAME-L decoder. Input latents // [1,256,128] f32, expected audio [1,2,524288] f32. // dit: sa3-dit-BF16.gguf, DiffusionTransformer single forward. Inputs // x [1,256,T] f32, t [1] f32, cross_attn_cond [1,S,768] f32, // global_embed [1,768] f32, local_add_cond [1,257,T] f32, // padding_mask [1,T] u8; expected v [1,256,T] f32. // // Exit code 0 only if every run component passes cosine > 0.999. #include "sa3-dit-ggml.h" #include "sa3-same-ggml.h" #include "sa3-t5gemma-enc.h" #include "yyjson.h" #include #include #include #include #include #include #include static const double PASS_COSINE = 0.999; static bool read_file(const std::string & path, std::vector & out) { FILE * f = fopen(path.c_str(), "rb"); if (!f) { fprintf(stderr, "[Test] cannot open %s\n", path.c_str()); return false; } fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); out.resize((size_t) sz); size_t rd = fread(out.data(), 1, (size_t) sz, f); fclose(f); if (rd != (size_t) sz) { fprintf(stderr, "[Test] short read on %s\n", path.c_str()); return false; } return true; } struct Metrics { double cosine; double max_abs_diff; }; static Metrics compare(const float * a, const float * b, size_t n) { double dot = 0, na = 0, nb = 0, mad = 0; for (size_t i = 0; i < n; i++) { dot += (double) a[i] * b[i]; na += (double) a[i] * a[i]; nb += (double) b[i] * b[i]; double d = fabs((double) a[i] - b[i]); if (d > mad) { mad = d; } } Metrics m; m.cosine = (na > 0 && nb > 0) ? dot / (sqrt(na) * sqrt(nb)) : 0.0; m.max_abs_diff = mad; return m; } // Manifest helpers: get golden[""]["inputs"/"outputs"][""]["file"] static std::string manifest_file(yyjson_val * root, const char * comp, const char * io, const char * name) { yyjson_val * c = yyjson_obj_get(root, comp); yyjson_val * g = c ? yyjson_obj_get(c, io) : NULL; yyjson_val * t = g ? yyjson_obj_get(g, name) : NULL; yyjson_val * f = t ? yyjson_obj_get(t, "file") : NULL; return (f && yyjson_is_str(f)) ? yyjson_get_str(f) : ""; } static int64_t manifest_shape_prod(yyjson_val * root, const char * comp, const char * io, const char * name) { yyjson_val * c = yyjson_obj_get(root, comp); yyjson_val * g = c ? yyjson_obj_get(c, io) : NULL; yyjson_val * t = g ? yyjson_obj_get(g, name) : NULL; yyjson_val * s = t ? yyjson_obj_get(t, "shape") : NULL; if (!s || !yyjson_is_arr(s)) { return 0; } int64_t prod = 1; size_t idx, max; yyjson_val * d; yyjson_arr_foreach(s, idx, max, d) { prod *= yyjson_get_int(d); } return prod; } static bool run_text_enc(const std::string & models, const std::string & goldens, yyjson_val * root, Metrics * out) { std::string ids_f = manifest_file(root, "text_enc", "inputs", "input_ids"); std::string mask_f = manifest_file(root, "text_enc", "inputs", "attention_mask"); std::string exp_f = manifest_file(root, "text_enc", "outputs", "embeddings"); if (ids_f.empty() || mask_f.empty() || exp_f.empty()) { fprintf(stderr, "[Test] text_enc: manifest missing entries\n"); return false; } int64_t S = manifest_shape_prod(root, "text_enc", "inputs", "input_ids"); int64_t n_out = manifest_shape_prod(root, "text_enc", "outputs", "embeddings"); if (S <= 0 || n_out <= 0 || n_out % S != 0) { fprintf(stderr, "[Test] text_enc: bad shapes in manifest\n"); return false; } int64_t H = n_out / S; std::vector ids_raw, mask_raw, exp_raw; if (!read_file(goldens + "/" + ids_f, ids_raw) || !read_file(goldens + "/" + mask_f, mask_raw) || !read_file(goldens + "/" + exp_f, exp_raw)) { return false; } if (ids_raw.size() != (size_t) S * 8 || mask_raw.size() != (size_t) S || exp_raw.size() != (size_t) n_out * 4) { fprintf(stderr, "[Test] text_enc: golden file sizes do not match manifest shapes\n"); return false; } std::vector ids((size_t) S); const int64_t * ids64 = (const int64_t *) ids_raw.data(); for (int64_t i = 0; i < S; i++) { ids[(size_t) i] = (int32_t) ids64[i]; } SA3T5GemmaEnc enc = {}; if (!sa3_t5gemma_load(&enc, (models + "/sa3-text-enc-BF16.gguf").c_str())) { return false; } if ((int64_t) enc.cfg.hidden_size != H) { fprintf(stderr, "[Test] text_enc: model H=%d but golden H=%lld\n", enc.cfg.hidden_size, (long long) H); sa3_t5gemma_free(&enc); return false; } const char * env_layers = getenv("SA3_T5G_LAYERS"); if (env_layers) { enc.debug_n_layers = atoi(env_layers); fprintf(stderr, "[Test] text_enc: DEBUG truncated to %d layers\n", enc.debug_n_layers); } std::vector got((size_t) n_out); sa3_t5gemma_forward(&enc, ids.data(), mask_raw.data(), (int) S, got.data()); sa3_t5gemma_free(&enc); *out = compare(got.data(), (const float *) exp_raw.data(), (size_t) n_out); return true; } static bool run_seconds(const std::string & models, const std::string & goldens, yyjson_val * root, Metrics * out) { std::string in_f = manifest_file(root, "seconds", "inputs", "seconds"); std::string exp_f = manifest_file(root, "seconds", "outputs", "embed"); if (in_f.empty() || exp_f.empty()) { fprintf(stderr, "[Test] seconds: manifest missing entries\n"); return false; } std::vector in_raw, exp_raw; if (!read_file(goldens + "/" + in_f, in_raw) || !read_file(goldens + "/" + exp_f, exp_raw)) { return false; } if (in_raw.size() != 4) { fprintf(stderr, "[Test] seconds: bad input size\n"); return false; } float seconds = *(const float *) in_raw.data(); int64_t n_out = manifest_shape_prod(root, "seconds", "outputs", "embed"); if (n_out <= 0 || exp_raw.size() != (size_t) n_out * 4) { fprintf(stderr, "[Test] seconds: golden size mismatch\n"); return false; } SA3SecondsEmbedder emb; if (!sa3_seconds_embedder_load(&emb, (models + "/sa3-dit-BF16.gguf").c_str())) { return false; } if ((int64_t) emb.out_dim != n_out) { fprintf(stderr, "[Test] seconds: model out=%d but golden %lld\n", emb.out_dim, (long long) n_out); return false; } std::vector got((size_t) n_out); sa3_seconds_embed(emb, seconds, got.data()); *out = compare(got.data(), (const float *) exp_raw.data(), (size_t) n_out); return true; } // Shared runner for the SAME autoencoder halves. is_encoder selects the // direction; the golden manifest supplies both tensors' shapes. static bool run_same(const std::string & models, const std::string & goldens, yyjson_val * root, bool is_encoder, Metrics * out) { const char * comp = is_encoder ? "same_enc" : "same_dec"; const char * in_name = is_encoder ? "audio" : "latents"; const char * out_name = is_encoder ? "latents" : "audio"; std::string in_f = manifest_file(root, comp, "inputs", in_name); std::string exp_f = manifest_file(root, comp, "outputs", out_name); if (in_f.empty() || exp_f.empty()) { fprintf(stderr, "[Test] %s: manifest missing entries\n", comp); return false; } int64_t n_in = manifest_shape_prod(root, comp, "inputs", in_name); int64_t n_out = manifest_shape_prod(root, comp, "outputs", out_name); int64_t n_lat = is_encoder ? n_out : n_in; if (n_in <= 0 || n_out <= 0 || n_lat % 256 != 0) { fprintf(stderr, "[Test] %s: bad shapes in manifest\n", comp); return false; } int n_latents = (int) (n_lat / 256); // latent_dim 256 std::vector in_raw, exp_raw; if (!read_file(goldens + "/" + in_f, in_raw) || !read_file(goldens + "/" + exp_f, exp_raw)) { return false; } if (in_raw.size() != (size_t) n_in * 4 || exp_raw.size() != (size_t) n_out * 4) { fprintf(stderr, "[Test] %s: golden file sizes do not match manifest shapes\n", comp); return false; } SA3Same same = {}; // F16 is the current conversion (see convert-sa3.py); fall back to the // older BF16 name if that is what is on disk. std::string gguf = models + (is_encoder ? "/sa3-same-enc-F16.gguf" : "/sa3-same-dec-F16.gguf"); { FILE * f = fopen(gguf.c_str(), "rb"); if (f) { fclose(f); } else { gguf = models + (is_encoder ? "/sa3-same-enc-BF16.gguf" : "/sa3-same-dec-BF16.gguf"); } } if (!sa3_same_load(&same, gguf.c_str(), is_encoder)) { return false; } // Debug hooks: SA3_SAME_STAGE= dumps the token sequence after n layers // (0 = folded input) to SA3_SAME_DUMP (default sa3_same_stage.bin). const char * env_stage = getenv("SA3_SAME_STAGE"); if (env_stage) { same.debug_stage = atoi(env_stage); fprintf(stderr, "[Test] %s: DEBUG dumping stage %d\n", comp, same.debug_stage); } std::vector got((size_t) n_out); sa3_same_forward(&same, (const float *) in_raw.data(), got.data(), n_latents); if (env_stage && !same.debug_out.empty()) { const char * dump = getenv("SA3_SAME_DUMP"); std::string path = dump ? dump : "sa3_same_stage.bin"; FILE * f = fopen(path.c_str(), "wb"); if (f) { fwrite(same.debug_out.data(), sizeof(float), same.debug_out.size(), f); fclose(f); fprintf(stderr, "[Test] %s: stage tensor (%zu floats) -> %s\n", comp, same.debug_out.size(), path.c_str()); } } sa3_same_free(&same); *out = compare(got.data(), (const float *) exp_raw.data(), (size_t) n_out); return true; } static bool run_dit(const std::string & models, const std::string & goldens, yyjson_val * root, Metrics * out) { const char * in_names[] = { "x", "t", "cross_attn_cond", "global_embed", "local_add_cond", "padding_mask" }; std::vector> raw(7); for (int i = 0; i < 6; i++) { std::string f = manifest_file(root, "dit", "inputs", in_names[i]); if (f.empty() || !read_file(goldens + "/" + f, raw[(size_t) i])) { fprintf(stderr, "[Test] dit: missing input '%s'\n", in_names[i]); return false; } } std::string exp_f = manifest_file(root, "dit", "outputs", "v"); if (exp_f.empty() || !read_file(goldens + "/" + exp_f, raw[6])) { fprintf(stderr, "[Test] dit: missing output 'v'\n"); return false; } int64_t n_x = manifest_shape_prod(root, "dit", "inputs", "x"); int64_t n_cross = manifest_shape_prod(root, "dit", "inputs", "cross_attn_cond"); int64_t n_glob = manifest_shape_prod(root, "dit", "inputs", "global_embed"); int64_t n_local = manifest_shape_prod(root, "dit", "inputs", "local_add_cond"); int64_t T = manifest_shape_prod(root, "dit", "inputs", "padding_mask"); int64_t n_out = manifest_shape_prod(root, "dit", "outputs", "v"); if (T <= 0 || n_x != 256 * T || n_out != n_x || n_glob != 768 || n_cross % 768 != 0 || n_local != 257 * T) { fprintf(stderr, "[Test] dit: bad shapes in manifest\n"); return false; } int64_t S_c = n_cross / 768; if (raw[0].size() != (size_t) n_x * 4 || raw[1].size() != 4 || raw[2].size() != (size_t) n_cross * 4 || raw[3].size() != (size_t) n_glob * 4 || raw[4].size() != (size_t) n_local * 4 || raw[5].size() != (size_t) T || raw[6].size() != (size_t) n_out * 4) { fprintf(stderr, "[Test] dit: golden file sizes do not match manifest shapes\n"); return false; } SA3DiT dit = {}; if (!sa3_dit_load(&dit, (models + "/sa3-dit-BF16.gguf").c_str())) { return false; } // Debug hook: SA3_DIT_STAGE= dumps the token sequence after n layers // (0 = memory+projected input) to SA3_DIT_DUMP (default sa3_dit_stage.bin). const char * env_stage = getenv("SA3_DIT_STAGE"); if (env_stage) { dit.debug_stage = atoi(env_stage); fprintf(stderr, "[Test] dit: DEBUG dumping stage %d\n", dit.debug_stage); } std::vector got((size_t) n_out); sa3_dit_forward(&dit, (const float *) raw[0].data(), *(const float *) raw[1].data(), (const float *) raw[2].data(), S_c, (const float *) raw[3].data(), (const float *) raw[4].data(), raw[5].data(), T, got.data()); if (env_stage && !dit.debug_out.empty()) { const char * dump = getenv("SA3_DIT_DUMP"); std::string path = dump ? dump : "sa3_dit_stage.bin"; FILE * f = fopen(path.c_str(), "wb"); if (f) { fwrite(dit.debug_out.data(), sizeof(float), dit.debug_out.size(), f); fclose(f); fprintf(stderr, "[Test] dit: stage tensor (%zu floats) -> %s\n", dit.debug_out.size(), path.c_str()); } } sa3_dit_free(&dit); *out = compare(got.data(), (const float *) raw[6].data(), (size_t) n_out); return true; } int main(int argc, char ** argv) { std::string models, goldens, component = "all"; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--models") && i + 1 < argc) { models = argv[++i]; } else if (!strcmp(argv[i], "--goldens") && i + 1 < argc) { goldens = argv[++i]; } else if (!strcmp(argv[i], "--component") && i + 1 < argc) { component = argv[++i]; } else { fprintf(stderr, "Usage: sa3-ggml-test --models --goldens [--component text_enc|seconds|same_enc|same_dec|dit|all]\n"); return 2; } } if (models.empty() || goldens.empty()) { fprintf(stderr, "Usage: sa3-ggml-test --models --goldens [--component text_enc|seconds|same_enc|same_dec|dit|all]\n"); return 2; } std::vector manifest_raw; if (!read_file(goldens + "/manifest.json", manifest_raw)) { return 2; } yyjson_doc * doc = yyjson_read((const char *) manifest_raw.data(), manifest_raw.size(), 0); if (!doc) { fprintf(stderr, "[Test] cannot parse manifest.json\n"); return 2; } yyjson_val * root = yyjson_doc_get_root(doc); bool all_pass = true; bool any_run = false; if (component == "all" || component == "text_enc") { Metrics m; any_run = true; if (run_text_enc(models, goldens, root, &m)) { bool pass = m.cosine > PASS_COSINE; printf("text_enc: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff, pass ? "PASS" : "FAIL"); all_pass = all_pass && pass; } else { printf("text_enc: ERROR\n"); all_pass = false; } } if (component == "all" || component == "seconds") { Metrics m; any_run = true; if (run_seconds(models, goldens, root, &m)) { bool pass = m.cosine > PASS_COSINE; printf("seconds: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff, pass ? "PASS" : "FAIL"); all_pass = all_pass && pass; } else { printf("seconds: ERROR\n"); all_pass = false; } } if (component == "all" || component == "same_enc") { Metrics m; any_run = true; if (run_same(models, goldens, root, true, &m)) { bool pass = m.cosine > PASS_COSINE; printf("same_enc: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff, pass ? "PASS" : "FAIL"); all_pass = all_pass && pass; } else { printf("same_enc: ERROR\n"); all_pass = false; } } if (component == "all" || component == "same_dec") { Metrics m; any_run = true; if (run_same(models, goldens, root, false, &m)) { bool pass = m.cosine > PASS_COSINE; printf("same_dec: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff, pass ? "PASS" : "FAIL"); all_pass = all_pass && pass; } else { printf("same_dec: ERROR\n"); all_pass = false; } } if (component == "all" || component == "dit") { Metrics m; any_run = true; if (run_dit(models, goldens, root, &m)) { bool pass = m.cosine > PASS_COSINE; printf("dit: cosine=%.6f max_abs_diff=%.6f %s\n", m.cosine, m.max_abs_diff, pass ? "PASS" : "FAIL"); all_pass = all_pass && pass; } else { printf("dit: ERROR\n"); all_pass = false; } } yyjson_doc_free(doc); if (!any_run) { fprintf(stderr, "[Test] unknown component '%s'\n", component.c_str()); return 2; } return all_pass ? 0 : 1; }