// ace-synth.cpp: ACE-Step synthesis CLI // Thin wrapper: parses args, scans the model registry, calls pipeline-synth, // writes output files. Model selection (synth_model, adapter, output_format) // comes from the request JSON. The registry resolves names to GGUF paths // under --models and --adapters . #include "audio-io.h" #include "backend.h" #include "ggml.h" #include "lua-plugin-registry.h" #include "model-registry.h" #include "model-store.h" #include "pipeline-synth.h" #include "request.h" #include "synth-batch-runner.h" #include "task-types.h" #include "version.h" #include #include #include #include #include #include #include // ─── Per-section mask broadcast self-test (HOTSTEP_BCAST_TEST) ─────────────── // Verifies the exact tensor ops the per-section masking relies on, on the real // backend, with no models. Test 1: ggml_mul([out,S,N] f32, [1,S,1] f32) — the // per-frame mask broadcast over the feature (ne0) and batch (ne2) dims. Test 2: // the real chain mul_mat(BF16 delta, f32 x) -> mul(mask). Run: set the env var // and invoke ace-synth (it runs the test and exits). static int run_bcast_selftest() { BackendPair bp = backend_init("BcastTest"); ggml_backend_t backend = bp.backend; fprintf(stderr, "[BcastTest] backend=%s\n", ggml_backend_name(backend)); const int64_t out = 8, S = 6, N = 2, in = 4; int fails = 0; // Test 1: pure elementwise broadcast mul. { struct ggml_init_params p = { ggml_tensor_overhead() * 16 + ggml_graph_overhead() + 4096, NULL, true }; struct ggml_context * ctx = ggml_init(p); struct ggml_tensor * a = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, out, S, N); struct ggml_tensor * mask = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 1, S, 1); ggml_set_input(a); ggml_set_input(mask); struct ggml_tensor * y = ggml_mul(ctx, a, mask); struct ggml_cgraph * gf = ggml_new_graph(ctx); ggml_build_forward_expand(gf, y); ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors(ctx, backend); std::vector ad((size_t) (out * S * N), 1.0f), md((size_t) S); for (int s = 0; s < S; s++) md[s] = (float) (s + 1) * 0.25f; // distinct per frame ggml_backend_tensor_set(a, ad.data(), 0, ad.size() * sizeof(float)); ggml_backend_tensor_set(mask, md.data(), 0, md.size() * sizeof(float)); ggml_backend_graph_compute(backend, gf); std::vector yd((size_t) (out * S * N)); ggml_backend_tensor_get(y, yd.data(), 0, yd.size() * sizeof(float)); int bad = 0; for (int n = 0; n < N; n++) for (int s = 0; s < S; s++) for (int f = 0; f < out; f++) { float got = yd[(size_t) n * S * out + (size_t) s * out + f]; if (fabsf(got - md[s]) > 1e-4f) { if (bad < 6) fprintf(stderr, "[BcastTest] T1 MISMATCH f=%d s=%d n=%d exp=%.3f got=%.3f\n", f, s, n, md[s], got); bad++; } } fprintf(stderr, "[BcastTest] T1 ggml_mul[out,S,N]x[1,S,1]: %s (%d/%lld bad)\n", bad ? "FAIL" : "PASS", bad, (long long) (out * S * N)); fails += bad ? 1 : 0; ggml_backend_buffer_free(buf); ggml_free(ctx); } // Test 2: real chain — mul_mat(BF16 delta[in,out], f32 x[in,S,N]) then mask. { struct ggml_init_params p = { ggml_tensor_overhead() * 32 + ggml_graph_overhead() + 4096, NULL, true }; struct ggml_context * ctx = ggml_init(p); struct ggml_tensor * d = ggml_new_tensor_2d(ctx, GGML_TYPE_BF16, in, out); struct ggml_tensor * x = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, in, S, N); struct ggml_tensor * mask = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 1, S, 1); ggml_set_input(d); ggml_set_input(x); ggml_set_input(mask); struct ggml_tensor * dy = ggml_mul_mat(ctx, d, x); // [out,S,N] struct ggml_tensor * dym = ggml_mul(ctx, dy, mask); ggml_set_output(dy); ggml_set_output(dym); struct ggml_cgraph * gf = ggml_new_graph(ctx); ggml_build_forward_expand(gf, dym); ggml_build_forward_expand(gf, dy); ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors(ctx, backend); std::vector dd((size_t) (in * out)); for (int i = 0; i < in * out; i++) dd[i] = ggml_fp32_to_bf16(0.5f); // all 0.5 std::vector xd((size_t) (in * S * N), 1.0f), md((size_t) S); for (int s = 0; s < S; s++) md[s] = (float) (s + 1) * 0.25f; ggml_backend_tensor_set(d, dd.data(), 0, dd.size() * sizeof(ggml_bf16_t)); ggml_backend_tensor_set(x, xd.data(), 0, xd.size() * sizeof(float)); ggml_backend_tensor_set(mask, md.data(), 0, md.size() * sizeof(float)); ggml_backend_graph_compute(backend, gf); std::vector dyd((size_t) (out * S * N)), dymd((size_t) (out * S * N)); ggml_backend_tensor_get(dy, dyd.data(), 0, dyd.size() * sizeof(float)); ggml_backend_tensor_get(dym, dymd.data(), 0, dymd.size() * sizeof(float)); // Each dy element = sum_in(0.5 * 1.0) = in*0.5 = 2.0; masked = 2.0*md[s]. int bad = 0; for (int n = 0; n < N; n++) for (int s = 0; s < S; s++) for (int f = 0; f < out; f++) { size_t idx = (size_t) n * S * out + (size_t) s * out + f; float exp = dyd[idx] * md[s]; if (fabsf(dymd[idx] - exp) > 1e-3f) { if (bad < 6) fprintf(stderr, "[BcastTest] T2 MISMATCH f=%d s=%d n=%d dy=%.3f exp=%.3f got=%.3f\n", f, s, n, dyd[idx], exp, dymd[idx]); bad++; } } fprintf(stderr, "[BcastTest] T2 dy=%.3f (expect 2.0); mask chain: %s (%d/%lld bad)\n", dyd[0], bad ? "FAIL" : "PASS", bad, (long long) (out * S * N)); fails += bad ? 1 : 0; ggml_backend_buffer_free(buf); ggml_free(ctx); } fprintf(stderr, "[BcastTest] RESULT: %s\n", fails ? "FAIL — broadcast is the bug" : "PASS — broadcast is fine, look elsewhere"); backend_release(bp.backend, bp.cpu_backend); return fails ? 1 : 0; } // ─── LoKr Kronecker-apply self-test (HOTSTEP_KRON_TEST) ────────────────────── // Phase-2 prototype for low-rank runtime adapters (docs/plans/lowrank-runtime- // adapters.md): verifies that (w1 ⊗ w2)@x can be computed on the real backend // from the factors alone — no materialized Kronecker delta — via // y[oa·c+oc, s] = Σ_ib w1[oa,ib] · ( Σ_id w2[oc,id] · x[ib·d+id, s] ) // i.e. mul_mat(w2) → permute/cont → mul_mat(w1) → permute/cont. Pass 0 runs // F32 factors (validates the choreography exactly), pass 1 runs BF16 factors // (the production storage type). Compared against a host-side dense-kron // reference. Batched inputs [in,S,N] flatten to [in,S·N] first, so 2D covers // them. Run: set the env var and invoke ace-synth (runs the test and exits). static int run_kron_selftest() { BackendPair bp = backend_init("KronTest"); ggml_backend_t backend = bp.backend; fprintf(stderr, "[KronTest] backend=%s\n", ggml_backend_name(backend)); // PyTorch shapes: w1 [a,b], w2 [c,d]; delta = kron(w1,w2) [out=a·c, in=b·d] const int64_t a = 3, b = 4, c = 5, d = 6, S = 7; const int64_t out = a * c, in = b * d; int fails = 0; // deterministic fill (LCG) — same values every run and backend auto fill = [](std::vector & v, uint32_t seed) { uint32_t s = seed; for (auto & f : v) { s = s * 1664525u + 1013904223u; f = ((float) (s >> 8) / (float) (1u << 24)) - 0.5f; // [-0.5, 0.5) } }; std::vector w1d((size_t) (a * b)), w2d((size_t) (c * d)), xd((size_t) (in * S)); fill(w1d, 1); fill(w2d, 2); fill(xd, 3); // Host reference: dense kron, then y = kron(w1,w2) @ x. // w1d/w2d are row-major PyTorch [out, in]: w1[oa,ib] = w1d[oa·b+ib]. std::vector yref((size_t) (out * S), 0.0f); for (int64_t oa = 0; oa < a; oa++) for (int64_t oc = 0; oc < c; oc++) for (int64_t s = 0; s < S; s++) { float acc = 0.0f; for (int64_t ib = 0; ib < b; ib++) for (int64_t id = 0; id < d; id++) acc += w1d[(size_t) (oa * b + ib)] * w2d[(size_t) (oc * d + id)] * xd[(size_t) ((ib * d + id) + in * s)]; yref[(size_t) ((oa * c + oc) + out * s)] = acc; } for (int pass = 0; pass < 2; pass++) { const bool bf16 = (pass == 1); const ggml_type ftype = bf16 ? GGML_TYPE_BF16 : GGML_TYPE_F32; // F32 tol allows CUDA's TF32-accumulated cuBLAS matmul (~1e-3 rel), which // reorders/rounds vs the naive host reference; a wrong permute would be // off by whole values on most elements, not 1e-4 on a few. const float tol = bf16 ? 2e-2f : 1e-3f; struct ggml_init_params p = { ggml_tensor_overhead() * 32 + ggml_graph_overhead() + 4096, NULL, true }; struct ggml_context * ctx = ggml_init(p); // Row-major PyTorch [rows, cols] uploads directly as ggml [cols, rows]: // w1g [b, a] element (ib, oa) == w1[oa, ib]; same for w2g [d, c]. struct ggml_tensor * w1g = ggml_new_tensor_2d(ctx, ftype, b, a); struct ggml_tensor * w2g = ggml_new_tensor_2d(ctx, ftype, d, c); struct ggml_tensor * x = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, in, S); ggml_set_input(w1g); ggml_set_input(w2g); ggml_set_input(x); // x [in=b·d, S] viewed as [d, b·S]: column ib·d+id has id fastest — matches // the kron column convention, so the reshape is a free view. struct ggml_tensor * X2 = ggml_reshape_2d(ctx, x, d, b * S); struct ggml_tensor * T = ggml_mul_mat(ctx, w2g, X2); // [c, b·S] = T(oc; ib,s) struct ggml_tensor * T3 = ggml_reshape_3d(ctx, T, c, b, S); struct ggml_tensor * P = ggml_cont(ctx, ggml_permute(ctx, T3, 1, 0, 2, 3)); // [b, c, S] struct ggml_tensor * P2 = ggml_reshape_2d(ctx, P, b, c * S); struct ggml_tensor * Y = ggml_mul_mat(ctx, w1g, P2); // [a, c·S] = y(oa; oc,s) struct ggml_tensor * Y3 = ggml_reshape_3d(ctx, Y, a, c, S); struct ggml_tensor * YP = ggml_cont(ctx, ggml_permute(ctx, Y3, 1, 0, 2, 3)); // [c, a, S] → flat out=oa·c+oc ggml_set_output(YP); struct ggml_cgraph * gf = ggml_new_graph(ctx); ggml_build_forward_expand(gf, YP); ggml_backend_buffer_t buf = ggml_backend_alloc_ctx_tensors(ctx, backend); if (bf16) { std::vector w1b((size_t) (a * b)), w2b((size_t) (c * d)); ggml_fp32_to_bf16_row(w1d.data(), w1b.data(), a * b); ggml_fp32_to_bf16_row(w2d.data(), w2b.data(), c * d); ggml_backend_tensor_set(w1g, w1b.data(), 0, w1b.size() * sizeof(ggml_bf16_t)); ggml_backend_tensor_set(w2g, w2b.data(), 0, w2b.size() * sizeof(ggml_bf16_t)); } else { ggml_backend_tensor_set(w1g, w1d.data(), 0, w1d.size() * sizeof(float)); ggml_backend_tensor_set(w2g, w2d.data(), 0, w2d.size() * sizeof(float)); } ggml_backend_tensor_set(x, xd.data(), 0, xd.size() * sizeof(float)); ggml_backend_graph_compute(backend, gf); std::vector yd((size_t) (out * S)); ggml_backend_tensor_get(YP, yd.data(), 0, yd.size() * sizeof(float)); int bad = 0; float max_err = 0.0f; for (int64_t o = 0; o < out; o++) for (int64_t s = 0; s < S; s++) { float exp = yref[(size_t) (o + out * s)]; float got = yd[(size_t) (o + out * s)]; float err = fabsf(got - exp); if (err > max_err) max_err = err; if (err > tol) { if (bad < 6) fprintf(stderr, "[KronTest] %s MISMATCH o=%lld s=%lld exp=%.5f got=%.5f\n", bf16 ? "BF16" : "F32", (long long) o, (long long) s, exp, got); bad++; } } fprintf(stderr, "[KronTest] %s factors: %s (%d/%lld bad, max_err=%.3g, tol=%.3g)\n", bf16 ? "BF16" : "F32", bad ? "FAIL" : "PASS", bad, (long long) (out * S), max_err, tol); fails += bad ? 1 : 0; ggml_backend_buffer_free(buf); ggml_free(ctx); } fprintf(stderr, "[KronTest] RESULT: %s\n", fails ? "FAIL — Kronecker apply choreography is wrong" : "PASS — LoKr factor apply is viable on this backend"); backend_release(bp.backend, bp.cpu_backend); return fails ? 1 : 0; } static void usage(const char * prog) { AceSynthParams d; ace_synth_default_params(&d); fprintf(stderr, "acestep.cpp %s\n\n", ACE_VERSION); fprintf(stderr, "Usage: %s --models --request [options]\n\n" "Required:\n" " --models Directory of GGUF model files\n" " --request One or more request JSONs (from ace-lm --request)\n\n" "Optional:\n" " --adapters Directory of adapter files (enables JSON adapter field)\n" " --src-audio Source audio (WAV or MP3)\n" " --ref-audio Timbre reference audio (WAV or MP3)\n\n" "Model selection comes from the request JSON: synth_model picks the DiT,\n" "adapter picks an adapter from --adapters, output_format picks the output\n" "extension. When synth_model is empty the first DiT in the registry is used;\n" "text-encoder and VAE are always the first in their registry bucket.\n\n" "Audio encoding:\n" " --mp3-bitrate MP3 bitrate (default: 128)\n\n" "Memory control:\n" " --vae-chunk Latent frames per tile (default: %d)\n" " --vae-overlap Overlap frames per side (default: %d)\n\n" "Debug:\n" " --no-fa Disable flash attention\n" " --no-batch-cfg Split DiT CFG into two separate forwards\n" " --clamp-fp16 Clamp hidden states to FP16 range\n" " --dump Dump intermediate tensors\n", prog, d.vae_chunk, d.vae_overlap); } int main(int argc, char ** argv) { if (std::getenv("HOTSTEP_BCAST_TEST")) { return run_bcast_selftest(); } if (std::getenv("HOTSTEP_KRON_TEST")) { return run_kron_selftest(); } if (argc < 2) { usage(argv[0]); return 1; } // Load solver/scheduler/guidance Lua plugins. The fork routes ALL // sampling through hot-step-sampler.h, which resolves solvers from the // plugin registry — without this init the standalone CLI had no solvers // at all ("unknown solver 'euler'") and crashed in the fallback path. // Same exe-relative resolution as hot-step-server.cpp. { std::filesystem::path exe_path = std::filesystem::canonical(argv[0]); std::filesystem::path exe_dir = exe_path.parent_path(); std::string dir_name = exe_dir.filename().string(); std::filesystem::path engine_dir; if (dir_name == "Release" || dir_name == "Debug" || dir_name == "RelWithDebInfo" || dir_name == "MinSizeRel") { engine_dir = exe_dir.parent_path().parent_path(); } else if (dir_name == "build") { engine_dir = exe_dir.parent_path(); } else { engine_dir = exe_dir; } std::filesystem::path project_dir = engine_dir.parent_path(); PluginRegistry::instance().init(engine_dir.string(), project_dir.string()); } // Defaults live in ace_synth_default_params. CLI locals read from params // so there is exactly one place in the codebase that picks the numbers. AceSynthParams params; ace_synth_default_params(¶ms); std::vector request_paths; const char * models_dir = NULL; const char * adapters_dir = NULL; const char * src_audio_path = NULL; const char * ref_audio_path = NULL; const char * dump_dir = NULL; bool use_fa = true; bool use_batch_cfg = true; bool clamp_fp16 = false; int vae_chunk = params.vae_chunk; int vae_overlap = params.vae_overlap; int mp3_kbps = 128; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--request")) { // Collect all following non-option args while (i + 1 < argc && argv[i + 1][0] != '-') { request_paths.push_back(argv[++i]); } } else if (!strcmp(argv[i], "--models") && i + 1 < argc) { models_dir = argv[++i]; } else if (!strcmp(argv[i], "--adapters") && i + 1 < argc) { adapters_dir = argv[++i]; } else if (!strcmp(argv[i], "--src-audio") && i + 1 < argc) { src_audio_path = argv[++i]; } else if (!strcmp(argv[i], "--ref-audio") && i + 1 < argc) { ref_audio_path = argv[++i]; } else if (!strcmp(argv[i], "--dump") && i + 1 < argc) { dump_dir = argv[++i]; } else if (!strcmp(argv[i], "--no-fa")) { use_fa = false; } else if (!strcmp(argv[i], "--no-batch-cfg")) { use_batch_cfg = false; } else if (!strcmp(argv[i], "--clamp-fp16")) { clamp_fp16 = true; } else if (!strcmp(argv[i], "--vae-chunk") && i + 1 < argc) { vae_chunk = atoi(argv[++i]); } else if (!strcmp(argv[i], "--vae-overlap") && i + 1 < argc) { vae_overlap = atoi(argv[++i]); } else if (!strcmp(argv[i], "--mp3-bitrate") && i + 1 < argc) { mp3_kbps = atoi(argv[++i]); } else if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) { usage(argv[0]); return 0; } else { fprintf(stderr, "Unknown option: %s\n", argv[i]); usage(argv[0]); return 1; } } if (!models_dir) { fprintf(stderr, "[CLI] ERROR: --models required\n"); usage(argv[0]); return 1; } if (request_paths.empty()) { fprintf(stderr, "[CLI] ERROR: --request required\n"); usage(argv[0]); return 1; } // Parse all requests first: the first request drives model selection. int batch_n = (int) request_paths.size(); std::vector reqs(batch_n); std::vector basenames(batch_n); for (int ri = 0; ri < batch_n; ri++) { const char * rpath = request_paths[ri]; if (!request_parse(&reqs[ri], rpath)) { fprintf(stderr, "[Ace-Synth] FATAL: failed to parse %s\n", rpath); return 1; } request_dump(&reqs[ri], stderr); if (reqs[ri].caption.empty() && reqs[ri].task_type != TASK_LEGO && reqs[ri].task_type != TASK_EXTRACT && reqs[ri].task_type != TASK_COMPLETE) { fprintf(stderr, "[Ace-Synth] FATAL: caption is empty in %s\n", rpath); return 1; } // output basename: strip .json suffix basenames[ri] = rpath; size_t dot = basenames[ri].rfind(".json"); if (dot != std::string::npos) { basenames[ri] = basenames[ri].substr(0, dot); } } fprintf(stderr, "[Ace-Synth] Batch: %d request(s)\n", batch_n); // Scan the registry and resolve model paths from the first request. ModelRegistry registry; if (!registry_scan(®istry, models_dir)) { fprintf(stderr, "[Ace-Synth] FATAL: cannot scan --models %s\n", models_dir); return 1; } if (adapters_dir) { registry_scan_adapters(®istry, adapters_dir); } if (registry.dit.empty() || registry.text_enc.empty() || registry.vae.empty()) { fprintf(stderr, "[Ace-Synth] FATAL: registry needs DiT, text-encoder and VAE models\n"); return 1; } const ModelEntry * dit_entry = reqs[0].synth_model.empty() ? ®istry.dit[0] : registry_find(registry.dit, reqs[0].synth_model.c_str()); if (!dit_entry) { fprintf(stderr, "[Ace-Synth] FATAL: synth_model '%s' not found in registry\n", reqs[0].synth_model.c_str()); return 1; } const AdapterEntry * adapter_entry = NULL; if (!reqs[0].adapter.empty()) { adapter_entry = registry_find_adapter(registry, reqs[0].adapter.c_str()); if (!adapter_entry) { fprintf(stderr, "[Ace-Synth] FATAL: adapter '%s' not found (use --adapters )\n", reqs[0].adapter.c_str()); return 1; } } // Multi-adapter stack: the `adapters` array supersedes the single `adapter` // field. Fold the single field into a one-element stack so the load path is // uniform. The resolved stack drives merge/runtime loading via the sideband. g_hotstep_params.adapters.clear(); { std::vector stack = reqs[0].adapters; if (stack.empty() && adapter_entry) { stack.push_back({ reqs[0].adapter, reqs[0].adapter_scale }); } for (const auto & ar : stack) { const AdapterEntry * e = registry_find_adapter(registry, ar.name.c_str()); std::string path; if (e) { path = e->path; } else { FILE * t = fopen(ar.name.c_str(), "rb"); if (t) { fclose(t); path = ar.name; } } if (path.empty()) { fprintf(stderr, "[Ace-Synth] FATAL: adapter '%s' not found (use --adapters )\n", ar.name.c_str()); return 1; } g_hotstep_params.adapters.push_back({ path, ar.scale }); } } // Resolve output_format to (is_mp3, wav_fmt). bool is_mp3 = true; WavFormat wav_fmt = WAV_S16; if (!audio_parse_format(reqs[0].output_format.c_str(), is_mp3, wav_fmt)) { fprintf(stderr, "[Ace-Synth] FATAL: invalid output_format '%s' (use: mp3, wav16, wav24, wav32)\n", reqs[0].output_format.c_str()); return 1; } // Fill params from registry lookups and CLI flags. params.text_encoder_path = registry.text_enc[0].path.c_str(); params.dit_path = dit_entry->path.c_str(); params.vae_path = registry.vae[0].path.c_str(); params.adapter_path = g_hotstep_params.adapters.empty() ? NULL : g_hotstep_params.adapters[0].path.c_str(); params.adapter_scale = g_hotstep_params.adapters.empty() ? 1.0f : g_hotstep_params.adapters[0].scale; params.use_fa = use_fa; params.use_batch_cfg = use_batch_cfg; params.clamp_fp16 = clamp_fp16; params.vae_chunk = vae_chunk; params.vae_overlap = vae_overlap; params.dump_dir = dump_dir; // Local store with the default STRICT policy: at most one GPU module // resident at a time for this one-shot CLI. No module sharing across runs, // so EVICT_STRICT frees the DiT before the VAE loads, and so on. ModelStore * store = store_create(EVICT_STRICT); AceSynth * ctx = ace_synth_load(store, ¶ms); if (!ctx) { store_free(store); return 1; } // Read source audio (cover/lego mode) float * src_interleaved = NULL; int src_len = 0; if (src_audio_path) { int T_audio = 0; float * planar = audio_read_48k(src_audio_path, &T_audio); if (!planar) { fprintf(stderr, "[Ace-Synth] FATAL: cannot read --src-audio %s\n", src_audio_path); ace_synth_free(ctx); store_free(store); return 1; } fprintf(stderr, "[Ace-Synth] Source audio: %.2fs @ 48kHz\n", (float) T_audio / 48000.0f); src_interleaved = audio_planar_to_interleaved(planar, T_audio); free(planar); src_len = T_audio; } // Read reference audio (timbre conditioning) float * ref_interleaved = NULL; int ref_len = 0; if (ref_audio_path) { int T_audio = 0; float * planar = audio_read_48k(ref_audio_path, &T_audio); if (!planar) { fprintf(stderr, "[Ace-Synth] FATAL: cannot read --ref-audio %s\n", ref_audio_path); free(src_interleaved); ace_synth_free(ctx); store_free(store); return 1; } fprintf(stderr, "[Ace-Synth] Reference audio: %.2fs @ 48kHz\n", (float) T_audio / 48000.0f); ref_interleaved = audio_planar_to_interleaved(planar, T_audio); free(planar); ref_len = T_audio; } // Generate every request in one DiT batch. synth_batch_size expands each // request into per-seed variants in groups[0]. Total clamped to DiT max 9. int total_alloc = 0; for (int ri = 0; ri < batch_n; ri++) { int sbs = reqs[ri].synth_batch_size; total_alloc += sbs < 1 ? 1 : (sbs > 9 ? 9 : sbs); } if (total_alloc > 9) { fprintf(stderr, "[Ace-Synth] Batch %d exceeds DiT max 9, clamping\n", total_alloc); total_alloc = 9; } std::vector all_audio(total_alloc); std::vector all_basenames(total_alloc); std::vector all_synth_indices(total_alloc); std::vector> groups(1); groups[0].reserve(total_alloc); int off = 0; for (int ri = 0; ri < batch_n && off < total_alloc; ri++) { int sbs = reqs[ri].synth_batch_size; if (sbs < 1) { sbs = 1; } if (sbs > 9) { sbs = 9; } if (off + sbs > total_alloc) { sbs = total_alloc - off; } // resolve seed once per original request request_resolve_seed(&reqs[ri]); const long long base_seed = reqs[ri].seed; for (int i = 0; i < sbs; i++) { AceRequest r = reqs[ri]; r.seed = base_seed + i; groups[0].push_back(r); all_basenames[off + i] = basenames[ri]; all_synth_indices[off + i] = i; } off += sbs; } if (total_alloc > 1) { fprintf(stderr, "[Ace-Synth] Batch: %d track(s) from %d request(s)\n", total_alloc, batch_n); } // Two-phase run: DiT resident for all groups, then VAE for all jobs. const int rc = synth_batch_run(ctx, groups, src_interleaved, src_len, nullptr, 0, // src_latents ref_interleaved, ref_len, nullptr, 0, // ref_latents all_audio.data()); if (rc != 0) { fprintf(stderr, "[Ace-Synth] ERROR: batch run failed\n"); for (auto & a : all_audio) { ace_audio_free(&a); } free(src_interleaved); free(ref_interleaved); ace_synth_free(ctx); store_free(store); return 1; } // Write output files for (int b = 0; b < (int) all_audio.size(); b++) { if (!all_audio[b].samples) { continue; } const char * ext = is_mp3 ? ".mp3" : ".wav"; char out_path[1024]; snprintf(out_path, sizeof(out_path), "%s%d%s", all_basenames[b].c_str(), all_synth_indices[b], ext); if (!audio_write(out_path, all_audio[b].samples, all_audio[b].n_samples, 48000, mp3_kbps, wav_fmt)) { fprintf(stderr, "[Ace-Synth Batch%d] FATAL: failed to write %s\n", b, out_path); } ace_audio_free(&all_audio[b]); } free(src_interleaved); free(ref_interleaved); ace_synth_free(ctx); store_free(store); fprintf(stderr, "[Ace-Synth] All done\n"); return 0; }