// Latent ("soft") thinking for LFM3 on llama.cpp, with continuous batching. // // Every input (prompt, thinking, answer) is fed as an embedding vector, so all modes share one code path and the // discrete mode is an exact control: // discrete next input = E[sampled token] (control) // soft next input = sum_j p_j E[j] over the top --topk-soft candidates (Soft Thinking) // gumbel next input = sum_j softmax((log p_j + g_j) / tau) E[j], g ~ Gumbel (stochastic soft thinking) // moi next input = sum_j w_j E[j], w = (H p + (beta + 1 - H) onehot(sampled)) / (beta + 1), // H = entropy / log(n_vocab) (Mixture of Inputs) // mti discrete sampling, but where the entropy of the next-token distribution exceeds --tau-mti the logits are // replaced by classifier-free guidance against a negative branch: the context continued by a short // negative cue (--neg, " OUTPUT ERROR") in a scratch copy of the sequence; log p = (1 - w) log p_neg + // w log p, w = --omega (Minimal Test-Time Intervention, ACL 2026). Applied while thinking and answering. // Thinking ends when is the chosen token (sampled for discrete/moi/mti, argmax of the weights for soft/gumbel), // at --budget (forced), or for soft/gumbel when the entropy stays below --cold-tau for --cold-len steps ("cold stop"). // The answer is then sampled discretely in every mode. // Sampling chain as in our llama-server runs: top_k 50 -> top_p 0.95 (temperature 1) -> temperature 0.6. // // --prompts: one line per prompt: id token ids separated by spaces // --out: id think_steps forced cold thinking ids answer ids (appended) // --selftest compares the token path with the embedding path, alone and batched. #include "ggml-backend.h" #include "llama.h" #include #include #include #include #include #include #include #include #include #include #include #include #include static const llama_token THINK_END = 124902; static const llama_token EOS_TOK = 124900; enum Mode { DISCRETE, SOFT, GUMBEL, MOI, MTI }; struct Args { std::string model, embd, prompts, out, mode = "discrete"; int budget = 6144, answer = 768, par = 64, top_k = 50, topk_soft = 15, cold_len = 256, ngl = 99, limit = 0; float top_p = 0.95f, temp = 0.6f, tau = 0.5f, beta = 1.0f, cold_tau = 0.01f, tau_mti = 1.0f, omega = 1.5f; int kvu = -1; // unified KV buffer: -1 = only for mti (cheap scratch copies), 0 / 1 = force std::vector neg; unsigned long long seed = 0; bool selftest = false; bool tokens = false; // discrete mode only: feed token ids instead of their embeddings (speed check) }; struct Prompt { std::string id; std::vector toks; }; struct Seq { int p = -1; // prompt index, -1 = idle int pos = 0; int phase = 0; // 0 thinking, 1 answer, 2 finished bool prefill = false; int think_steps = 0, low_run = 0; bool forced = false, cold = false; std::vector think, answer; std::vector next; // next input embedding llama_token next_tok = -1; // the same input as a token id, when it is one token std::mt19937_64 rng; int out = -1; // index of its logits in the batch // MTI: guidance at this step, index of the negative branch's logits, conditional top-k ids and log-probabilities bool cfg = false; int out2 = -1; std::vector cid; std::vector clp; }; static int n_embd = 0; static int n_vocab = 0; static std::vector E; // n_vocab x n_embd static const float * row_ptr(llama_token t) { return &E[(size_t) t * n_embd]; } static void set_row(llama_token t, std::vector & out) { out.assign(row_ptr(t), row_ptr(t) + n_embd); } static void mix(const std::vector & ids, const std::vector & w, size_t k, std::vector & out) { out.assign(n_embd, 0.0f); for (size_t j = 0; j < k; j++) { if (w[j] == 0.0f) continue; const float * e = row_ptr(ids[j]); for (int d = 0; d < n_embd; d++) out[d] += w[j] * e[d]; } } // top_k -> top_p (temperature 1) -> temperature; ids sorted by probability, probabilities renormalised static void filtered(const float * logits, const Args & a, std::vector & ids, std::vector & ps) { std::vector> h; h.reserve(a.top_k + 1); auto gt = [](const std::pair & x, const std::pair & y) { return x.first > y.first; }; for (int i = 0; i < n_vocab; i++) { const float v = logits[i]; if ((int) h.size() < a.top_k) { h.push_back({ v, i }); std::push_heap(h.begin(), h.end(), gt); } else if (v > h.front().first) { std::pop_heap(h.begin(), h.end(), gt); h.back() = { v, i }; std::push_heap(h.begin(), h.end(), gt); } } std::sort(h.begin(), h.end(), gt); const double mx = h[0].first; std::vector p1(h.size()); double s = 0; for (size_t i = 0; i < h.size(); i++) s += (p1[i] = std::exp(h[i].first - mx)); size_t keep = 0; double cum = 0; for (size_t i = 0; i < h.size(); i++) { keep = i + 1; cum += p1[i] / s; if (cum >= a.top_p) break; } ids.resize(keep); ps.resize(keep); double z = 0; for (size_t i = 0; i < keep; i++) { ids[i] = h[i].second; z += (p1[i] = std::exp((h[i].first - mx) / a.temp)); } for (size_t i = 0; i < keep; i++) ps[i] = (float) (p1[i] / z); } static void step(Seq & s, const float * logits, const Args & a, Mode mode) { std::vector ids; std::vector ps; filtered(logits, a, ids, ps); std::uniform_real_distribution U(0.0, 1.0); auto sample = [&]() { const double u = U(s.rng); double c = 0; for (size_t j = 0; j < ps.size(); j++) { c += ps[j]; if (u < c) return ids[j]; } return ids.back(); }; if (s.phase == 1) { // answer: always discrete const llama_token t = sample(); if (t == EOS_TOK || (int) s.answer.size() >= a.answer) { s.phase = 2; return; } s.answer.push_back(t); set_row(t, s.next); s.next_tok = t; return; } llama_token chosen; std::vector w; size_t k = 0; if (mode == DISCRETE || mode == MTI) { chosen = sample(); } else if (mode == MOI) { chosen = sample(); double H = 0; for (float p : ps) if (p > 0) H -= p * std::log((double) p); H /= std::log((double) n_vocab); k = ps.size(); w.resize(k); for (size_t j = 0; j < k; j++) w[j] = (float) ((H * ps[j] + (ids[j] == chosen ? a.beta + 1.0 - H : 0.0)) / (a.beta + 1.0)); } else { k = std::min((size_t) a.topk_soft, ps.size()); w.assign(ps.begin(), ps.begin() + k); if (mode == GUMBEL) { std::vector z(k); double mz = -1e300; for (size_t j = 0; j < k; j++) { const double g = -std::log(-std::log(std::max(U(s.rng), 1e-300))); z[j] = (std::log(std::max((double) w[j], 1e-30)) + g) / a.tau; mz = std::max(mz, z[j]); } double sum = 0; for (size_t j = 0; j < k; j++) sum += (z[j] = std::exp(z[j] - mz)); for (size_t j = 0; j < k; j++) w[j] = (float) (z[j] / sum); } else { double sum = 0; for (size_t j = 0; j < k; j++) sum += w[j]; for (size_t j = 0; j < k; j++) w[j] = (float) (w[j] / sum); } chosen = ids[std::max_element(w.begin(), w.end()) - w.begin()]; double H = 0; for (float p : ps) if (p > 0) H -= p * std::log((double) p); s.low_run = H < a.cold_tau ? s.low_run + 1 : 0; } bool end = chosen == THINK_END; if (!end && s.think_steps >= a.budget) end = s.forced = true; if (!end && (mode == SOFT || mode == GUMBEL) && s.low_run >= a.cold_len) end = s.cold = true; if (end) { s.phase = 1; set_row(THINK_END, s.next); s.next_tok = THINK_END; return; } s.think_steps++; s.think.push_back(chosen); if (mode == DISCRETE || mode == MTI) set_row(chosen, s.next), s.next_tok = chosen; else mix(ids, w, k, s.next), s.next_tok = -1; } // entropy (nats) of softmax(logits) over the whole vocabulary static double entropy(const float * l) { double m = l[0]; for (int i = 1; i < n_vocab; i++) m = std::max(m, (double) l[i]); double z = 0, s = 0; for (int i = 0; i < n_vocab; i++) { const double e = std::exp(l[i] - m); z += e; s += e * (l[i] - m); } return std::log(z) - s / z; } static double logsumexp(const float * l) { double m = l[0]; for (int i = 1; i < n_vocab; i++) m = std::max(m, (double) l[i]); double z = 0; for (int i = 0; i < n_vocab; i++) z += std::exp(l[i] - m); return m + std::log(z); } // conditional top-k candidates and their log-probabilities (guidance is applied among the plausible candidates only) static void top_logprobs(const float * l, int k, std::vector & ids, std::vector & lp) { std::vector> h; auto gt = [](const std::pair & x, const std::pair & y) { return x.first > y.first; }; for (int i = 0; i < n_vocab; i++) { if ((int) h.size() < k) { h.push_back({ l[i], i }); std::push_heap(h.begin(), h.end(), gt); } else if (l[i] > h.front().first) { std::pop_heap(h.begin(), h.end(), gt); h.back() = { l[i], i }; std::push_heap(h.begin(), h.end(), gt); } } const double lse = logsumexp(l); ids.resize(h.size()); lp.resize(h.size()); for (size_t j = 0; j < h.size(); j++) ids[j] = h[j].second, lp[j] = (float) (h[j].first - lse); } static unsigned long long fnv(const std::string & s) { unsigned long long h = 1469598103934665603ULL; for (unsigned char c : s) h = (h ^ c) * 1099511628211ULL; return h; } static void add_embd(llama_batch & b, const float * e, llama_pos pos, llama_seq_id sq, bool out) { const int i = b.n_tokens++; std::memcpy(b.embd + (size_t) i * n_embd, e, sizeof(float) * n_embd); b.pos[i] = pos; b.n_seq_id[i] = 1; b.seq_id[i][0] = sq; b.logits[i] = out; } static Args parse(int argc, char ** argv) { Args a; for (int i = 1; i < argc; i++) { std::string k = argv[i]; auto v = [&]() { return std::string(argv[++i]); }; if (k == "--model") a.model = v(); else if (k == "--embd") a.embd = v(); else if (k == "--prompts") a.prompts = v(); else if (k == "--out") a.out = v(); else if (k == "--mode") a.mode = v(); else if (k == "--budget") a.budget = std::stoi(v()); else if (k == "--answer") a.answer = std::stoi(v()); else if (k == "--par") a.par = std::stoi(v()); else if (k == "--topk-soft") a.topk_soft = std::stoi(v()); else if (k == "--cold-len") a.cold_len = std::stoi(v()); else if (k == "--cold-tau") a.cold_tau = std::stof(v()); else if (k == "--tau") a.tau = std::stof(v()); else if (k == "--beta") a.beta = std::stof(v()); else if (k == "--tau-mti") a.tau_mti = std::stof(v()); else if (k == "--omega") a.omega = std::stof(v()); else if (k == "--kvu") a.kvu = std::stoi(v()); else if (k == "--neg") { std::istringstream ss(v()); int t; while (ss >> t) a.neg.push_back(t); } else if (k == "--seed") a.seed = std::stoull(v()); else if (k == "--ngl") a.ngl = std::stoi(v()); else if (k == "--limit") a.limit = std::stoi(v()); else if (k == "--selftest") a.selftest = true; else if (k == "--tokens") a.tokens = true; else { fprintf(stderr, "unknown argument %s\n", k.c_str()); exit(1); } } return a; } static std::vector read_prompts(const std::string & path, int limit, const std::set & skip) { std::vector P; std::ifstream f(path); std::string line; while (std::getline(f, line)) { const size_t tab = line.find('\t'); if (tab == std::string::npos) continue; Prompt p; p.id = line.substr(0, tab); std::istringstream ss(line.substr(tab + 1)); int t; while (ss >> t) p.toks.push_back(t); if (limit && (int) P.size() + (int) skip.size() >= limit) break; if (!skip.count(p.id)) P.push_back(p); } return P; } static std::vector logits_copy(llama_context * ctx, int i) { const float * l = llama_get_logits_ith(ctx, i); return std::vector(l, l + n_vocab); } // KL(p_x || p_y) at temperature 1 (the logit noise that matters for sampling) static float maxdiff(const std::vector & x, const std::vector & y) { const double mx = *std::max_element(x.begin(), x.end()), my = *std::max_element(y.begin(), y.end()); double zx = 0, zy = 0; for (size_t i = 0; i < x.size(); i++) zx += std::exp(x[i] - mx), zy += std::exp(y[i] - my); double kl = 0; for (size_t i = 0; i < x.size(); i++) { const double lpx = x[i] - mx - std::log(zx), lpy = y[i] - my - std::log(zy); kl += std::exp(lpx) * (lpx - lpy); } return (float) kl; } static int selftest(llama_context * ctx, const std::vector & P) { llama_memory_t mem = llama_get_memory(ctx); const auto & t0 = P[0].toks; const auto & t1 = P[1].toks; // A: token path, sequence 0 alone llama_batch bt = llama_batch_init((int) t0.size() + 8, 0, 1); bt.n_tokens = 0; for (size_t j = 0; j < t0.size(); j++) { const int i = bt.n_tokens++; bt.token[i] = t0[j], bt.pos[i] = (llama_pos) j, bt.n_seq_id[i] = 1, bt.seq_id[i][0] = 0; bt.logits[i] = j + 1 == t0.size(); } if (llama_decode(ctx, bt)) return fprintf(stderr, "decode A failed\n"), 1; auto A = logits_copy(ctx, bt.n_tokens - 1); // greedy continuation, token path std::vector> contA; std::vector cont; llama_token g = (llama_token) (std::max_element(A.begin(), A.end()) - A.begin()); for (int s = 0; s < 6; s++) { cont.push_back(g); bt.n_tokens = 1; bt.token[0] = g, bt.pos[0] = (llama_pos) (t0.size() + s), bt.n_seq_id[0] = 1, bt.seq_id[0][0] = 0, bt.logits[0] = 1; if (llama_decode(ctx, bt)) return fprintf(stderr, "decode A+ failed\n"), 1; contA.push_back(logits_copy(ctx, 0)); g = (llama_token) (std::max_element(contA.back().begin(), contA.back().end()) - contA.back().begin()); } llama_memory_clear(mem, true); // B: embedding path, sequence 0 alone llama_batch be = llama_batch_init((int) (t0.size() + t1.size()) + 8, n_embd, 1); be.n_tokens = 0; for (size_t j = 0; j < t0.size(); j++) add_embd(be, row_ptr(t0[j]), (llama_pos) j, 0, j + 1 == t0.size()); if (llama_decode(ctx, be)) return fprintf(stderr, "decode B failed\n"), 1; auto B = logits_copy(ctx, be.n_tokens - 1); llama_memory_clear(mem, true); // C: embedding path, sequences 0 and 1 in one batch, then 6 steps together be.n_tokens = 0; for (size_t j = 0; j < t1.size(); j++) add_embd(be, row_ptr(t1[j]), (llama_pos) j, 1, j + 1 == t1.size()); for (size_t j = 0; j < t0.size(); j++) add_embd(be, row_ptr(t0[j]), (llama_pos) j, 0, j + 1 == t0.size()); if (llama_decode(ctx, be)) return fprintf(stderr, "decode C failed\n"), 1; auto C = logits_copy(ctx, be.n_tokens - 1); float dc = 0; for (int s = 0; s < 6; s++) { be.n_tokens = 0; add_embd(be, row_ptr(cont[s]), (llama_pos) (t1.size() + s), 1, true); add_embd(be, row_ptr(cont[s]), (llama_pos) (t0.size() + s), 0, true); if (llama_decode(ctx, be)) return fprintf(stderr, "decode C+ failed\n"), 1; dc = std::max(dc, maxdiff(contA[s], logits_copy(ctx, 1))); } float scale = 0; for (float x : A) scale = std::max(scale, std::fabs(x)); printf("KL divergence (nats), logit scale %.2f | prefill: token vs embedding %.4g, alone vs batched %.4g | 6 decode " "steps, token alone vs embedding batched (max) %.4g\n", scale, maxdiff(A, B), maxdiff(A, C), dc); llama_memory_clear(mem, true); return 0; } int main(int argc, char ** argv) { Args a = parse(argc, argv); const Mode mode = a.mode == "soft" ? SOFT : a.mode == "gumbel" ? GUMBEL : a.mode == "moi" ? MOI : a.mode == "mti" ? MTI : DISCRETE; if (a.mode != "soft" && a.mode != "gumbel" && a.mode != "moi" && a.mode != "mti" && a.mode != "discrete") { fprintf(stderr, "unknown mode\n"); return 1; } if (mode == MTI && a.neg.empty()) { fprintf(stderr, "mti needs --neg token ids\n"); return 1; } std::set done; if (!a.selftest && !a.out.empty()) { std::ifstream f(a.out); std::string line; while (std::getline(f, line)) done.insert(line.substr(0, line.find('\t'))); } std::vector P = read_prompts(a.prompts, a.limit, done); std::sort(P.begin(), P.end(), [](const Prompt & x, const Prompt & y) { return x.toks.size() > y.toks.size(); }); if (P.empty()) { printf("nothing to do\n"); return 0; } size_t maxlen = 0; for (auto & p : P) maxlen = std::max(maxlen, p.toks.size()); llama_backend_init(); ggml_backend_load_all(); auto mp = llama_model_default_params(); mp.n_gpu_layers = a.ngl; llama_model * model = llama_model_load_from_file(a.model.c_str(), mp); if (!model) return fprintf(stderr, "model load failed\n"), 1; n_vocab = llama_vocab_n_tokens(llama_model_get_vocab(model)); n_embd = llama_model_n_embd(model); { std::ifstream f(a.embd, std::ios::binary | std::ios::ate); const size_t bytes = (size_t) f.tellg(); if (bytes < (size_t) n_vocab * n_embd * sizeof(float)) return fprintf(stderr, "embedding file too small: %zu bytes for %d x %d\n", bytes, n_vocab, n_embd), 1; f.seekg(0); E.resize((size_t) n_vocab * n_embd); f.read((char *) E.data(), E.size() * sizeof(float)); } const int n_batch = 2048 + a.par; auto cp = llama_context_default_params(); cp.n_seq_max = mode == MTI ? 2 * a.par : a.par; // MTI: scratch copy par + i of sequence i cp.n_ctx = (uint32_t) (a.par * (a.budget + a.answer + maxlen + 16 + a.neg.size())); cp.n_batch = std::max(n_batch, (int) maxlen + a.par); cp.n_ubatch = 512; cp.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_ENABLED; // a unified buffer makes every sequence attend over all cells (masked): slow for many independent sequences cp.kv_unified = a.kvu < 0 ? mode == MTI : a.kvu == 1; cp.n_threads = 8; cp.n_threads_batch = 8; cp.no_perf = true; llama_context * ctx = llama_init_from_model(model, cp); if (!ctx) return fprintf(stderr, "context failed\n"), 1; printf("model n_vocab %d n_embd %d | %zu prompts, mode %s, par %d, n_ctx %u\n", n_vocab, n_embd, P.size(), a.mode.c_str(), a.par, cp.n_ctx); fflush(stdout); if (a.selftest) return selftest(ctx, P); llama_memory_t mem = llama_get_memory(ctx); llama_batch b = llama_batch_init(cp.n_batch, n_embd, 1); llama_batch b2 = llama_batch_init(a.par * (int) a.neg.size() + 1, n_embd, 1); llama_batch bt = llama_batch_init(cp.n_batch, 0, 1); std::vector S(a.par); FILE * fo = fopen(a.out.c_str(), "ab"); size_t next_p = 0, n_done = 0; long long n_fed = 0, n_steps = 0, n_cfg = 0, n_seqsteps = 0; const auto t0 = std::chrono::steady_clock::now(); std::vector L(a.par); double t_dec = 0, t_smp = 0; auto now = [] { return std::chrono::steady_clock::now(); }; auto ms = [](auto x, auto y) { return std::chrono::duration(y - x).count(); }; while (true) { int room = 2048; // prompt tokens admitted per step for (int i = 0; i < a.par && next_p < P.size(); i++) { if (S[i].p >= 0) continue; const int n = (int) P[next_p].toks.size(); if (n > room && room < 2048) break; S[i] = Seq(); S[i].p = (int) next_p; S[i].prefill = true; S[i].rng.seed(a.seed * 1000003ULL + fnv(P[next_p].id)); room -= n; next_p++; } const bool use_tok = a.tokens && mode == DISCRETE; b.n_tokens = bt.n_tokens = 0; auto add_tok = [&](llama_token t, llama_pos pos, llama_seq_id sq, bool out) { const int k = bt.n_tokens++; bt.token[k] = t, bt.pos[k] = pos, bt.n_seq_id[k] = 1, bt.seq_id[k][0] = sq, bt.logits[k] = out; }; for (int i = 0; i < a.par; i++) { Seq & s = S[i]; s.out = -1; if (s.p < 0) continue; if (s.prefill) { const auto & t = P[s.p].toks; for (size_t j = 0; j < t.size(); j++) if (use_tok) add_tok(t[j], (llama_pos) j, i, j + 1 == t.size()); else add_embd(b, row_ptr(t[j]), (llama_pos) j, i, j + 1 == t.size()); s.pos = (int) t.size(); s.prefill = false; } else if (use_tok) { add_tok(s.next_tok, s.pos++, i, true); } else { add_embd(b, s.next.data(), s.pos++, i, true); } s.out = (use_tok ? bt.n_tokens : b.n_tokens) - 1; } if (use_tok) b.n_tokens = bt.n_tokens; if (b.n_tokens == 0) break; n_fed += b.n_tokens; for (int i = 0; i < a.par; i++) n_seqsteps += S[i].out >= 0; auto tA = now(); const int rc = llama_decode(ctx, use_tok ? bt : b); if (rc != 0) return fprintf(stderr, "llama_decode failed: %d\n", rc), 1; for (int i = 0; i < a.par; i++) L[i] = S[i].out >= 0 ? llama_get_logits_ith(ctx, S[i].out) : nullptr; auto tB = now(); t_dec += ms(tA, tB); if (mode == MTI) { #pragma omp parallel for schedule(dynamic) for (int i = 0; i < a.par; i++) { S[i].cfg = L[i] && entropy(L[i]) > a.tau_mti; if (S[i].cfg) top_logprobs(L[i], a.top_k, S[i].cid, S[i].clp); else if (L[i]) step(S[i], L[i], a, mode); } b2.n_tokens = 0; for (int i = 0; i < a.par; i++) { if (!S[i].cfg) continue; llama_memory_seq_cp(mem, i, a.par + i, -1, -1); for (size_t j = 0; j < a.neg.size(); j++) add_embd(b2, row_ptr(a.neg[j]), (llama_pos) (S[i].pos + j), a.par + i, j + 1 == a.neg.size()); S[i].out2 = b2.n_tokens - 1; n_cfg++; } if (b2.n_tokens) { const int rc2 = llama_decode(ctx, b2); if (rc2 != 0) return fprintf(stderr, "llama_decode (negative branch) failed: %d\n", rc2), 1; for (int i = 0; i < a.par; i++) L[i] = S[i].cfg ? llama_get_logits_ith(ctx, S[i].out2) : nullptr; #pragma omp parallel for schedule(dynamic) for (int i = 0; i < a.par; i++) { if (!S[i].cfg) continue; const double lse = logsumexp(L[i]); std::vector g(n_vocab, -1e30f); for (size_t j = 0; j < S[i].cid.size(); j++) { const int t = S[i].cid[j]; g[t] = (float) ((1.0 - a.omega) * (L[i][t] - lse) + a.omega * S[i].clp[j]); } step(S[i], g.data(), a, mode); } for (int i = 0; i < a.par; i++) if (S[i].cfg) llama_memory_seq_rm(mem, a.par + i, -1, -1); } } else { #pragma omp parallel for schedule(dynamic) for (int i = 0; i < a.par; i++) if (L[i]) step(S[i], L[i], a, mode); } t_smp += ms(tB, now()); for (int i = 0; i < a.par; i++) { Seq & s = S[i]; if (s.p < 0 || s.phase != 2) continue; fprintf(fo, "%s\t%d\t%d\t%d\t", P[s.p].id.c_str(), s.think_steps, (int) s.forced, (int) s.cold); for (size_t j = 0; j < s.think.size(); j++) fprintf(fo, j ? " %d" : "%d", s.think[j]); fprintf(fo, "\t"); for (size_t j = 0; j < s.answer.size(); j++) fprintf(fo, j ? " %d" : "%d", s.answer[j]); fprintf(fo, "\n"); fflush(fo); llama_memory_seq_rm(mem, i, -1, -1); s.p = -1; n_done++; } if (++n_steps % 200 == 0) { const double el = std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); fprintf(stderr, "step %lld: %zu/%zu done %.0f tok/s %.1f min guidance at %.1f%% of steps (decode %.1f ms, " "sampling %.1f ms per step)\n", n_steps, n_done, P.size(), n_fed / el, el / 60, 100.0 * n_cfg / std::max(1LL, n_seqsteps), t_dec / n_steps, t_smp / n_steps); } } const double el = std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); printf("%zu/%zu done %.0f tok/s %.1f min guidance at %.1f%% of steps\n", n_done, P.size(), n_fed / el, el / 60, 100.0 * n_cfg / std::max(1LL, n_seqsteps)); fclose(fo); llama_batch_free(b); llama_batch_free(b2); llama_free(ctx); llama_model_free(model); return 0; }