ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
api.cpp
Go to the documentation of this file.
1#include "api.h"
2#include "chat_gguf.h"
3#include "draft_table.h"
4#include "gguf_parser.h"
5#include "model_macros.h"
6
7#include <algorithm>
8#include <chrono>
9#include <cmath>
10#include <cstdint>
11#include <filesystem>
12#include <fstream>
13#include <functional>
14#include <iostream>
15#include <map>
16#include <memory>
17#include <mutex>
18#include <nlohmann/json.hpp>
19
20#include "graph_ir.h"
21#include <numeric>
22#include <random>
23#include <type_traits>
24#include <variant>
25#include <stdexcept>
26#include <string>
27#include <vector>
28#include <iomanip>
29#ifdef HAS_CUDA
30#include <nvtx3/nvToolsExt.h> // header-only; names the prefill range for nsys (docs/TTFT.md)
31#endif
32#include <sstream>
33
34#include "logger.h"
35#include "model.h"
36#include "safetensors_loader.h"
37#include "tokenizer.h"
38#include "utf8_text.h"
39
40namespace tinyllama {
41
42
43static void log_vector_summary_detailed(const std::string& name,
44 const std::vector<float>& v,
45 int current_pos, int current_layer,
46 int N) {
47 if (v.empty()) {
48 Logger::info(name + " (pos=" + std::to_string(current_pos) + ", layer=" +
49 std::to_string(current_layer) + "): EMPTY VECTOR");
50 return;
51 }
52 std::stringstream ss;
53 ss << name << " (pos=" << std::to_string(current_pos)
54 << ", layer=" << std::to_string(current_layer) << "): size=" << v.size();
55 ss << ", first " << N << ": [";
56 for (int i = 0; i < N && i < v.size(); ++i) {
57 ss << std::fixed << std::setprecision(4) << v[i]
58 << (i == N - 1 || i == v.size() - 1 ? "" : ", ");
59 }
60 ss << "]";
61 float min_val = v[0], max_val = v[0], sum = 0.0f;
62 bool all_finite = true;
63 for (float val : v) {
64 if (val < min_val) min_val = val;
65 if (val > max_val) max_val = val;
66 sum += val;
67 if (!std::isfinite(val)) all_finite = false;
68 }
69 ss << ", min=" << std::fixed << std::setprecision(4) << min_val;
70 ss << ", max=" << std::fixed << std::setprecision(4) << max_val;
71 ss << ", mean=" << std::fixed << std::setprecision(4) << (sum / v.size());
72 ss << ", finite=" << (all_finite ? "yes" : "no");
73 Logger::info(ss.str());
74}
75
76static std::string read_file_api(const std::string& path) {
77 std::filesystem::path fs_path(path);
78 std::ifstream file(fs_path, std::ios::binary);
79 if (!file) throw std::runtime_error("Failed to open file: " + path);
80 return std::string((std::istreambuf_iterator<char>(file)),
81 std::istreambuf_iterator<char>());
82}
83
84static int argmax(const std::vector<float>& v) {
85 if (v.empty()) {
86 Logger::error("Cannot perform argmax on empty vector");
87 return -1;
88 }
89
90 return std::distance(v.begin(), std::max_element(v.begin(), v.end()));
91}
92
93// Prompt-lookup drafting for speculative decoding: the most recent earlier
94// occurrence of the last 3 (then 2) tokens of the history, and up to k of
95// the tokens that followed it. Empty if there is no such match.
96static std::vector<int> prompt_lookup_draft(const std::vector<int>& h, int k) {
97 const int n = static_cast<int>(h.size());
98 for (int g = std::min(3, n - 1); g >= 2; --g) {
99 for (int s = n - g - 1; s >= 0; --s) {
100 if (std::equal(h.begin() + s, h.begin() + s + g, h.end() - g))
101 return std::vector<int>(h.begin() + s + g, h.begin() + std::min(n, s + g + k));
102 }
103 }
104 return {};
105}
106
107// EMBER_DRAFT=table: the corpus draft table EMBER_DRAFT_TABLE (draft_table.h), read once per process, or nullptr
108// (with one warning) when the variable is unset, the file is unreadable or it was built for another vocabulary.
109static const ember_draft::DraftTable* corpus_draft_table(int vocab_size) {
110 static ember_draft::DraftTable table;
111 static bool ok = false;
112 static int vocab_read = 0;
113 static std::once_flag once;
114 std::call_once(once, [&] {
115 const char* path = std::getenv("EMBER_DRAFT_TABLE");
116 std::string why = "EMBER_DRAFT_TABLE is not set";
117 ok = path && table.load(path, vocab_size, &why);
118 vocab_read = vocab_size;
119 if (ok)
120 Logger::info("EMBER_DRAFT=table: " + std::string(path) + ", " + std::to_string(table.entries()) +
121 " entries over orders 1-" + std::to_string(table.orders()));
122 else
123 Logger::warning("EMBER_DRAFT=table: no corpus table (" + why + "); prompt-lookup drafts only.");
124 });
125 return ok && vocab_size == vocab_read ? &table : nullptr;
126}
127
128#ifdef HAS_CUDA
129// What sample_top_k_top_p_temperature(row, temperature, top_k = 1, ...) returns for a verification row, from the
130// row's device record (speculative_verify.cpp), or -1 when the record cannot decide it. Below temperature 0.05 the
131// sampler takes max_element: the first index of the largest logit. Otherwise it keeps the largest (float
132// probability, index) pair: the last index of the largest logit, provided no smaller logit rounds to the same float
133// probability, which holds once the runner-up's scaled logit is below -1e-5 (the two probabilities are then more
134// than two float steps apart). The sampler then draws from its one-element distribution; the same draw is made here,
135// so the session's RNG moves exactly as it would.
136static int greedy_from_pick(const ember_verify::RowPick& p, float temperature, std::mt19937& rng) {
137 if (p.nan || !std::isfinite(p.max) || p.first < 0 || p.last < 0) return -1;
138 if (temperature < 0.05f) return p.first;
139 const float scaled = (p.second - p.max) * (1.0f / temperature);
140 if (!(scaled < -1e-5f)) return -1;
141 const std::vector<float> one(1, 1.0f);
142 std::discrete_distribution<int> dist(one.begin(), one.end());
143 (void)dist(rng);
144 return p.last;
145}
146#endif
147
148// CTRL-style repetition penalty over recently generated tokens. Tiny
149// word-level models fall into short degenerate cycles ("you are very kind."
150// x12, 2026-07-19); penalizing the last window of generated ids breaks the
151// cycle while leaving fresh vocabulary untouched.
152static void apply_repetition_penalty(std::vector<float>& logits,
153 const std::vector<int>& generated,
154 float penalty, size_t window = 64) {
155 if (penalty <= 1.0f || generated.empty()) return;
156 size_t start = generated.size() > window ? generated.size() - window : 0;
157 for (size_t i = start; i < generated.size(); ++i) {
158 int id = generated[i];
159 if (id < 0 || static_cast<size_t>(id) >= logits.size()) continue;
160 float& l = logits[id];
161 l = (l > 0.0f) ? l / penalty : l * penalty;
162 }
163}
164
165// The /chat contract's repetition penalty (LOCAL_LLM_STACK.md section 3, point
166// 4) over the id sequence a + b (prompt, then the reply so far): among its
167// last `last_n` ids, each distinct id outside the stop set has its logit l
168// replaced by l > 0 ? l / p : l * p, once. `seen` is scratch.
169template <class IsStop>
170static void apply_contract_penalty(std::vector<float>& lg, const std::vector<int>& a, const std::vector<int>& b,
171 float p, int last_n, IsStop is_stop, std::vector<char>& seen) {
172 if (p == 1.0f || last_n <= 0) return;
173 const size_t na = a.size(), total = na + b.size();
174 const size_t first = total > static_cast<size_t>(last_n) ? total - last_n : 0;
175 seen.assign(lg.size(), 0);
176 for (size_t i = first; i < total; ++i) {
177 const int id = i < na ? a[i] : b[i - na];
178 if (id < 0 || static_cast<size_t>(id) >= lg.size() || seen[id] || is_stop(id)) continue;
179 seen[id] = 1;
180 float& l = lg[id];
181 l = l > 0.0f ? l / p : l * p;
182 }
183}
184
185// TINYLLAMA_TOPK_DEBUG=1: dump the top-10 candidates (id, logit) at every
186// sampled position to stderr. The decoder-side counterpart of the trainer's
187// [TRACE] dumps — for asking "was the dropped word 2nd by a hair, or absent?"
188static void debug_dump_topk(const std::vector<float>& logits) {
189 // TINYLLAMA_LOGITS_DUMP=<path>: append the FULL logit vector at every
190 // sampled position (one space-joined line each). The engine half of the
191 // golden-batch GGUF-parity test — compare against the trainer's
192 // golden_batch_test --dump rows with compare_golden_logits.py. Nine significant digits, so each
193 // value reads back as the same float32 (bit-for-bit comparisons of two dumps).
194 static const char* dump_path = std::getenv("TINYLLAMA_LOGITS_DUMP");
195 if (dump_path) {
196 std::ofstream f(dump_path, std::ios::app);
197 f << std::setprecision(9);
198 for (size_t i = 0; i < logits.size(); ++i) f << (i ? " " : "") << logits[i];
199 f << "\n";
200 }
201 static const bool on = std::getenv("TINYLLAMA_TOPK_DEBUG") != nullptr;
202 if (!on) return;
203 std::vector<int> idx(logits.size());
204 for (size_t i = 0; i < idx.size(); ++i) idx[i] = static_cast<int>(i);
205 std::partial_sort(idx.begin(), idx.begin() + std::min<size_t>(10, idx.size()),
206 idx.end(), [&](int a, int b) { return logits[a] > logits[b]; });
207 std::ostringstream os;
208 os << "[TOPK]";
209 for (size_t r = 0; r < std::min<size_t>(10, idx.size()); ++r)
210 os << " " << idx[r] << ":" << logits[idx[r]];
211 fprintf(stderr, "%s\n", os.str().c_str());
212}
213
214// Either debug dump above is on: the speculative path then fetches every row it
215// samples from, so the dump holds one line per generated token, as plain decode's.
216static bool logits_debug_on() {
217 static const bool on = std::getenv("TINYLLAMA_LOGITS_DUMP") != nullptr ||
218 std::getenv("TINYLLAMA_TOPK_DEBUG") != nullptr;
219 return on;
220}
221
222static int sample_top_k_top_p_temperature(const std::vector<float>& logits,
223 float temperature, int top_k,
224 float top_p, std::mt19937& rng) {
225 if (logits.empty()) {
226 throw std::runtime_error("Cannot sample from empty logits.");
227 }
228
229 // If temperature is very low, fall back to greedy sampling
230 if (temperature < 0.05f) {
231 return std::distance(logits.begin(), std::max_element(logits.begin(), logits.end()));
232 }
233
234 int vocab_size = logits.size();
235
236 top_k = (std::min)(top_k, vocab_size);
237 if (top_k <= 0) top_k = vocab_size;
238
239 std::vector<float> scaled_logits(vocab_size);
240 float max_logit = -std::numeric_limits<float>::infinity();
241 for (float logit : logits) max_logit = (std::max)(max_logit, logit);
242
243 // Scale logits to avoid numerical instability
244 const float scale = 1.0f / temperature;
245 for (int i = 0; i < vocab_size; ++i) {
246 scaled_logits[i] = (logits[i] - max_logit) * scale;
247 }
248
249 std::vector<double> probs_double(vocab_size);
250 double sum_exp = 0.0;
251 for (int i = 0; i < vocab_size; ++i) {
252 probs_double[i] = std::exp(static_cast<double>(scaled_logits[i]));
253 sum_exp += probs_double[i];
254 }
255
256 // Pure temperature (top_k <= 0 or >= vocab, top_p >= 1): draw from the
257 // whole double-precision distribution, no sort and no float cumulative sum
258 // that can reach 1.0 early and drop the tail. Same distribution as
259 // paperkiln's sample_pure (charter 0001 §4); the RNG stream differs.
260 if (top_k >= vocab_size && top_p >= 1.0f && sum_exp > 0.0) {
261 std::discrete_distribution<int> pure(probs_double.begin(), probs_double.end());
262 return pure(rng);
263 }
264
265 // Normalize probabilities
266 if (sum_exp > 0.0) {
267 for (int i = 0; i < vocab_size; ++i) {
268 probs_double[i] /= sum_exp;
269 }
270 } else {
271 // If all probabilities are zero, fall back to uniform distribution
272 for (int i = 0; i < vocab_size; ++i) {
273 probs_double[i] = 1.0 / vocab_size;
274 }
275 }
276
277 std::vector<std::pair<float, int>> prob_idx(vocab_size);
278 for (int i = 0; i < vocab_size; ++i) {
279 prob_idx[i] = {static_cast<float>(probs_double[i]), i};
280 }
281
282 // Only the top_k head is used, so sort just that head: the same elements
283 // in the same order as a full sort (pairs are distinct, so the order is
284 // total), without sorting the whole vocabulary on every token.
285 if (top_k < vocab_size) {
286 std::partial_sort(prob_idx.begin(), prob_idx.begin() + top_k, prob_idx.end(),
287 std::greater<std::pair<float, int>>());
288 prob_idx.resize(top_k);
289 } else {
290 std::sort(prob_idx.begin(), prob_idx.end(), std::greater<std::pair<float, int>>());
291 }
292
293 float cumulative_prob = 0.0f;
294 int last_idx = 0;
295 for (int i = 0; i < prob_idx.size(); ++i) {
296 cumulative_prob += prob_idx[i].first;
297 last_idx = i;
298 if (cumulative_prob >= top_p) {
299 break;
300 }
301 }
302 prob_idx.resize(last_idx + 1);
303
304 float final_sum = 0.0f;
305 for (const auto& pi : prob_idx) {
306 final_sum += pi.first;
307 }
308
309 // Renormalize probabilities after top-k and top-p filtering
310 std::vector<float> final_probs(prob_idx.size());
311 if (final_sum > 0.0f) {
312 for (size_t i = 0; i < prob_idx.size(); ++i) {
313 final_probs[i] = prob_idx[i].first / final_sum;
314 }
315 } else {
316 // If all probabilities are zero after filtering, use uniform distribution
317 float uniform_prob = 1.0f / prob_idx.size();
318 std::fill(final_probs.begin(), final_probs.end(), uniform_prob);
319 }
320
321 std::discrete_distribution<int> dist(final_probs.begin(), final_probs.end());
322 int sampled_idx_in_filtered = dist(rng);
323
324 return prob_idx[sampled_idx_in_filtered].second;
325}
326
327
328TinyLlamaSession::TinyLlamaSession(const std::string& model_path_arg,
329 const std::string& tokenizer_path_arg,
330 int threads,
331 int num_gpu_layers_from_cli,
332 bool cli_use_mmap,
333 bool use_kv_quant,
334 bool use_batch_generation,
335 int max_batch_size)
336 : threads_(threads), use_batch_generation_(use_batch_generation),
337 max_batch_size_(max_batch_size), rng_(std::random_device{}()) {
338 Logger::info("TinyLlamaSession constructor entered. Model path: " + model_path_arg +
339 ", Tokenizer path: " + tokenizer_path_arg +
340 ", Threads: " + std::to_string(threads) +
341 ", Num GPU Layers (CLI): " + std::to_string(num_gpu_layers_from_cli) +
342 ", Use MMAP (CLI): " + (cli_use_mmap ? "true" : "false") +
343 ", Use KV Quant (CLI): " + (use_kv_quant ? "true" : "false"));
344#ifndef HAS_CUDA
345 // A CPU build runs every layer on the CPU whatever the CLI asks: a nonzero
346 // count here otherwise reaches generate(), which cannot run GPU layers.
347 if (num_gpu_layers_from_cli != 0) {
348 Logger::warning("This build has no CUDA: " + std::to_string(num_gpu_layers_from_cli) +
349 " GPU layer(s) requested; every layer runs on the CPU.");
350 num_gpu_layers_from_cli = 0;
351 }
352#endif
353
354 std::string effective_model_file_path = model_path_arg;
355 std::string path_for_config_json = model_path_arg;
356
357 ModelConfig initial_model_config_for_model_ctor;
358 initial_model_config_for_model_ctor.use_mmap_for_gguf = cli_use_mmap;
359 initial_model_config_for_model_ctor.use_kvcache_quantization = use_kv_quant;
360 if (num_gpu_layers_from_cli < 0) {
361 initial_model_config_for_model_ctor.num_cpu_offload_layers = 0;
362 } else {
363 initial_model_config_for_model_ctor.num_cpu_offload_layers = num_gpu_layers_from_cli;
364 }
365
366 std::filesystem::path fs_model_path(model_path_arg);
367 bool is_dir = std::filesystem::is_directory(fs_model_path);
368
369 if (is_dir) {
370 Logger::info("Model path is a directory. Assuming SafeTensors model directory: " + model_path_arg);
371 effective_model_file_path = (fs_model_path / "model.safetensors").string();
372 std::string config_json_path_in_dir = (fs_model_path / "config.json").string();
373
374 Logger::info("Derived SafeTensors model file path: " + effective_model_file_path);
375 Logger::info("Path for loading config.json: " + config_json_path_in_dir);
376
377 // Directly populate initial_model_config_for_model_ctor
378 // load_model_config_from_json returns bool and populates the passed ModelConfig&
379 bool st_config_loaded = SafeTensorsLoader::load_model_config_from_json(config_json_path_in_dir, initial_model_config_for_model_ctor);
380
381 if (st_config_loaded) {
382 Logger::info("Successfully loaded config.json directly into initial_model_config_for_model_ctor.");
383 // Log tokenizer_family IMMEDIATELY after loading from config.json
384 std::string family_after_json_load = "UNKNOWN_POST_JSON_LOAD_DIR_CASE";
385 if (initial_model_config_for_model_ctor.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA_SENTENCEPIECE) family_after_json_load = "LLAMA_SENTENCEPIECE";
386 else if (initial_model_config_for_model_ctor.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA3_TIKTOKEN) family_after_json_load = "LLAMA3_TIKTOKEN";
387 Logger::info("[API_CPP_POST_JSON_LOAD_DIR_CASE] Tokenizer family in initial_model_config_for_model_ctor: " + family_after_json_load);
388 // tokenizer_family should now be set in initial_model_config_for_model_ctor
389 // num_hidden_layers is also now set from config.json
390 } else {
391 Logger::warning("Failed to load config.json for SafeTensors. initial_model_config_for_model_ctor will have defaults/CLI overrides for some fields, tokenizer_family likely UNKNOWN.");
392 // If config.json fails, num_hidden_layers might be 0 or default, which will affect cpu offload calculation.
393 // It's crucial config.json loads for correct layer counts.
394 }
395
396 // Apply CLI overrides for mmap and GPU layers. GPU layer logic depends on total_hidden_layers from config.
397 initial_model_config_for_model_ctor.use_mmap_for_gguf = cli_use_mmap;
398
399 int total_layers_from_config = initial_model_config_for_model_ctor.num_hidden_layers;
400 if (total_layers_from_config <= 0 && st_config_loaded) {
401 Logger::warning("config.json loaded but num_hidden_layers is <= 0. GPU offload logic might be incorrect.");
402 } else if (total_layers_from_config <= 0 && !st_config_loaded) {
403 Logger::warning("config.json NOT loaded and num_hidden_layers is <= 0 (default). GPU offload may not work as expected. Model load will likely fail.");
404 // If config.json didn't load, total_layers_from_config is likely 0 from default ModelConfig.
405 // The TinyLlamaModel constructor will ultimately use its own parsed config, but this intermediate step needs care.
406 }
407
408 if (num_gpu_layers_from_cli < 0) { // -1 signifies all layers on GPU
409 initial_model_config_for_model_ctor.num_cpu_offload_layers = 0;
410 } else if (num_gpu_layers_from_cli == 0) { // 0 signifies all layers on CPU
411 initial_model_config_for_model_ctor.num_cpu_offload_layers = total_layers_from_config; // All layers offloaded to CPU
412 } else { // N > 0 signifies N layers on GPU
413 if (total_layers_from_config > 0) {
414 initial_model_config_for_model_ctor.num_cpu_offload_layers = total_layers_from_config - num_gpu_layers_from_cli;
415 } else {
416 // Cannot determine actual GPU layer count if total_layers_from_config is unknown.
417 // Pass the CLI hint, TinyLlamaModel ctor will deal with it against its own parsed config.
418 initial_model_config_for_model_ctor.num_cpu_offload_layers = num_gpu_layers_from_cli;
419 Logger::warning("Total hidden layers unknown from config.json before model load; passing num_gpu_layers_from_cli as num_cpu_offload_layers hint.");
420 }
421 }
422 // Clamp num_cpu_offload_layers
423 if (total_layers_from_config > 0) {
424 initial_model_config_for_model_ctor.num_cpu_offload_layers = std::max(0, std::min(initial_model_config_for_model_ctor.num_cpu_offload_layers, total_layers_from_config));
425 }
426
427 initial_model_config_for_model_ctor.is_gguf_file_loaded = false;
428
429 SafeTensorsLoader st_loader(effective_model_file_path);
430 model_ = std::make_unique<TinyLlamaModel>(initial_model_config_for_model_ctor, st_loader);
431 config_ = model_->get_config();
432 config_.is_gguf_file_loaded = false; // Ensure this is set for session's copy too
433
434 config_.use_kvcache_quantization = use_kv_quant; // Re-apply CLI/constructor preference
435
436 Logger::info("TinyLlamaSession: Finalizing ModelConfig for KVCache initialization. use_kvcache_quantization set to: " +
437 std::string(config_.use_kvcache_quantization ? "true" : "false"));
438
445
446 } else { // Not a directory, assume it's a file path and check extension
447 std::string extension = fs_model_path.extension().string();
448 std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
449
450 if (extension == ".gguf") {
451 Logger::info("GGUF model type detected by extension for Session constructor: " + model_path_arg);
452 // The GGUF constructor decodes num_cpu_offload_layers as the RAW
453 // GPU-layer hint (<0 all GPU, 0 all CPU, >0 that many GPU layers).
454 // The translation above turned -1 into 0, which that decoder reads as
455 // "all CPU": every default GGUF run was CPU-only (docs/TTFT.md).
456 initial_model_config_for_model_ctor.num_cpu_offload_layers = num_gpu_layers_from_cli;
457 model_ = std::make_unique<TinyLlamaModel>(initial_model_config_for_model_ctor, model_path_arg);
458 config_ = model_->get_config();
459 } else if (extension == ".safetensors") {
460 Logger::info("SafeTensors model type detected by extension for Session constructor (file case): " + model_path_arg);
461 effective_model_file_path = model_path_arg;
462
463 bool st_config_loaded = SafeTensorsLoader::load_model_config_from_json(effective_model_file_path, initial_model_config_for_model_ctor);
464 if (st_config_loaded) {
465 Logger::info("Successfully loaded config.json for SafeTensors in Session ctor (file case).");
466 // Log tokenizer_family IMMEDIATELY after loading from config.json (file case)
467 std::string family_after_json_load_file_case = "UNKNOWN_POST_JSON_LOAD_FILE_CASE";
468 if (initial_model_config_for_model_ctor.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA_SENTENCEPIECE) family_after_json_load_file_case = "LLAMA_SENTENCEPIECE";
469 else if (initial_model_config_for_model_ctor.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA3_TIKTOKEN) family_after_json_load_file_case = "LLAMA3_TIKTOKEN";
470 Logger::info("[API_CPP_POST_JSON_LOAD_FILE_CASE] Tokenizer family in initial_model_config_for_model_ctor: " + family_after_json_load_file_case);
471 // tokenizer_family and num_hidden_layers are now set in initial_model_config_for_model_ctor
472 } else {
473 Logger::warning("Failed to load config.json for SafeTensors in Session ctor (file case). Model will use defaults or CLI overrides.");
474 }
475
476 initial_model_config_for_model_ctor.use_mmap_for_gguf = cli_use_mmap;
477 // Correctly calculate num_cpu_offload_layers based on total_layers_from_config
478 int total_layers_from_config_file_case = initial_model_config_for_model_ctor.num_hidden_layers;
479 if (num_gpu_layers_from_cli < 0) {
480 initial_model_config_for_model_ctor.num_cpu_offload_layers = 0;
481 } else if (num_gpu_layers_from_cli == 0) {
482 initial_model_config_for_model_ctor.num_cpu_offload_layers = total_layers_from_config_file_case;
483 } else {
484 if (total_layers_from_config_file_case > 0) {
485 initial_model_config_for_model_ctor.num_cpu_offload_layers = total_layers_from_config_file_case - num_gpu_layers_from_cli;
486 } else {
487 initial_model_config_for_model_ctor.num_cpu_offload_layers = num_gpu_layers_from_cli;
488 Logger::warning("Total hidden layers unknown from config.json (file case); passing num_gpu_layers_from_cli as num_cpu_offload_layers hint.");
489 }
490 }
491 if (total_layers_from_config_file_case > 0) {
492 initial_model_config_for_model_ctor.num_cpu_offload_layers = std::max(0, std::min(initial_model_config_for_model_ctor.num_cpu_offload_layers, total_layers_from_config_file_case));
493 }
494
495 initial_model_config_for_model_ctor.is_gguf_file_loaded = false;
496
497 SafeTensorsLoader st_loader(effective_model_file_path);
498 model_ = std::make_unique<TinyLlamaModel>(initial_model_config_for_model_ctor, st_loader);
499 config_ = model_->get_config();
500 config_.is_gguf_file_loaded = false; // Ensure this is set for session's copy too
501
502 config_.use_kvcache_quantization = use_kv_quant; // Re-apply CLI/constructor preference
503
504 Logger::info("TinyLlamaSession: Finalizing ModelConfig for KVCache initialization. use_kvcache_quantization set to: " +
505 std::string(config_.use_kvcache_quantization ? "true" : "false"));
506
507 // Initialize KVCache with potentially updated config_ (from model load)
508 // and the now-set use_kvcache_quantization flag.
515 } else {
516 throw std::runtime_error("Unsupported model file type or extension in Session constructor: " + model_path_arg +
517 ". Please provide a directory for SafeTensors, a .gguf file, or a .safetensors file.");
518 }
519 }
520
521 if (!model_) {
522 throw std::runtime_error("Model pointer is null after instantiation attempt in Session constructor.");
523 }
524
525 try {
527 const GGUFData* gguf_data = model_->get_gguf_data();
528 if (!gguf_data) {
529 throw std::runtime_error("GGUF model loaded but GGUFData is null in Session constructor.");
530 }
531 tokenizer_ = std::make_unique<Tokenizer>(*gguf_data, config_);
532 Logger::info("Tokenizer initialized from GGUF metadata.");
533 } else { // SafeTensors (either from directory or direct .safetensors file)
534 std::filesystem::path p_tokenizer_arg(tokenizer_path_arg);
535 std::string tokenizer_dir = p_tokenizer_arg.parent_path().string();
536 if (tokenizer_dir.empty()) {
537 tokenizer_dir = ".";
538 }
539
540 std::string vocab_json_path = (std::filesystem::path(tokenizer_dir) / "tokenizer.json").string();
541 // The model_path for the tokenizer constructor should be the actual sentencepiece model file, e.g. data/tokenizer.model
542 // tokenizer_path_arg already holds this (e.g. data/tokenizer.model)
543 std::string sp_model_path = tokenizer_path_arg;
544
545 Logger::info("Initializing Tokenizer for SafeTensors. Vocab JSON path: " + vocab_json_path + ", SP Model path: " + sp_model_path);
546 // Log the tokenizer_family from the config_ that will be passed to the Tokenizer
547 std::string family_to_log = "UNKNOWN_IN_API_CPP";
548 if (config_.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA_SENTENCEPIECE) family_to_log = "LLAMA_SENTENCEPIECE";
549 else if (config_.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA3_TIKTOKEN) family_to_log = "LLAMA3_TIKTOKEN";
550 Logger::info("[API_CPP_TOKENIZER_INIT] Tokenizer family from session config for SafeTensors: " + family_to_log);
551
552 tokenizer_ = std::make_unique<Tokenizer>(vocab_json_path, sp_model_path, config_);
553 Logger::info("Tokenizer initialized from external files for SafeTensors model.");
554 }
555 } catch (const std::exception& e) {
556 throw std::runtime_error(std::string("Failed to initialize Tokenizer: ") + e.what());
557 }
558
559 if (!tokenizer_) {
560 throw std::runtime_error("Tokenizer pointer is null after instantiation attempt.");
561 }
562
563 eos_token_id_ = config_.eos_token_id; // Use the session's config_ which is now model's config
564 // paperkiln IR (LOCAL_LLM_STACK.md section 4): EMBER_IR=path runs the
565 // interpreter; an IR embedded in the GGUF (`paperkiln.ir`) runs it unless it
566 // is the graph the tuned llama path implements (ember_ir::tuned_llama_match:
567 // a structural match against the file's llama metadata; family_hint "llama"
568 // is only its pre-filter, IR_SPEC 9.1). EMBER_INTERPRETER=1/0 forces it on or off.
569 if (const GGUFData* gd = model_->get_gguf_data()) {
570 const std::string ir = ember_ir::find_ir(*gd);
571 const char* force = std::getenv("EMBER_INTERPRETER");
572 if (model_->ir_only() && ir.empty()) throw std::runtime_error("IR-only model but no IR found");
573 if (model_->ir_only() && force && std::string(force) == "0")
574 throw std::runtime_error("EMBER_INTERPRETER=0, but this GGUF can only run through the IR interpreter");
575 if (!ir.empty() && !(force && std::string(force) == "0")) {
576 const std::string family = nlohmann::json::parse(ir).value("family_hint", "");
577 if ((force && std::string(force) == "1") || std::getenv("EMBER_IR") || model_->ir_only() ||
578 !ember_ir::tuned_llama_match(ir, *gd)) {
579 // CUDA builds run the interpreter on the device when the CLI asked for
580 // GPU layers (any nonzero count: the interpreter does not split
581 // layers); EMBER_IR_DEVICE=cpu keeps it on the CPU. Every op the
582 // interpreter has (recurrent state, gating and attention-residual ops
583 // and sliding-window attention included) runs on either; an IR past
584 // the device kernels' limits stays on the CPU (ember_ir::device_supported).
585 bool ir_gpu = false;
586#ifdef HAS_CUDA
587 const char* dev = std::getenv("EMBER_IR_DEVICE");
588 ir_gpu = num_gpu_layers_from_cli != 0 && !(dev && std::string(dev) == "cpu") &&
590#endif
591 ir_ = std::make_unique<ember_ir::Interpreter>(ir, *gd, ir_gpu);
592 ir_doc_ = ir;
593 Logger::warning("Running the model through the paperkiln IR interpreter (" +
594 std::string(ir_->on_gpu() ? "CUDA" : "CPU") + "; family_hint '" + family +
595 "'). The llama path's weights stay loaded but unused.");
596 }
597 }
598 }
599 // Small quantised models (a Q8_0 download for the browser): dequantise
600 // once, so the CPU path runs at f32 speed on the quantised values.
601 // EMBER_DEQUANT_AT_LOAD=0 keeps the per-call quantised kernels.
602 {
603 const char* dq = std::getenv("EMBER_DEQUANT_AT_LOAD");
604 if (!(dq && std::string(dq) == "0") && model_->dequantize_all_if_small(64u * 1000 * 1000))
605 Logger::info("Dequantised all weights to f32 at load (small model).");
606 }
607
608 // Chat mode (chat_tokenizer.h): the GGUF's chat template is resolved by chat::resolve (declared
609 // paperkiln.chat_template, paperkiln's specials, a word-level vocabulary, a third-party record by
610 // sha256 or markers and architecture), and the vocabulary goes through the vendored paperkiln
611 // tokenizers ("bpe", "gpt2" with "llama-bpe" or "qwen2") or the SentencePiece port ("llama",
612 // Gemma 3). A GGUF with no template keeps ember's own tokenizers and serves raw completion.
613 {
614 std::string ext = std::filesystem::path(model_path_arg).extension().string();
615 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
616 if (ext == ".gguf") {
618 if (const GGUFData* gd = model_->get_gguf_data()) {
619 v = ember::chat_vocab_from_gguf(*gd, model_path_arg);
620 } else {
621 v.g = microtorch::bpe::read_gguf_tokenizer(model_path_arg);
622 }
623 const size_t n_merges = v.g.merges.size();
626 bpe_ = std::move(cs.tok);
627 if (bpe_) {
628 repetition_penalty_ = 1.0f; // mtstudio chat applies none
629 Logger::info(std::string("Chat tokenizer (") + bpe_->kind() + "): " + std::to_string(bpe_->size()) +
630 " tokens, " + std::to_string(n_merges) + " merges (chat mode)");
631 Logger::info("Chat template " + bpe_->format().name + " (" + cs.why + ")");
632 } else if (cs.resolved) {
633 word_template_ = std::make_unique<microtorch::chat::Template>(cs.tmpl);
634 Logger::info("Chat template " + cs.tmpl.id() + " (" + cs.why + "), served by ember's own tokenizer");
635 } else {
636 Logger::info("No chat template: " + cs.why);
637 }
638 }
639 }
640 // Per-turn state tokens (state_tokens.h): k from the GGUF's paperkiln.state_tokens.k (absent =
641 // 0); the interpreter has already checked it against its state_weight. The template contract
642 // needs paperkiln's chat format and the interpreter.
643 if (const GGUFData* gd = model_->get_gguf_data()) {
644 auto it = gd->metadata.find("paperkiln.state_tokens.k");
645 int k = 0;
646 if (it != gd->metadata.end())
647 std::visit(
648 [&](const auto& v) {
649 using V = std::decay_t<decltype(v)>;
650 if constexpr (std::is_arithmetic_v<V> && !std::is_same_v<V, bool>) k = static_cast<int>(v);
651 },
652 it->second);
653 if (k > 0) {
654 if (!ir_ || ir_->state_k() != k)
655 throw std::runtime_error("paperkiln.state_tokens.k is " + std::to_string(k) +
656 ": the model must run through the IR interpreter with a state_weight of k rows");
657 // The insertion rule is defined over paperkiln/1's markers (state_tokens.h; ir.hpp names
658 // no other template yet).
659 if (!bpe_ || !bpe_->format().v1_markers())
660 throw std::runtime_error("paperkiln.state_tokens.k > 0 needs the paperkiln/1 chat template (state blocks are "
661 "defined over its markers); this file's template is " +
662 std::string(bpe_ ? bpe_->format().name : "none") + " (" + chat_template_why_ + ")");
663 state_k_ = k;
664 state_vocab_ = ir_->vocab();
665 Logger::info("Per-turn state tokens: k " + std::to_string(k) + " (ids " + std::to_string(state_vocab_) + " .. " +
666 std::to_string(state_vocab_ + k - 1) + ") after every <|endoftext|> that ends a reply" +
667 (ir_->evicts() ? "; kv eviction on" : "; every kv row kept"));
668 }
669 }
670
671 const ModelConfig& final_model_config = model_->get_config(); // Explicitly use model's config
672 int total_model_layers = final_model_config.num_hidden_layers;
673
674 int effective_cpu_offload_layers = final_model_config.num_cpu_offload_layers;
675 int gpu_layers_for_kvcache = total_model_layers - effective_cpu_offload_layers;
676 if (gpu_layers_for_kvcache < 0) gpu_layers_for_kvcache = 0; // Sanity check, should not happen if model ctor is correct
677 if (gpu_layers_for_kvcache > total_model_layers) gpu_layers_for_kvcache = total_model_layers; // Sanity
678
679 Logger::info("[Session KVCache Init] Total Layers: " + std::to_string(total_model_layers) +
680 ", Effective CPU Offload by Model: " + std::to_string(effective_cpu_offload_layers) +
681 ", GPU Layers for KVCache: " + std::to_string(gpu_layers_for_kvcache));
682
683 if (total_model_layers <= 0) {
684 throw std::runtime_error("Model config has zero or negative num_hidden_layers before KVCache init.");
685 }
686 if (final_model_config.num_attention_heads <= 0) {
687 throw std::runtime_error("Model config has zero or negative num_attention_heads before KVCache init.");
688 }
689
690 int head_dim = final_model_config.hidden_size / final_model_config.num_attention_heads;
691
692 kv_cache_.initialize(final_model_config, // Total layers for CPU part of KVCache
693 total_model_layers,
694 gpu_layers_for_kvcache, // Actual GPU layers for device memory
695 final_model_config.max_position_embeddings,
696 final_model_config.num_key_value_heads,
697 head_dim,
699 Logger::info("TinyLlamaSession initialization complete (after KVCache init).");
700}
701
703 Logger::info("TinyLlamaSession: Destroyed.");
704}
705
706std::string TinyLlamaSession::generate(const std::string& user_prompt, int steps,
707 float temperature,
708 int top_k, float top_p,
709 const std::string& system_prompt_arg, // Renamed for clarity inside function
710 bool apply_q_a_format_cli_hint) { // Renamed for clarity
711 auto t_start = std::chrono::high_resolution_clock::now(); // Start timing
712
713 // TTFT instrumentation (docs/TTFT.md): each phase up to the first token.
714 using ttft_clock = std::chrono::steady_clock;
715 auto ms_between = [](ttft_clock::time_point a, ttft_clock::time_point b) {
716 return std::chrono::duration<double, std::milli>(b - a).count();
717 };
719 last_ttft_.t_entry = ttft_clock::now();
720
721 generated_text_for_api_return_.clear(); // Clear for new generation
722 generated_stream_.str(""); // Clear for new generation
723 generated_stream_.clear(); // Clear error flags
724
725 Logger::info("[Generate API] User prompt: \"" + user_prompt + "\", System prompt: \"" + system_prompt_arg + "\", Steps: " + std::to_string(steps));
726
727 if (!model_ || !tokenizer_) {
728 throw std::runtime_error("Model or tokenizer not loaded.");
729 }
730
731 std::string final_prompt_for_encoding;
732 bool used_chat_template = false;
733
734 // Log conditions for chat template application
735 if (tokenizer_) {
736 bool gguf_template_empty = tokenizer_->get_gguf_chat_template().empty();
737 Logger::info("[Generate API] GGUF chat template from tokenizer is empty: " + std::string(gguf_template_empty ? "true" : "false"));
738 if (!gguf_template_empty) {
739 Logger::info("[Generate API] GGUF Template Content (first 100 chars): " + tokenizer_->get_gguf_chat_template().substr(0, 100));
740 }
741 } else {
742 Logger::warning("[Generate API] Tokenizer is null before checking chat template!");
743 }
744 std::string family_log_str = "UNKNOWN";
745 if (config_.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA_SENTENCEPIECE) family_log_str = "LLAMA_SENTENCEPIECE";
746 else if (config_.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA3_TIKTOKEN) family_log_str = "LLAMA3_TIKTOKEN";
747 Logger::info("[Generate API] Configured tokenizer_family: " + family_log_str);
748
749 // New Priority Logic:
750 // Priority 0: WORD_LEVEL models (transformer_cpp exports) are raw word-level
751 // LMs: "Q:"/"A:" wrappers are OOV noise that shift every position, and any
752 // chat formatting ("user: ... assistant:") is the caller's contract.
753 // BPE chat models: the caller (server.cpp) already built the exact SFT
754 // template with its <|user|>/<|assistant|> markers; nothing is added.
755 if (bpe_) {
756 final_prompt_for_encoding = user_prompt;
757 used_chat_template = false;
759 Logger::info("[Generate API] WORD_LEVEL tokenizer: using raw prompt (no Q/A or chat template).");
760 final_prompt_for_encoding = user_prompt;
761 used_chat_template = false;
762 }
763 // Priority 1 (NEW): Legacy Q/A formatting if CLI hint is true
764 else if (apply_q_a_format_cli_hint) {
765 Logger::info("[Generate API] Using legacy Q/A formatting (CLI Hint is true - Priority 1).");
766 std::string temp_prompt = user_prompt;
767 if (!system_prompt_arg.empty()) {
768 temp_prompt = system_prompt_arg + "\\n\\nQ: " + user_prompt + "\\nA:";
769 } else {
770 temp_prompt = "Q: " + user_prompt + "\\nA:";
771 }
772 final_prompt_for_encoding = temp_prompt;
773 used_chat_template = false; // Q/A is not a 'chat template' in the GGUF sense
774 }
775 // Priority 2 (WAS 1): GGUF Chat Template from Tokenizer (only if Q/A hint is false)
776 else if (tokenizer_ && !tokenizer_->get_gguf_chat_template().empty()) {
777 std::string gguf_template_content = tokenizer_->get_gguf_chat_template();
778 bool is_llama_sentencepiece_family = (config_.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA_SENTENCEPIECE);
779 bool looks_like_jinja = (gguf_template_content.find("{%") != std::string::npos);
780
781 if (is_llama_sentencepiece_family && looks_like_jinja) {
782 Logger::info("[Generate API] Detected LLAMA_SENTENCEPIECE model with a Jinja-like GGUF template. Forcing Q/A format to avoid C++ Jinja processing issues (Priority 2 Override).");
783 std::string temp_prompt = user_prompt;
784 if (!system_prompt_arg.empty()) {
785 temp_prompt = system_prompt_arg + "\\\\n\\\\nQ: " + user_prompt + "\\\\nA:";
786 } else {
787 temp_prompt = "Q: " + user_prompt + "\\\\nA:";
788 }
789 final_prompt_for_encoding = temp_prompt;
790 used_chat_template = false;
791 } else {
792 Logger::info("[Generate API] Using GGUF chat template from tokenizer (Q/A Hint false - Priority 2).");
793 final_prompt_for_encoding = tokenizer_->apply_chat_template(user_prompt, system_prompt_arg, config_);
794 used_chat_template = true;
795 }
796 }
797 // Priority 3 (WAS 2): Llama 3 family specific template (only if Q/A hint false and no GGUF template)
799 Logger::info("[Generate API] Llama 3 tokenizer family detected, using apply_chat_template (Q/A Hint false, No GGUF template - Priority 3).");
800 final_prompt_for_encoding = tokenizer_->apply_chat_template(user_prompt, system_prompt_arg, config_);
801 used_chat_template = true;
802 }
803 // Priority 4 (WAS 3/4 depending on apply_q_a_format_cli_hint): Raw prompt (if all above are false)
804 else {
805 Logger::info("[Generate API] No applicable template/hint. Using user prompt as is (prepending system prompt if available - Priority 4).");
806 if (!system_prompt_arg.empty()) {
807 final_prompt_for_encoding = system_prompt_arg + "\\n\\n" + user_prompt;
808 } else {
809 final_prompt_for_encoding = user_prompt;
810 }
811 used_chat_template = false;
812 }
813
814 Logger::debug("[Generate API] Final prompt for encoding (first 100 chars): \\\"" + final_prompt_for_encoding.substr(0, 100) + "\\\"");
815
816 // BPE chat models take no BOS (the SFT template starts at <|user|>).
817 // Per-turn state tokens: the k state ids after every <|endoftext|> that ends a reply, and
818 // <|endoftext|> + the state ids for a reply that stopped early (bpe_prompt_ids, state_tokens.h).
819 std::vector<int> tokens = bpe_ ? bpe_prompt_ids(final_prompt_for_encoding)
820 : tokenizer_->encode(final_prompt_for_encoding, true, false, Tokenizer::PreTokenizeMethod::DEFAULT); // add_bos=true, add_eos=false (EOS handled by loop)
821 // Stop tokens: in chat mode the chat format's (paperkiln: <|endoftext|>, or a
822 // generated <|user|> or <|assistant|>, the model starting the next turn;
823 // ChatML: <|im_end|> or <|endoftext|>), else the GGUF's eos.
824 auto is_stop = [&](int id) {
825 return bpe_ ? bpe_->format().is_stop(id) : id == eos_token_id_;
826 };
827 auto decode_one = [&](int id) {
828 return bpe_ ? bpe_->decode({id}) : tokenizer_->decode({id}, false);
829 };
830 // Streaming: the per-token callback (set_token_callback) sees every
831 // generated token except stop markers, in order.
832 auto emit_token = [&](int id) {
833 if (token_callback_ && !is_stop(id)) token_callback_(id);
834 };
835 bool stopped_at_first = false;
836 last_stop_reason_ = "length";
838 last_prompt_ids_ = tokens;
839 last_generated_ids_.clear();
840 last_stop_id_ = -1;
841
842 if (tokens.empty()) {
843 Logger::warning("Tokenization resulted in empty ID list for prompt: " +
844 final_prompt_for_encoding);
845 return "";
846 }
847
848 int num_prompt_tokens = tokens.size();
849 Logger::info("[Generate API] Number of prompt tokens: " + std::to_string(num_prompt_tokens));
850 last_ttft_.num_prompt_tokens = num_prompt_tokens;
851 last_ttft_.tokenize_ms = ms_between(last_ttft_.t_entry, ttft_clock::now());
852 {
853 // Prefix-cache opportunity against what the previous request cached.
854 size_t lcp = 0;
855 while (lcp < kv_token_history_.size() && lcp < tokens.size() &&
856 kv_token_history_[lcp] == tokens[lcp]) {
857 ++lcp;
858 }
859 last_ttft_.prefix_prev_cached_tokens = static_cast<int>(kv_token_history_.size());
861 static_cast<int>(std::min(lcp, tokens.size() - 1));
863 static_cast<double>(last_ttft_.prefix_reusable_tokens) / num_prompt_tokens;
864 }
865 // Prefix reuse (EMBER_PREFIX_REUSE, on by default): the KV cache still
866 // holds the previous request's prompt and reply, so the shared leading
867 // tokens are kept and only the rest is prefilled, from that offset. At
868 // least the last prompt token is always recomputed, for its logits. Only
869 // the batched prefill path reuses; the token-by-token path starts over.
870 static const bool prefix_reuse_on = [] {
871 const char* v = std::getenv("EMBER_PREFIX_REUSE");
872 return !(v && std::string(v) == "0");
873 }();
874 int reuse = prefix_reuse_on ? last_ttft_.prefix_reusable_tokens : 0;
875 // Recurrent IR state (linear attention, state-space scans) cannot drop
876 // tokens: resume where the interpreter holds the state after the prefix
877 // (the live state or a snapshot), else nearer the start or from 0.
878 // Under turn_state eviction, a prefix ending inside a turn whose text rows were dropped (an
879 // edited earlier message) resumes from that turn's start, which is re-prefilled.
880 if (ir_ && (ir_->has_recurrent() || ir_->evicts())) reuse = ir_->resume_point(reuse);
881 kv_token_history_.clear(); // refilled only if this request completes
882
883 // Test hook (set_scripted_replies): the next scripted ids stand in for sampling, through the same
884 // callback, stop rule and reply decoding as sampled ids; kv_token_history_ stays empty.
885 if (bpe_ && !scripted_replies_.empty()) {
886 const std::vector<int> script = scripted_replies_.front();
888 std::vector<int> out;
889 for (int id : script) {
890 if (static_cast<int>(out.size()) >= steps) break;
891 out.push_back(id);
892 emit_token(id);
893 if (is_stop(id)) break;
894 }
895 if (!out.empty() && is_stop(out.back())) {
896 last_stop_reason_ = bpe_->format().is_eos(out.back()) ? "eos" : "turn";
897 last_stop_id_ = out.back();
898 out.pop_back();
899 }
900 last_generated_tokens_ = static_cast<int>(out.size());
902 const std::string text = bpe_->decode(out);
903 const char* ws = " \t\n\v\f\r";
904 const auto first = text.find_first_not_of(ws);
905 const auto last = text.find_last_not_of(ws);
906 return first == std::string::npos ? std::string() : text.substr(first, last - first + 1);
907 }
908
909 int total_steps = num_prompt_tokens + steps -1; // Max total tokens including prompt
910 int generated_count = 0;
911 int next_token_id = -1;
912
913 std::vector<float> logits; // Declare logits here, to be populated by prefill or loop
914 std::vector<int> generated_token_ids; // Track generated tokens separately
915
916 // The /chat contract's repetition penalty (set_repeat_penalty) over prompt
917 // + reply so far, the token being sampled not included.
918 const bool repeat_on = repeat_penalty_ != 1.0f && repeat_last_n_ > 0;
919 std::vector<char> repeat_seen;
920 auto apply_repeat_penalty = [&](std::vector<float>& lg) {
921 if (repeat_on)
922 apply_contract_penalty(lg, tokens, generated_token_ids, repeat_penalty_, repeat_last_n_, is_stop, repeat_seen);
923 };
924
925 auto t_kv_reset = ttft_clock::now();
926 if (reuse > 0) {
927 // Keep positions [0, reuse); every later position is rewritten before
928 // attention reads it (attention reads only positions <= the current one).
929 kv_cache_.seq_len = reuse;
930 } else {
931 kv_cache_.clear_data(); // Clear K/V vector data for all layers
932 kv_cache_.seq_len = 0; // Reset KVCache logical sequence length for new sequence
933 }
934 last_ttft_.kv_reset_ms = ms_between(t_kv_reset, ttft_clock::now());
935
936 std::vector<float> current_data_host; // To hold embedding or output of CPU layers
937 int start_pos_for_loop = 0;
938
939 // Batch prefill for every prompt length. The old 32-token threshold cost
940 // short (chat-sized) prompts: on chat_sft_bpe, 20 prompts of 10-30 tokens,
941 // p50 TTFT 294-338 ms -> 53-55 ms on CUDA and 983-1251 -> 592-650 ms on CPU,
942 // with identical greedy replies (experiments/chat_2026-10-18/).
943 // EMBER_PREFILL_MIN restores a threshold (measurement knob).
944 static const int prefill_min = [] {
945 const char* v = std::getenv("EMBER_PREFILL_MIN");
946 return v && *v ? std::max(1, std::atoi(v)) : 1;
947 }();
948 bool prefill_enabled = num_prompt_tokens >= prefill_min;
949 // Reused positions: [0, prefill_from) stay in the cache untouched.
950 const int prefill_from = prefill_enabled ? reuse : 0;
951 const int prefill_count = num_prompt_tokens - prefill_from;
952 last_ttft_.prefix_reused_tokens = prefill_from;
953
954if (prefill_enabled) {
955 Logger::info("[Generate API] Prefill enabled. num_prompt_tokens: " + std::to_string(num_prompt_tokens) +
956 ", num_cpu_offload_layers: " + std::to_string(config_.num_cpu_offload_layers) +
957 ", total_hidden_layers: " + std::to_string(config_.num_hidden_layers));
959 auto t_embed = ttft_clock::now();
960
961 std::vector<float> batch_initial_embeddings(ir_ ? 0 : prefill_count * config_.hidden_size);
962 for (int i = 0; i < (ir_ ? 0 : prefill_count); ++i) { // the interpreter embeds tokens itself
963 std::vector<float> token_embedding = model_->lookup_embedding(tokens[prefill_from + i]);
964 if (token_embedding.empty()) {
965 Logger::error("Prefill: Embedding lookup returned empty vector for token ID: " + std::to_string(tokens[i]) + " at prompt pos " + std::to_string(i));
966 return ""; // Or handle error appropriately
967 }
968 std::copy(token_embedding.begin(), token_embedding.end(), batch_initial_embeddings.begin() + i * config_.hidden_size);
969 }
970 auto t_prefill = ttft_clock::now();
971 last_ttft_.embed_ms = ms_between(t_embed, t_prefill);
972
973 if (ir_) {
974 const std::vector<int> slice(tokens.begin() + prefill_from, tokens.begin() + prefill_from + prefill_count);
975 logits = ir_->forward(slice, prefill_from);
976 } else {
977 // If there are CPU layers to process for prefill
978 std::vector<float> cpu_processed_embeddings;
980 Logger::info("[Generate API] Prefill: Processing " + std::to_string(config_.num_cpu_offload_layers) + " CPU layers for the batch.");
981 cpu_processed_embeddings = model_->forward_cpu_batch(batch_initial_embeddings, prefill_count, config_.num_cpu_offload_layers, prefill_from, &kv_cache_);
982 if (cpu_processed_embeddings.empty()) {
983 Logger::error("Prefill: forward_cpu_batch returned empty or failed.");
984 return "";
985 }
986 } else {
987 cpu_processed_embeddings = batch_initial_embeddings; // No CPU layers, pass embeddings directly
988 }
989
990 // If all layers are CPU layers (i.e., num_gpu_layers == 0)
992 Logger::info("[Generate API] Prefill: All layers are on CPU. Getting logits from final CPU layer output.");
993 std::vector<float> batch_logits = model_->forward_cpu_logits_batch(cpu_processed_embeddings, prefill_count);
994 if (batch_logits.empty() || batch_logits.size() % config_.vocab_size != 0) {
995 Logger::error("Prefill: forward_cpu_logits_batch returned invalid logits.");
996 return "";
997 }
998 // Extract logits for the last token of the prompt
999 logits.assign(batch_logits.begin() + (prefill_count - 1) * config_.vocab_size,
1000 batch_logits.begin() + prefill_count * config_.vocab_size);
1001 } else { // GPU layers exist and need to be processed
1002#ifdef HAS_CUDA
1003 Logger::info("[Generate API] Prefill: Processing GPU layers for the batch.");
1004 // Copy the (potentially CPU-processed) embeddings to the device
1005 float* d_temp_batch_embeddings = nullptr;
1006 size_t batch_embeddings_size_bytes = cpu_processed_embeddings.size() * sizeof(float);
1007
1008 if (batch_embeddings_size_bytes == 0) {
1009 Logger::error("Prefill: cpu_processed_embeddings is empty, cannot proceed with GPU batch prefill.");
1010 return ""; // Or handle error appropriately
1011 }
1012
1013 gpuErrchk(cudaMalloc(&d_temp_batch_embeddings, batch_embeddings_size_bytes));
1014 if (!d_temp_batch_embeddings) {
1015 Logger::error("Prefill: cudaMalloc failed for d_temp_batch_embeddings.");
1016 return ""; // Or handle error appropriately
1017 }
1018
1019 gpuErrchk(cudaMemcpy(d_temp_batch_embeddings, cpu_processed_embeddings.data(),
1020 batch_embeddings_size_bytes, cudaMemcpyHostToDevice));
1021
1022 // Events on the default stream (the forward runs on stream 0).
1023 cudaEvent_t ev_fwd_start, ev_fwd_end;
1024 gpuErrchk(cudaEventCreate(&ev_fwd_start));
1025 gpuErrchk(cudaEventCreate(&ev_fwd_end));
1026 gpuErrchk(cudaEventRecord(ev_fwd_start, 0));
1027 nvtxRangePushA("ttft_prefill_forward");
1028 logits = model_->forward_device_batch_prefill(d_temp_batch_embeddings, prefill_count, prefill_from, &kv_cache_, 0);
1029 nvtxRangePop();
1030 gpuErrchk(cudaEventRecord(ev_fwd_end, 0));
1031 gpuErrchk(cudaEventSynchronize(ev_fwd_end));
1032 float fwd_span_ms = 0.0f;
1033 gpuErrchk(cudaEventElapsedTime(&fwd_span_ms, ev_fwd_start, ev_fwd_end));
1034 last_ttft_.prefill_gpu_span_ms = fwd_span_ms;
1035 last_ttft_.op_ms = model_->last_prefill_op_ms();
1036 gpuErrchk(cudaEventDestroy(ev_fwd_start));
1037 gpuErrchk(cudaEventDestroy(ev_fwd_end));
1038
1039 if (d_temp_batch_embeddings) {
1040 gpuErrchk(cudaFree(d_temp_batch_embeddings));
1041 }
1042#else
1043 // An error, not an empty reply: a caller must not read it as a finished generation.
1044 throw std::runtime_error("[Generate API] GPU layers requested but CUDA not available");
1045#endif
1046 }
1047 } // llama path
1048
1049 if (logits.empty()) {
1050 Logger::error("Prefill: Logits are empty after prefill processing.");
1051 return ""; // Critical error
1052 }
1053 auto t_sample = ttft_clock::now();
1054 last_ttft_.prefill_wall_ms = ms_between(t_prefill, t_sample);
1055 apply_repetition_penalty(logits, generated_token_ids, repetition_penalty_);
1056 apply_repeat_penalty(logits);
1057 debug_dump_topk(logits);
1058 next_token_id = sample_top_k_top_p_temperature(logits, temperature, top_k, top_p, rng_);
1059 generated_token_ids.push_back(next_token_id); // Track generated token
1060 generated_count++;
1061 emit_token(next_token_id);
1062
1063 // Stream the first generated token from prefill
1064 generated_stream_ << decode_one(next_token_id);
1065 generated_text_for_api_return_ += decode_one(next_token_id);
1066 last_ttft_.t_first_token = ttft_clock::now();
1067 last_ttft_.first_token = true;
1068 last_ttft_.sample_ms = ms_between(t_sample, last_ttft_.t_first_token);
1070
1071 start_pos_for_loop = num_prompt_tokens; // Next token will be at num_prompt_tokens
1072 if (bpe_ && is_stop(next_token_id)) {
1073 stopped_at_first = true; // the reply is empty; skip the decode loop
1074 total_steps = 0;
1075 }
1076 kv_cache_.seq_len = num_prompt_tokens; // KVCache is now filled up to num_prompt_tokens
1077
1078 Logger::info("[Generate API] Prefill completed. next_token_id: " + std::to_string(next_token_id) +
1079 ", Decoded: \"" + decode_one(next_token_id) + "\"" +
1080 ", start_pos_for_loop set to: " + std::to_string(start_pos_for_loop));
1081 }
1082
1083 // EMBER_DECODE_TIMING=1: wall time per decode phase, host side here and
1084 // GPU-forward side in TinyLlamaModel::forward_device, summarised on stderr
1085 // after the loop (per decode step).
1086 static const bool decode_timing = std::getenv("EMBER_DECODE_TIMING") != nullptr;
1087 std::map<std::string, double> host_phase_ms;
1088 int decode_steps = 0;
1089 auto t_phase = ttft_clock::now();
1090 auto host_phase = [&](const char* name) {
1091 if (!decode_timing) return;
1092 const auto now = ttft_clock::now();
1093 host_phase_ms[name] += ms_between(t_phase, now);
1094 t_phase = now;
1095 };
1096#ifdef HAS_CUDA
1097 if (decode_timing) model_->decode_phase_ms().clear();
1098#endif
1099 const auto t_decode_start = ttft_clock::now();
1100 // EMBER_SPEC=k: speculative decoding with prompt-lookup drafts of up to k
1101 // tokens, greedy (top_k == 1) only, all-GPU or all-CPU layers, and off
1102 // under the contract repetition penalty (its window moves with each row).
1103 static const int spec_k = [] {
1104 const char* e = std::getenv("EMBER_SPEC");
1105 return e ? std::max(0, std::atoi(e)) : 0;
1106 }();
1107 const bool spec_on = spec_k > 0 && top_k == 1 && !repeat_on &&
1110 // EMBER_DRAFT selects the drafter (experiments/draft_2026-10-27). Unset or "pld": prompt lookup alone, as
1111 // before. "table": prompt lookup first; where it finds nothing, the corpus table EMBER_DRAFT_TABLE
1112 // (draft_table.h), its draft cut where the product of the entries' shares falls below EMBER_DRAFT_THETA.
1113 // Verification decides every token either way, so the replies are plain decode's.
1114 static const bool draft_table_mode = [] {
1115 const char* e = std::getenv("EMBER_DRAFT");
1116 const std::string v = e ? e : "";
1117 if (!v.empty() && v != "pld" && v != "table")
1118 Logger::warning("EMBER_DRAFT=" + v + " is not a drafter (pld, table); prompt-lookup drafts only.");
1119 return v == "table";
1120 }();
1121 static const double draft_theta = [] {
1122 const char* e = std::getenv("EMBER_DRAFT_THETA");
1123 return e && *e ? std::atof(e) : 0.02;
1124 }();
1125 const ember_draft::DraftTable* draft_table =
1126 spec_on && draft_table_mode ? corpus_draft_table(config_.vocab_size) : nullptr;
1127
1128 for (int pos = start_pos_for_loop; pos < total_steps; ++pos) {
1129 t_phase = ttft_clock::now();
1130 ++decode_steps;
1132 Logger::warning("Reached max sequence length (" +
1133 std::to_string(config_.max_position_embeddings) +
1134 "). Stopping.");
1135 break;
1136 }
1137
1138 int input_token_id;
1139
1140 if (pos == num_prompt_tokens && start_pos_for_loop == num_prompt_tokens) {
1141 // This is the first token *after* a successful prefill.
1142 // `next_token_id` was already sampled using prefill's logits from the *last prompt token*.
1143 // This `next_token_id` is the actual input for the current position `pos`.
1144 input_token_id = next_token_id;
1145 Logger::debug("[Generate Loop] First token post-prefill. Using prefill's next_token_id: " + std::to_string(input_token_id) + " for pos " + std::to_string(pos));
1146 } else {
1147 // Standard iterative logic:
1148 // If prefill didn't run (start_pos_for_loop == 0):
1149 // For pos < num_prompt_tokens: use prompt token.
1150 // For pos >= num_prompt_tokens: use previously sampled next_token_id.
1151 // If prefill did run (start_pos_for_loop == num_prompt_tokens):
1152 // This 'else' block is for pos > num_prompt_tokens, so use previously sampled next_token_id.
1153 input_token_id = (pos < num_prompt_tokens && start_pos_for_loop == 0) ? tokens[pos] : next_token_id;
1154 if (start_pos_for_loop == 0 && pos < num_prompt_tokens) {
1155 Logger::debug("[Generate Loop] No prefill, prompt token. Using tokens[" + std::to_string(pos) + "]: " + std::to_string(input_token_id) + " for pos " + std::to_string(pos));
1156 } else {
1157 Logger::debug("[Generate Loop] Standard generation. Using previously sampled next_token_id: " + std::to_string(input_token_id) + " for pos " + std::to_string(pos));
1158 }
1159 }
1160
1161 // Speculative step: one batched forward over [input, draft...] from pos
1162 // gives the logits at every position; tokens are taken greedily while
1163 // they equal the draft, plus the first one that does not (or a stop).
1164 // KV entries written for rejected draft positions lie past the valid
1165 // length and are overwritten by the next steps. Recurrent IR state has
1166 // absorbed them all: the next forward, from pos + accepted + 1, restores
1167 // the interpreter's snapshot after that row (all_rows keeps every row).
1168 if (spec_on && pos >= num_prompt_tokens) {
1169 const int room = std::min({spec_k, steps - generated_count, total_steps - pos - 1,
1171 const auto t_draft = ttft_clock::now();
1172 std::vector<int> hist(tokens.begin(), tokens.begin() + num_prompt_tokens);
1173 hist.insert(hist.end(), generated_token_ids.begin(), generated_token_ids.end());
1174 std::vector<int> draft = room > 0 ? prompt_lookup_draft(hist, room) : std::vector<int>{};
1175 if (draft.empty() && draft_table && room > 0) draft = draft_table->draft(hist, room, draft_theta);
1176 last_ttft_.spec_draft_ms += ms_between(t_draft, ttft_clock::now());
1178 // A draft never proposes a state id (the head has no logits for them).
1179 for (size_t i = 0; i < draft.size(); ++i)
1180 if (state_k_ > 0 && draft[i] >= state_vocab_) {
1181 draft.resize(i);
1182 break;
1183 }
1184 if (!draft.empty()) {
1185 std::vector<int> batch = {input_token_id};
1186 batch.insert(batch.end(), draft.begin(), draft.end());
1187 const int n = static_cast<int>(batch.size());
1188 const int hs = config_.hidden_size, vs = config_.vocab_size;
1189 std::vector<float> emb(ir_ ? 0 : (size_t)n * hs);
1190 for (int i = 0; i < (ir_ ? 0 : n); ++i) { // the interpreter embeds tokens itself
1191 const std::vector<float> e = model_->lookup_embedding(batch[i]);
1192 std::copy(e.begin(), e.end(), emb.begin() + (size_t)i * hs);
1193 }
1194 std::vector<float> all;
1195#ifdef HAS_CUDA
1196 // The verification pass as a captured CUDA graph with a greedy pick per
1197 // row on the device (speculative_verify.cpp); EMBER_VERIFY=legacy, or a
1198 // shape it does not cover, takes the passes below.
1199 static const bool verify_legacy = [] {
1200 const char* v = std::getenv("EMBER_VERIFY");
1201 return v && std::string(v) == "legacy";
1202 }();
1203 std::vector<ember_verify::RowPick> picks;
1204 const bool have_picks = !ir_ && !verify_legacy && config_.num_cpu_offload_layers == 0 &&
1205 model_->forward_device_verify_graph(batch, pos, &kv_cache_, &picks);
1206#else
1207 const bool have_picks = false;
1208#endif
1209 if (have_picks) {
1210 // every row's pick is in `picks`; its logits stay on the device
1211 } else if (ir_) {
1212 all = ir_->forward(batch, pos, /*all_rows=*/true);
1214 all = model_->forward_cpu_logits_batch(
1215 model_->forward_cpu_batch(emb, n, config_.num_hidden_layers, pos, &kv_cache_), n);
1216 } else {
1217#ifdef HAS_CUDA
1218 // The per-launch verification path; the batched prefill when it does
1219 // not apply.
1220 if (config_.num_cpu_offload_layers == 0) all = model_->forward_device_verify(batch, pos, &kv_cache_);
1221 if (all.empty()) {
1222 float* d_emb = nullptr;
1223 gpuErrchk(cudaMalloc(&d_emb, emb.size() * sizeof(float)));
1224 gpuErrchk(cudaMemcpy(d_emb, emb.data(), emb.size() * sizeof(float), cudaMemcpyHostToDevice));
1225 all = model_->forward_device_batch_prefill(d_emb, n, pos, &kv_cache_, 0, /*all_logits=*/true);
1226 gpuErrchk(cudaFree(d_emb));
1227 }
1228#endif
1229 }
1230 if (!have_picks && all.size() != (size_t)n * vs) {
1231 Logger::error("Speculative verification returned " + std::to_string(all.size()) + " logits; stopping.");
1232 break;
1233 }
1234 int accepted = 0;
1235 bool stop = false;
1236 for (int i = 0; i < n; ++i) {
1237 int t = -1;
1238#ifdef HAS_CUDA
1239 if (have_picks && repetition_penalty_ <= 1.0f) t = greedy_from_pick(picks[i], temperature, rng_);
1240 // The record cannot decide, or a debug dump wants the row: the row itself
1241 // (with the record's pick kept when it decided).
1242 if (have_picks && (t < 0 || logits_debug_on())) {
1243 std::vector<float> lg = model_->verify_row_logits(i);
1244 if (lg.size() != static_cast<size_t>(vs)) {
1245 Logger::error("Speculative verification: row " + std::to_string(i) + " unavailable; stopping.");
1246 stop = true;
1247 break;
1248 }
1249 apply_repetition_penalty(lg, generated_token_ids, repetition_penalty_);
1250 debug_dump_topk(lg);
1251 if (t < 0) t = sample_top_k_top_p_temperature(lg, temperature, top_k, top_p, rng_);
1252 }
1253#endif
1254 if (!have_picks) {
1255 std::vector<float> lg(all.begin() + (size_t)i * vs, all.begin() + (size_t)(i + 1) * vs);
1256 apply_repetition_penalty(lg, generated_token_ids, repetition_penalty_);
1257 debug_dump_topk(lg);
1258 t = sample_top_k_top_p_temperature(lg, temperature, top_k, top_p, rng_);
1259 }
1260 generated_token_ids.push_back(t);
1261 generated_count++;
1262 emit_token(t);
1263 generated_stream_ << decode_one(t);
1264 generated_text_for_api_return_ += decode_one(t);
1265 next_token_id = t;
1266 if (is_stop(t) || generated_count >= steps) { stop = true; break; }
1267 if (i + 1 < n && t == batch[i + 1]) { ++accepted; continue; }
1268 break;
1269 }
1270 last_ttft_.spec_drafted += static_cast<int>(draft.size());
1271 last_ttft_.spec_accepted += accepted;
1273 kv_cache_.seq_len = pos + accepted + 1; // input + accepted drafts are now cached
1274 host_phase("host.spec_step");
1275 if (stop) break;
1276 pos += accepted; // the loop's ++pos then moves to the next input position
1277 continue;
1278 }
1279 }
1280
1281 host_phase("host.loop_head");
1282 if (!ir_) current_data_host = model_->lookup_embedding(input_token_id);
1283 if (pos == 14 || pos == 15 || pos == 16) {
1284 log_vector_summary_detailed("[API_CPP GenLoop] current_data_host after lookup_embedding for input_token_id=" + std::to_string(input_token_id),
1285 current_data_host, pos, -100, 8);
1286 }
1287 host_phase("host.embed_lookup");
1288
1289 if (!ir_ && current_data_host.empty()) {
1290 Logger::error("Embedding lookup returned empty vector for token ID: " + std::to_string(input_token_id) + " at pos " + std::to_string(pos));
1291 break;
1292 }
1293
1294 // Mixed-mode forward pass logic
1295 if (ir_) {
1296 logits = ir_->forward({input_token_id}, pos);
1298#ifdef HAS_CUDA
1299 // Mixed CPU/GPU mode: First process CPU layers, then GPU layers
1300 Logger::debug("[Mixed Mode] Processing " + std::to_string(config_.num_cpu_offload_layers) + " CPU layers first");
1301 std::vector<float> intermediate_activations = model_->forward(current_data_host, pos, &kv_cache_, nullptr);
1302
1303 Logger::debug("[Mixed Mode] CPU layers complete, transferring to GPU for remaining layers");
1304 gpuErrchk(cudaMemcpy(model_->get_x_dev(), intermediate_activations.data(), intermediate_activations.size() * sizeof(float), cudaMemcpyHostToDevice));
1305 logits = model_->forward_device(model_->get_x_dev(), pos, &kv_cache_, nullptr);
1306#else
1307 Logger::error("[Mixed Mode] Mixed CPU/GPU mode requested but CUDA not available. Cannot proceed.");
1308 break;
1309#endif
1310 } else if (config_.num_cpu_offload_layers == 0) {
1311#ifdef HAS_CUDA
1312 // GPU-only mode: the captured decode graph when it applies, else the
1313 // per-launch forward.
1314 logits = model_->forward_device_graph(input_token_id, pos, &kv_cache_);
1315 host_phase("host.forward_device_graph");
1316 if (logits.empty()) {
1317 gpuErrchk(cudaMemcpy(model_->get_x_dev(), current_data_host.data(), current_data_host.size() * sizeof(float), cudaMemcpyHostToDevice));
1318 host_phase("host.embed_h2d");
1319 logits = model_->forward_device(model_->get_x_dev(), pos, &kv_cache_, nullptr);
1320 host_phase("host.forward_device(total)");
1321 }
1322#else
1323 Logger::error("[GPU-only Mode] GPU-only mode requested but CUDA not available. Cannot proceed.");
1324 break;
1325#endif
1326 } else {
1327 // CPU-only mode
1328 logits = model_->forward(current_data_host, pos, &kv_cache_, nullptr);
1329 }
1330
1331 // Sampling logic: Only sample if we're at the last prompt token or generating
1332 if (pos == num_prompt_tokens - 1 || pos >= num_prompt_tokens) {
1333 apply_repetition_penalty(logits, generated_token_ids, repetition_penalty_);
1334 apply_repeat_penalty(logits);
1335 debug_dump_topk(logits);
1336 next_token_id = sample_top_k_top_p_temperature(logits, temperature, top_k, top_p, rng_);
1337 host_phase("host.sample");
1338
1339 // Only add to generated tokens if we're actually generating (not just finishing prompt)
1340 if (pos >= num_prompt_tokens) {
1341 generated_token_ids.push_back(next_token_id);
1342 generated_count++;
1343 emit_token(next_token_id);
1344
1345 // Stream the generated token
1346 generated_stream_ << decode_one(next_token_id);
1347 generated_text_for_api_return_ += decode_one(next_token_id);
1348 } else {
1349 // This is the first token sampled from the last prompt position
1350 generated_token_ids.push_back(next_token_id);
1351 generated_count++;
1352 emit_token(next_token_id);
1353
1354 // Stream the first generated token
1355 generated_stream_ << decode_one(next_token_id);
1356 generated_text_for_api_return_ += decode_one(next_token_id);
1357 // Token-by-token path: only the total is recorded (batch_prefill=false).
1358 last_ttft_.t_first_token = ttft_clock::now();
1359 last_ttft_.first_token = true;
1361
1362 Logger::info("[Generate API] First token sampled from prompt: " + std::to_string(next_token_id) +
1363 ", Decoded: \"" + decode_one(next_token_id) + "\"");
1364 }
1365 }
1366
1367 if (is_stop(next_token_id) && pos >= (bpe_ ? num_prompt_tokens - 1 : num_prompt_tokens)) { // EOS only if we are generating
1368 Logger::info("EOS token (" + std::to_string(eos_token_id_) +
1369 ") sampled at pos " + std::to_string(pos) + ". Stopping.");
1370 break;
1371 }
1372
1373 if (generated_count >= steps) { // steps is max new tokens
1374 Logger::info("Reached max generation steps (" + std::to_string(steps) + "). Stopping.");
1375 break;
1376 }
1377
1378 // KVCache seq_len update for single token pass (already handled by batch prefill for its tokens)
1379 // This needs to happen *after* the forward pass for the current 'pos' has updated the cache for 'pos'.
1380 // So, after processing 'pos', the cache now contains information up to and including 'pos'.
1381 // The length of the sequence in the cache is pos + 1.
1382 if (!prefill_enabled || pos >= num_prompt_tokens) { // only update if not prefill, or if we are past prompt token processing in prefill case
1383 kv_cache_.seq_len = pos + 1;
1384 // Logger::debug("[Generate Loop] KVCache seq_len updated to: " + std::to_string(kv_cache_.seq_len) + " after processing pos " + std::to_string(pos));
1385 }
1386 host_phase("host.decode_text_and_rest");
1387 }
1388 if (decode_timing && decode_steps > 0) {
1389 const double total = ms_between(t_decode_start, ttft_clock::now());
1390 std::fprintf(stderr, "[decode timing] %d steps, %.3f ms/step wall (timed run, phases sync)\n", decode_steps,
1391 total / decode_steps);
1392 for (const auto& kv : host_phase_ms)
1393 std::fprintf(stderr, " %-34s %8.3f ms/step\n", kv.first.c_str(), kv.second / decode_steps);
1394#ifdef HAS_CUDA
1395 for (const auto& kv : model_->decode_phase_ms())
1396 std::fprintf(stderr, " gpu.%-30s %8.3f ms/step\n", kv.first.c_str(), kv.second / decode_steps);
1397#endif
1398 }
1399
1400 // Log all generated IDs before decoding
1401 std::string generated_ids_str = "[Generated IDs Pre-Decode] ";
1402 for(int gen_id : generated_token_ids) {
1403 generated_ids_str += std::to_string(gen_id) + " ";
1404 }
1405 Logger::debug(generated_ids_str);
1406
1407 std::string result;
1408 if (bpe_) {
1409 std::vector<int> out = generated_token_ids;
1410 if (!out.empty() && is_stop(out.back())) {
1411 last_stop_reason_ = bpe_->format().is_eos(out.back()) ? "eos" : "turn";
1412 last_stop_id_ = out.back();
1413 out.pop_back();
1414 }
1415 last_generated_tokens_ = static_cast<int>(out.size());
1417 result = bpe_->decode(out);
1418 // Strip leading and trailing whitespace (the C isspace set), as mtstudio does.
1419 const char* ws = " \t\n\v\f\r";
1420 const auto first = result.find_first_not_of(ws);
1421 const auto last = result.find_last_not_of(ws);
1422 result = first == std::string::npos ? std::string() : result.substr(first, last - first + 1);
1423 (void)stopped_at_first;
1424 } else {
1425 const bool stopped = !generated_token_ids.empty() && is_stop(generated_token_ids.back());
1426 if (stopped) last_stop_reason_ = "eos";
1427 last_generated_tokens_ = static_cast<int>(generated_token_ids.size()) - (stopped ? 1 : 0);
1428 result = tokenizer_->decode(generated_token_ids, true);
1429 }
1430 Logger::info("Generated response: " + ember::utf8_valid(result));
1431
1432 // What the KV cache now holds: the prompt plus every generated token that
1433 // was fed back through the model (all but the last one sampled).
1434 kv_token_history_ = tokens;
1435 if (!generated_token_ids.empty()) {
1436 kv_token_history_.insert(kv_token_history_.end(), generated_token_ids.begin(),
1437 generated_token_ids.end() - 1);
1438 }
1439 // Per-turn state tokens: a reply that ended with a generated <|endoftext|> is followed by the k
1440 // state ids, prefilled with it (no sampling), so the cache holds the finished exchange as the
1441 // next request's prompt writes it. A reply that stopped early gets <|endoftext|> and the state
1442 // ids from the next prompt instead (state_tokens.h).
1443 if (ir_ && state_k_ > 0 && !generated_token_ids.empty() && bpe_->format().is_eos(generated_token_ids.back())) {
1444 const int at = static_cast<int>(kv_token_history_.size());
1445 if (at + 1 + state_k_ <= config_.max_position_embeddings) {
1446 std::vector<int> close = {generated_token_ids.back()};
1447 for (int j = 0; j < state_k_; ++j) close.push_back(state_vocab_ + j);
1448 ir_->forward(close, at);
1449 kv_token_history_.insert(kv_token_history_.end(), close.begin(), close.end());
1450 }
1451 }
1452
1453 auto t_end = std::chrono::high_resolution_clock::now(); // End timing
1454 double time_taken_ms = std::chrono::duration<double, std::milli>(t_end - t_start).count();
1455
1456 // Create a string with the desired precision for the time
1457 std::ostringstream time_ss;
1458 time_ss << std::fixed << std::setprecision(4) << time_taken_ms;
1459 Logger::info("[INFO] Total generation processing time: " + time_ss.str() + " ms");
1460
1461 return result;
1462}
1463
1464int TinyLlamaSession::sample_step(std::vector<float>& logits, const std::vector<int>& context, float temperature,
1465 int top_k, float top_p) {
1466 if (!bpe_) throw std::runtime_error("sample_step: chat models only");
1467 std::vector<char> seen;
1468 apply_contract_penalty(logits, context, std::vector<int>(), repeat_penalty_, repeat_last_n_,
1469 [this](int id) { return bpe_->format().is_stop(id); }, seen);
1470 return sample_top_k_top_p_temperature(logits, temperature, top_k, top_p, rng_);
1471}
1472
1473std::vector<std::string> TinyLlamaSession::generate_batch(const std::vector<std::string>& prompts,
1474 int steps,
1475 float temperature,
1476 int top_k, float top_p,
1477 const std::string& system_prompt_arg,
1478 bool apply_q_a_format_cli_hint) {
1479 auto t_start = std::chrono::high_resolution_clock::now();
1480
1481 if (prompts.empty()) {
1482 throw std::runtime_error("Cannot process empty prompts vector for batch generation.");
1483 }
1484
1485 if (static_cast<int>(prompts.size()) > max_batch_size_) {
1486 throw std::runtime_error("Batch size " + std::to_string(prompts.size()) +
1487 " exceeds maximum batch size " + std::to_string(max_batch_size_));
1488 }
1489
1490 Logger::info("[Batch Generate API] Processing " + std::to_string(prompts.size()) +
1491 " prompts in batch. Steps: " + std::to_string(steps));
1492
1493 if (!model_ || !tokenizer_) {
1494 throw std::runtime_error("Model or tokenizer not loaded for batch generation.");
1495 }
1496
1497 // Process each prompt to create final prompts and tokenize them
1498 std::vector<std::string> final_prompts(prompts.size());
1499 std::vector<std::vector<int>> all_tokens(prompts.size());
1500 std::vector<int> prompt_lengths(prompts.size());
1501 int max_prompt_length = 0;
1502
1503 for (size_t i = 0; i < prompts.size(); ++i) {
1504 // Apply same prompt processing logic as single generate()
1505 std::string final_prompt_for_encoding;
1506 bool used_chat_template = false;
1507
1508 // Same priority logic as single generate()
1509 if (apply_q_a_format_cli_hint) {
1510 Logger::info("[Batch Generate API] Using legacy Q/A formatting for prompt " + std::to_string(i));
1511 if (!system_prompt_arg.empty()) {
1512 final_prompt_for_encoding = system_prompt_arg + "\\n\\nQ: " + prompts[i] + "\\nA:";
1513 } else {
1514 final_prompt_for_encoding = "Q: " + prompts[i] + "\\nA:";
1515 }
1516 } else if (tokenizer_ && !tokenizer_->get_gguf_chat_template().empty()) {
1517 std::string gguf_template_content = tokenizer_->get_gguf_chat_template();
1518 bool is_llama_sentencepiece_family = (config_.tokenizer_family == ModelConfig::TokenizerFamily::LLAMA_SENTENCEPIECE);
1519 bool looks_like_jinja = (gguf_template_content.find("{%") != std::string::npos);
1520
1521 if (is_llama_sentencepiece_family && looks_like_jinja) {
1522 Logger::info("[Batch Generate API] Using Q/A format override for prompt " + std::to_string(i));
1523 if (!system_prompt_arg.empty()) {
1524 final_prompt_for_encoding = system_prompt_arg + "\\\\n\\\\nQ: " + prompts[i] + "\\\\nA:";
1525 } else {
1526 final_prompt_for_encoding = "Q: " + prompts[i] + "\\\\nA:";
1527 }
1528 } else {
1529 Logger::info("[Batch Generate API] Using GGUF chat template for prompt " + std::to_string(i));
1530 final_prompt_for_encoding = tokenizer_->apply_chat_template(prompts[i], system_prompt_arg, config_);
1531 used_chat_template = true;
1532 }
1534 Logger::info("[Batch Generate API] Using Llama 3 chat template for prompt " + std::to_string(i));
1535 final_prompt_for_encoding = tokenizer_->apply_chat_template(prompts[i], system_prompt_arg, config_);
1536 used_chat_template = true;
1537 } else {
1538 Logger::info("[Batch Generate API] Using raw prompt for prompt " + std::to_string(i));
1539 if (!system_prompt_arg.empty()) {
1540 final_prompt_for_encoding = system_prompt_arg + "\\n\\n" + prompts[i];
1541 } else {
1542 final_prompt_for_encoding = prompts[i];
1543 }
1544 }
1545
1546 final_prompts[i] = final_prompt_for_encoding;
1547 all_tokens[i] = tokenizer_->encode(final_prompt_for_encoding, true, false, Tokenizer::PreTokenizeMethod::DEFAULT);
1548
1549 if (all_tokens[i].empty()) {
1550 Logger::warning("Batch tokenization resulted in empty ID list for prompt " + std::to_string(i));
1551 all_tokens[i].push_back(tokenizer_->bos_token_id()); // Fallback to BOS token
1552 }
1553
1554 prompt_lengths[i] = all_tokens[i].size();
1555 max_prompt_length = std::max(max_prompt_length, prompt_lengths[i]);
1556
1557 Logger::info("[Batch Generate API] Prompt " + std::to_string(i) + ": " +
1558 std::to_string(prompt_lengths[i]) + " tokens");
1559 }
1560
1561 // Initialize batch mode
1562 kv_cache_.initialize_batch(static_cast<int>(prompts.size()));
1564 kv_token_history_.clear(); // the cache no longer holds the last generate()'s tokens
1565
1566 std::vector<std::string> results(prompts.size());
1567
1568 // Try parallel batch processing if enabled
1570 Logger::info("[Batch Generate API] Using parallel batch processing");
1571
1572 Logger::info("[DEBUG] Initializing KV cache for batch mode");
1573 kv_cache_.initialize_batch(static_cast<int>(prompts.size()));
1574 Logger::info("[DEBUG] KV cache batch initialization completed");
1575
1576 Logger::info("[DEBUG] Clearing KV cache data");
1578 kv_token_history_.clear(); // the cache no longer holds the last generate()'s tokens
1579 Logger::info("[DEBUG] KV cache clear completed");
1580
1581 // Phase 1: Parallel Batch Prefill
1582 Logger::info("[DEBUG] About to call batch_prefill_parallel");
1583 Logger::info("[DEBUG] all_tokens.size()=" + std::to_string(all_tokens.size()) + ", prompt_lengths.size()=" + std::to_string(prompt_lengths.size()));
1584
1585 std::vector<std::vector<float>> batch_final_logits;
1586
1587 bool prefill_success = batch_prefill_parallel(all_tokens, prompt_lengths, batch_final_logits);
1588
1589 if (prefill_success && batch_final_logits.size() == prompts.size()) {
1590 Logger::info("[Batch Generate API] Batch prefill successful, starting parallel generation");
1591
1592 // Add safety check for batch_final_logits
1593 Logger::info("[DEBUG] Checking batch_final_logits integrity after prefill");
1594 for (size_t i = 0; i < batch_final_logits.size(); ++i) {
1595 if (batch_final_logits[i].empty()) {
1596 Logger::error("[DEBUG] batch_final_logits[" + std::to_string(i) + "] is empty!");
1597 goto fallback_sequential;
1598 }
1599 if (batch_final_logits[i].size() != static_cast<size_t>(config_.vocab_size)) {
1600 Logger::error("[DEBUG] batch_final_logits[" + std::to_string(i) + "] has wrong size: " +
1601 std::to_string(batch_final_logits[i].size()) + " vs expected " + std::to_string(config_.vocab_size));
1602 goto fallback_sequential;
1603 }
1604 // Check for NaN/Inf values
1605 for (size_t j = 0; j < std::min(static_cast<size_t>(10UL), batch_final_logits[i].size()); ++j) {
1606 if (!std::isfinite(batch_final_logits[i][j])) {
1607 Logger::error("[DEBUG] batch_final_logits[" + std::to_string(i) + "][" + std::to_string(j) + "] is not finite: " + std::to_string(batch_final_logits[i][j]));
1609 }
1610 }
1611 }
1612 Logger::info("[DEBUG] batch_final_logits integrity check passed");
1613
1614 // Sample first tokens for all sequences
1615 std::vector<int> current_tokens(prompts.size());
1616 std::vector<std::vector<int>> all_generated_tokens(prompts.size());
1617 std::vector<int> sequence_positions(prompts.size());
1618 std::vector<bool> sequence_finished(prompts.size(), false);
1619
1620 Logger::info("[DEBUG] Starting token sampling for " + std::to_string(prompts.size()) + " sequences");
1621
1622 for (size_t i = 0; i < prompts.size(); ++i) {
1623 Logger::info("[DEBUG] Sampling token for sequence " + std::to_string(i));
1624
1625 // Safety check before sampling
1626 if (i >= batch_final_logits.size()) {
1627 Logger::error("[DEBUG] Index " + std::to_string(i) + " out of bounds for batch_final_logits (size: " + std::to_string(batch_final_logits.size()) + ")");
1629 }
1630
1631 try {
1633 Logger::info("[DEBUG] Sampled token " + std::to_string(current_tokens[i]) + " for sequence " + std::to_string(i));
1634 } catch (const std::exception& e) {
1635 Logger::error("[DEBUG] Exception during sampling for sequence " + std::to_string(i) + ": " + std::string(e.what()));
1637 }
1638
1640 sequence_positions[i] = prompt_lengths[i]; // Position for next token
1641
1642 // Check for EOS
1643 if (current_tokens[i] == eos_token_id_) {
1644 sequence_finished[i] = true;
1645 Logger::info("[DEBUG] Sequence " + std::to_string(i) + " finished with EOS token");
1646 }
1647 }
1648
1649 Logger::info("[DEBUG] Token sampling completed, starting generation loop");
1650
1651 // Phase 2: Parallel Batch Generation
1652 for (int step = 1; step < steps; ++step) {
1653 Logger::info("[DEBUG] Starting generation step " + std::to_string(step));
1654
1655 // Check if all sequences are finished
1656 bool all_finished = true;
1657 for (bool finished : sequence_finished) {
1658 if (!finished) {
1659 all_finished = false;
1660 break;
1661 }
1662 }
1663 if (all_finished) {
1664 Logger::info("[Batch Generate API] All sequences finished at step " + std::to_string(step));
1665 break;
1666 }
1667
1668 Logger::info("[DEBUG] Collecting active sequences for step " + std::to_string(step));
1669
1670 // Collect active sequences
1671 std::vector<int> active_tokens;
1672 std::vector<int> active_positions;
1673 std::vector<int> active_sequence_indices;
1674 std::vector<int> batch_to_original_seq_mapping; // Map batch index to original sequence index
1675
1676 for (size_t i = 0; i < prompts.size(); ++i) {
1677 if (!sequence_finished[i]) {
1678 active_tokens.push_back(current_tokens[i]);
1680 active_sequence_indices.push_back(active_tokens.size() - 1); // Use contiguous 0-based index
1681 batch_to_original_seq_mapping.push_back(i); // Remember original sequence index
1682 Logger::info("[DEBUG] Active sequence " + std::to_string(i) + " mapped to batch index " + std::to_string(active_tokens.size() - 1) +
1683 ": token=" + std::to_string(current_tokens[i]) + ", pos=" + std::to_string(sequence_positions[i]));
1684 }
1685 }
1686
1687 if (active_tokens.empty()) {
1688 Logger::info("[DEBUG] No active tokens, breaking from generation loop");
1689 break;
1690 }
1691
1692 Logger::info("[DEBUG] About to call batch_generation_parallel with " + std::to_string(active_tokens.size()) + " active sequences");
1693
1694 // Process active sequences in parallel
1695 std::vector<std::vector<float>> step_logits;
1697 Logger::info("[DEBUG] batch_generation_parallel returned: " + std::string(generation_success ? "success" : "failure"));
1698
1699 if (!generation_success || step_logits.size() != active_tokens.size()) {
1700 Logger::warning("[Batch Generate API] Parallel generation failed at step " + std::to_string(step) +
1701 ", falling back to sequential processing");
1703 }
1704
1705 Logger::info("[DEBUG] Starting token sampling for step " + std::to_string(step));
1706
1707 // Sample next tokens for active sequences
1708 for (size_t active_idx = 0; active_idx < active_tokens.size(); ++active_idx) {
1709 size_t original_seq_idx = batch_to_original_seq_mapping[active_idx]; // Use mapping to get original sequence index
1710 Logger::info("[DEBUG] Sampling for active_idx=" + std::to_string(active_idx) + ", original_seq_idx=" + std::to_string(original_seq_idx));
1711
1712 // Safety checks
1713 if (active_idx >= step_logits.size()) {
1714 Logger::error("[DEBUG] active_idx " + std::to_string(active_idx) + " out of bounds for step_logits (size: " + std::to_string(step_logits.size()) + ")");
1716 }
1717 if (original_seq_idx >= prompts.size()) {
1718 Logger::error("[DEBUG] original_seq_idx " + std::to_string(original_seq_idx) + " out of bounds for prompts (size: " + std::to_string(prompts.size()) + ")");
1720 }
1721
1722 try {
1723 int next_token = sample_top_k_top_p_temperature(step_logits[active_idx], temperature, top_k, top_p, rng_);
1724 Logger::info("[DEBUG] Sampled next token " + std::to_string(next_token) + " for original_seq_idx " + std::to_string(original_seq_idx));
1725
1729
1730 // Check for EOS
1731 if (next_token == eos_token_id_) {
1733 Logger::info("[DEBUG] Sequence " + std::to_string(original_seq_idx) + " finished with EOS at step " + std::to_string(step));
1734 }
1735 } catch (const std::exception& e) {
1736 Logger::error("[DEBUG] Exception during sampling at step " + std::to_string(step) + " for original_seq_idx " + std::to_string(original_seq_idx) + ": " + std::string(e.what()));
1738 }
1739 }
1740
1741 Logger::info("[DEBUG] Completed generation step " + std::to_string(step));
1742 }
1743
1744 // Decode results for all sequences
1745 for (size_t i = 0; i < prompts.size(); ++i) {
1746 results[i] = tokenizer_->decode(all_generated_tokens[i], true);
1747 }
1748
1749 Logger::info("[Batch Generate API] Parallel batch processing completed successfully");
1750 } else {
1751 Logger::warning("[Batch Generate API] Batch prefill failed, falling back to sequential processing");
1753 }
1754 } else {
1756 Logger::info("[Batch Generate API] Using sequential processing");
1757
1758 for (size_t i = 0; i < prompts.size(); ++i) {
1759 Logger::info("[Batch Generate API] Processing prompt " + std::to_string(i + 1) +
1760 "/" + std::to_string(prompts.size()));
1761
1762 // Reset to single-sequence mode for this prompt
1763 kv_cache_.seq_len = 0;
1764 kv_token_history_.clear(); // the cache no longer holds the last generate()'s tokens
1765
1766 // Use existing single-sequence generation logic
1767 std::string result = generate(prompts[i], steps, temperature, top_k, top_p,
1769 results[i] = result;
1770 }
1771 }
1772
1773 auto t_end = std::chrono::high_resolution_clock::now();
1774 double time_taken_ms = std::chrono::duration<double, std::milli>(t_end - t_start).count();
1775
1776 std::ostringstream time_ss;
1777 time_ss << std::fixed << std::setprecision(4) << time_taken_ms;
1778 Logger::info("[Batch Generate API] Total batch processing time: " + time_ss.str() + " ms for " +
1779 std::to_string(prompts.size()) + " prompts");
1780
1781 return results;
1782}
1783
1784bool TinyLlamaSession::batch_prefill_parallel(const std::vector<std::vector<int>>& all_tokens,
1785 const std::vector<int>& prompt_lengths,
1786 std::vector<std::vector<float>>& batch_final_logits) {
1787 Logger::info("[EMERGENCY_DEBUG] batch_prefill_parallel function entry - FIRST LINE");
1788 Logger::info("[DEBUG] Entering batch_prefill_parallel");
1789 // Calculate total tokens across all prompts for batch prefill
1791 for (int len : prompt_lengths) {
1793 }
1794
1796 Logger::error("[Batch Prefill] No tokens to process in batch prefill.");
1797 return false;
1798 }
1799
1800 Logger::info("[Batch Prefill] Processing " + std::to_string(all_tokens.size()) +
1801 " sequences with total " + std::to_string(total_tokens_across_all_prompts) + " tokens");
1802
1803 // Process all tokens for all sequences in batch
1804 Logger::info("[Batch Prefill] Preparing batch embeddings for " +
1805 std::to_string(total_tokens_across_all_prompts) + " tokens");
1806
1807 // Calculate required memory
1808 size_t required_memory_bytes = static_cast<size_t>(total_tokens_across_all_prompts) * config_.hidden_size * sizeof(float);
1809 Logger::info("[DEBUG] About to allocate " + std::to_string(required_memory_bytes) + " bytes (" +
1810 std::to_string(required_memory_bytes / (1024*1024)) + " MB) for batch embeddings");
1811
1813 Logger::info("[DEBUG] batch_embeddings allocation completed successfully");
1814
1815 int token_offset = 0;
1816
1817 // Add detailed logging for token processing
1818 Logger::info("[DEBUG] Starting token embedding processing for " + std::to_string(all_tokens.size()) + " sequences");
1819
1820 for (size_t seq_idx = 0; seq_idx < all_tokens.size(); ++seq_idx) {
1821 Logger::info("[DEBUG] Processing sequence " + std::to_string(seq_idx) + " with " + std::to_string(prompt_lengths[seq_idx]) + " tokens");
1822
1823 // Log first few tokens of this sequence
1824 std::string token_ids_str = "Token IDs: ";
1825 for (int i = 0; i < std::min(5, prompt_lengths[seq_idx]); ++i) {
1826 token_ids_str += std::to_string(all_tokens[seq_idx][i]) + " ";
1827 }
1828 if (prompt_lengths[seq_idx] > 5) token_ids_str += "...";
1829 Logger::info("[DEBUG] Sequence " + std::to_string(seq_idx) + " " + token_ids_str);
1830
1831 for (int token_idx = 0; token_idx < prompt_lengths[seq_idx]; ++token_idx) {
1833
1834 // Log token placement in batch
1835 if (seq_idx < 2 && token_idx < 3) { // Only log first few tokens of first two sequences
1836 Logger::info("[DEBUG] Placing token " + std::to_string(current_token_id) +
1837 " from seq " + std::to_string(seq_idx) + " pos " + std::to_string(token_idx) +
1838 " at batch offset " + std::to_string(token_offset));
1839 }
1840
1841 std::vector<float> token_embedding = model_->lookup_embedding(current_token_id);
1842 if (token_embedding.empty() || token_embedding.size() != static_cast<size_t>(config_.hidden_size)) {
1843 Logger::error("[Batch Prefill] Embedding lookup failed for token " +
1844 std::to_string(current_token_id) +
1845 " in sequence " + std::to_string(seq_idx));
1846 return false;
1847 }
1848
1849 // Ensure we don't write beyond bounds
1852 Logger::error("[Batch Prefill] Buffer overflow detected at token offset " + std::to_string(token_offset));
1853 return false;
1854 }
1855
1856 std::copy(token_embedding.begin(), token_embedding.end(),
1858 token_offset++;
1859 }
1860
1861 Logger::info("[DEBUG] Sequence " + std::to_string(seq_idx) + " complete. Next token_offset: " + std::to_string(token_offset));
1862 }
1863
1864 // Process CPU layers if any
1865 std::vector<float> cpu_processed_embeddings;
1867 Logger::info("[Batch Prefill] Processing " + std::to_string(config_.num_cpu_offload_layers) +
1868 " CPU layers for batch prefill");
1869 cpu_processed_embeddings = model_->forward_cpu_batch(batch_embeddings,
1872 0, &kv_cache_,
1874
1875 if (cpu_processed_embeddings.empty()) {
1876 Logger::error("[Batch Prefill] CPU batch processing failed.");
1877 return false;
1878 }
1879 } else {
1881 }
1882
1883 // Process GPU layers if any
1884 std::vector<float> final_batch_logits;
1885
1887 // All CPU - get logits from CPU
1888 Logger::info("[Batch Prefill] All layers on CPU, computing logits");
1889 final_batch_logits = model_->forward_cpu_logits_batch(cpu_processed_embeddings,
1891 } else {
1892 // GPU layers exist - transfer to GPU and process
1893#ifdef HAS_CUDA
1894 Logger::info("[Batch Prefill] Processing GPU layers for batch prefill");
1895
1896 Logger::info("[DEBUG] About to allocate GPU memory for batch prefill");
1897 float* d_batch_embeddings = nullptr;
1898 size_t batch_size_bytes = cpu_processed_embeddings.size() * sizeof(float);
1899 Logger::info("[DEBUG] GPU allocation size: " + std::to_string(batch_size_bytes) + " bytes (" +
1900 std::to_string(batch_size_bytes / (1024*1024)) + " MB)");
1901
1902 Logger::info("[DEBUG] Calling cudaMalloc...");
1904 Logger::info("[DEBUG] cudaMalloc completed successfully");
1905
1906 Logger::info("[DEBUG] Calling cudaMemcpy host to device...");
1909 Logger::info("[DEBUG] cudaMemcpy completed successfully");
1910
1911 // Call forward_device_batch_prefill ONCE with all the batch data
1912 Logger::info("[DEBUG] Calling forward_device_batch_prefill with " + std::to_string(total_tokens_across_all_prompts) + " total tokens");
1913 std::vector<float> all_batch_logits = model_->forward_device_batch_prefill(
1915
1916 Logger::info("[DEBUG] forward_device_batch_prefill completed, returned " + std::to_string(all_batch_logits.size()) + " total logits");
1917
1919
1921#else
1922 Logger::error("[Batch Prefill] GPU processing requested but CUDA not available.");
1923 return false;
1924#endif
1925 }
1926
1927 Logger::info("[Batch Prefill] Successfully processed batch prefill for " +
1928 std::to_string(all_tokens.size()) + " sequences");
1929
1930 Logger::info("[DEBUG] About to return from batch_prefill_parallel");
1931 Logger::info("[DEBUG] batch_final_logits.size()=" + std::to_string(batch_final_logits.size()));
1932 for (size_t i = 0; i < batch_final_logits.size() && i < 3; ++i) {
1933 Logger::info("[DEBUG] batch_final_logits[" + std::to_string(i) + "].size()=" + std::to_string(batch_final_logits[i].size()));
1934 }
1935
1936 // Extract logits for the last token of each sequence
1937 batch_final_logits.clear();
1938 batch_final_logits.resize(all_tokens.size());
1939
1941 // For CPU-only, extract last token logits from the flat array
1942 if (final_batch_logits.size() != static_cast<size_t>(total_tokens_across_all_prompts * config_.vocab_size)) {
1943 Logger::error("[Batch Prefill] CPU logits size mismatch. Expected: " +
1945 ", got: " + std::to_string(final_batch_logits.size()));
1946 return false;
1947 }
1948
1949 int token_offset = 0;
1950 for (size_t seq_idx = 0; seq_idx < all_tokens.size(); ++seq_idx) {
1952
1953 Logger::info("[DEBUG] Extracting logits for sequence " + std::to_string(seq_idx) +
1954 ": token_offset=" + std::to_string(token_offset) +
1955 ", prompt_length=" + std::to_string(prompt_lengths[seq_idx]) +
1956 ", last_token_pos=" + std::to_string(last_token_pos));
1957
1959
1960 // Bounds check before copying
1963 if (src_end > final_batch_logits.size()) {
1964 Logger::error("[Batch Prefill] CPU logits bounds check failed for sequence " + std::to_string(seq_idx));
1965 return false;
1966 }
1967
1968 std::copy(final_batch_logits.begin() + src_start,
1969 final_batch_logits.begin() + src_end,
1970 batch_final_logits[seq_idx].begin());
1971
1972 // Log a few logit values for debugging
1973 if (seq_idx < 2) { // Only for first two sequences
1974 std::string logit_sample = "First 5 logits: ";
1975 for (int i = 0; i < 5 && i < config_.vocab_size; ++i) {
1976 logit_sample += std::to_string(batch_final_logits[seq_idx][i]) + " ";
1977 }
1978 Logger::info("[DEBUG] Sequence " + std::to_string(seq_idx) + " " + logit_sample);
1979 }
1980
1982 }
1983 } else {
1984 // For GPU, check if logits are for all tokens or just last tokens
1985 Logger::info("[DEBUG] GPU batch logits size: " + std::to_string(final_batch_logits.size()) +
1986 ", expected for all tokens: " + std::to_string(total_tokens_across_all_prompts * config_.vocab_size) +
1987 ", expected for last tokens only: " + std::to_string(all_tokens.size() * config_.vocab_size));
1988
1989 if (final_batch_logits.size() == static_cast<size_t>(total_tokens_across_all_prompts * config_.vocab_size)) {
1990 // GPU returned logits for all tokens, extract last token for each sequence
1991 Logger::info("[DEBUG] GPU returned logits for all tokens, extracting last token logits");
1992 int token_offset = 0;
1993 for (size_t seq_idx = 0; seq_idx < all_tokens.size(); ++seq_idx) {
1995
1996 Logger::info("[DEBUG] GPU: Extracting logits for sequence " + std::to_string(seq_idx) +
1997 ": token_offset=" + std::to_string(token_offset) +
1998 ", prompt_length=" + std::to_string(prompt_lengths[seq_idx]) +
1999 ", last_token_pos=" + std::to_string(last_token_pos));
2000
2002
2005 if (src_end > final_batch_logits.size()) {
2006 Logger::error("[Batch Prefill] GPU logits bounds check failed for sequence " + std::to_string(seq_idx));
2007 return false;
2008 }
2009
2010 std::copy(final_batch_logits.begin() + src_start,
2011 final_batch_logits.begin() + src_end,
2012 batch_final_logits[seq_idx].begin());
2013
2014 // Log a few logit values for debugging
2015 if (seq_idx < 2) { // Only for first two sequences
2016 std::string logit_sample = "First 5 logits: ";
2017 for (int i = 0; i < 5 && i < config_.vocab_size; ++i) {
2018 logit_sample += std::to_string(batch_final_logits[seq_idx][i]) + " ";
2019 }
2020 Logger::info("[DEBUG] GPU Sequence " + std::to_string(seq_idx) + " " + logit_sample);
2021 }
2022
2024 }
2025 } else if (final_batch_logits.size() == static_cast<size_t>(all_tokens.size() * config_.vocab_size)) {
2026 // GPU returned logits for last tokens only
2027 Logger::info("[DEBUG] GPU returned logits for last tokens only");
2028 for (size_t seq_idx = 0; seq_idx < all_tokens.size(); ++seq_idx) {
2029 Logger::info("[DEBUG] GPU Last-Token-Only: Processing sequence " + std::to_string(seq_idx) +
2030 " at logit offset " + std::to_string(seq_idx * config_.vocab_size));
2031
2033
2036 if (src_end > final_batch_logits.size()) {
2037 Logger::error("[Batch Prefill] GPU logits bounds check failed for sequence " + std::to_string(seq_idx));
2038 return false;
2039 }
2040
2041 std::copy(final_batch_logits.begin() + src_start,
2042 final_batch_logits.begin() + src_end,
2043 batch_final_logits[seq_idx].begin());
2044
2045 // Log a few logit values for debugging
2046 if (seq_idx < 2) { // Only for first two sequences
2047 std::string logit_sample = "First 5 logits: ";
2048 for (int i = 0; i < 5 && i < config_.vocab_size; ++i) {
2049 logit_sample += std::to_string(batch_final_logits[seq_idx][i]) + " ";
2050 }
2051 Logger::info("[DEBUG] GPU Last-Token Sequence " + std::to_string(seq_idx) + " " + logit_sample);
2052 }
2053 }
2054 } else {
2055 Logger::error("[Batch Prefill] GPU logits size doesn't match expected patterns");
2056 return false;
2057 }
2058 }
2059
2060 return true;
2061}
2062
2064 const std::vector<int>& token_positions,
2065 const std::vector<int>& sequence_indices,
2066 std::vector<std::vector<float>>& batch_logits) {
2067 Logger::info("[DEBUG] Entering batch_generation_parallel");
2068
2069 int num_sequences = current_tokens.size();
2070
2071 if (num_sequences == 0 || token_positions.size() != current_tokens.size()) {
2072 Logger::error("[Batch Generation] Invalid input sizes");
2073 return false;
2074 }
2075
2076 Logger::info("[Batch Generation] Processing " + std::to_string(num_sequences) +
2077 " sequences in parallel generation step");
2078
2079 // Create batch embeddings for all sequences
2080 std::vector<float> batch_embeddings;
2082
2083 for (int i = 0; i < num_sequences; ++i) {
2084 std::vector<float> token_embedding = model_->lookup_embedding(current_tokens[i]);
2085 if (token_embedding.empty()) {
2086 Logger::error("[Batch Generation] Embedding lookup failed for token " + std::to_string(current_tokens[i]));
2087 return false;
2088 }
2090 }
2091
2092 // Process through CPU layers if any
2094 Logger::info("[Batch Generation] Processing " + std::to_string(config_.num_cpu_offload_layers) +
2095 " CPU layers for batch generation");
2096
2097 std::vector<std::vector<float>> cpu_batch_logits = model_->forward_cpu_batch_generation(
2099
2100 if (cpu_batch_logits.size() != static_cast<size_t>(num_sequences)) {
2101 Logger::error("[Batch Generation] CPU batch generation returned wrong number of results");
2102 return false;
2103 }
2104
2105 // If all layers are on CPU, we're done
2108 Logger::info("[Batch Generation] All CPU layers processed, returning logits");
2109 return true;
2110 }
2111
2112 // Convert CPU results back to batch embeddings for GPU processing
2113 batch_embeddings.clear();
2115 // Note: This would need the CPU layer output activations, not logits
2116 // For now, fall back to sequential processing if mixed CPU/GPU
2117 Logger::warning("[Batch Generation] Mixed CPU/GPU not yet implemented for batch generation");
2118 return false;
2119 }
2120
2121 // GPU-only processing
2123#ifdef HAS_CUDA
2124 Logger::info("[Batch Generation] Processing GPU layers for batch generation");
2125
2126 float* d_batch_embeddings = nullptr;
2127 size_t batch_size_bytes = batch_embeddings.size() * sizeof(float);
2128
2132
2133 std::vector<std::vector<float>> gpu_batch_logits = model_->forward_device_batch_generation(
2135
2137
2138 if (gpu_batch_logits.size() != static_cast<size_t>(num_sequences)) {
2139 Logger::error("[Batch Generation] GPU batch generation returned wrong number of results");
2140 return false;
2141 }
2142
2144 Logger::info("[Batch Generation] GPU batch generation completed successfully");
2145 return true;
2146#else
2147 Logger::error("[Batch Generation] GPU processing requested but CUDA not available.");
2148 return false;
2149#endif
2150 }
2151
2152 Logger::error("[Batch Generation] No valid processing path found");
2153 return false;
2154}
2155
2156} // namespace tinyllama
static void debug(const std::string &message)
Definition logger.cpp:131
static void warning(const std::string &message)
Definition logger.cpp:139
static void info(const std::string &message)
Definition logger.cpp:135
static void error(const std::string &message)
Definition logger.cpp:143
Main class for loading tensors from SafeTensors format files (single or sharded)
static bool load_model_config_from_json(const std::string &model_path_or_dir, ModelConfig &config_to_populate)
Loads model configuration from a JSON file corresponding to a .safetensors model path.
bool load(const std::string &path, int vocab_size, std::string *why)
Definition draft_table.h:31
std::vector< int > draft(const std::vector< int > &h, int k, double theta) const
Definition draft_table.h:82
size_t entries() const
Definition draft_table.h:75
ModelConfig config_
Definition api.h:294
bool batch_generation_parallel(const std::vector< int > &current_tokens, const std::vector< int > &token_positions, const std::vector< int > &original_sequence_indices, std::vector< std::vector< float > > &batch_logits)
Definition api.cpp:2063
std::string chat_template_why_
Definition api.h:309
std::unique_ptr< ember::ChatTokenizer > bpe_
Definition api.h:307
bool batch_prefill_parallel(const std::vector< std::vector< int > > &all_tokens, const std::vector< int > &prompt_lengths, std::vector< std::vector< float > > &batch_final_logits)
Definition api.cpp:1784
std::mt19937 rng_
Definition api.h:297
std::stringstream generated_stream_
Definition api.h:303
std::vector< int > last_prompt_ids_
Definition api.h:312
std::string last_stop_reason_
Definition api.h:310
std::unique_ptr< TinyLlamaModel > model_
Definition api.h:288
TinyLlamaSession(const std::string &model_path, const std::string &tokenizer_path, int threads=1, int num_gpu_layers_from_cli=0, bool cli_use_mmap=true, bool use_kv_quant=false, bool use_batch_generation=false, int max_batch_size=1)
Loads the model, config, and tokenizer from the specified directory or GGUF file.
Definition api.cpp:328
std::vector< std::vector< int > > scripted_replies_
Definition api.h:315
TtftBreakdown last_ttft_
Definition api.h:305
std::function< void(int)> token_callback_
Definition api.h:314
std::vector< int > bpe_prompt_ids(const std::string &text) const
Definition api.h:213
std::unique_ptr< ember_ir::Interpreter > ir_
Definition api.h:291
std::unique_ptr< Tokenizer > tokenizer_
Definition api.h:293
std::vector< int > kv_token_history_
Definition api.h:306
std::string generate(const std::string &prompt, int steps=128, float temperature=0.1f, int top_k=40, float top_p=0.9f, const std::string &system_prompt="", bool apply_q_a_format=false)
Generates text based on a given prompt.
Definition api.cpp:706
std::string ir_doc_
Definition api.h:292
std::vector< std::string > generate_batch(const std::vector< std::string > &prompts, int steps=128, float temperature=0.1f, int top_k=40, float top_p=0.9f, const std::string &system_prompt="", bool apply_q_a_format=false)
Generates text for multiple prompts in a single batch (parallel processing).
Definition api.cpp:1473
std::vector< int > last_generated_ids_
Definition api.h:312
std::string generated_text_for_api_return_
Definition api.h:304
~TinyLlamaSession()
Destructor to ensure proper cleanup (e.g., KVCache CUDA memory).
Definition api.cpp:702
std::unique_ptr< microtorch::chat::Template > word_template_
Definition api.h:308
int sample_step(std::vector< float > &logits, const std::vector< int > &context, float temperature, int top_k, float top_p)
Definition api.cpp:1464
Parser for GGUF (GPT-Generated Unified Format) files.
std::string why
Definition graph_ir.cpp:280
const float * p[kIrDepthSourcesMax]
int n
std::string text
std::vector< float > v
const char * name
std::vector< E > a
node out
node scale
size_t j
Logging utilities for the TinyLlama implementation.
bool tuned_llama_match(const std::string &ir_json, const GGUFData &gguf, std::string *why)
Definition graph_ir.cpp:440
bool device_supported(const std::string &ir_json)
Definition graph_ir.cpp:463
std::string find_ir(const GGUFData &gguf)
Definition graph_ir.cpp:525
ChatVocab chat_vocab_from_gguf(const GGUFData &gd, const std::string &path)
Definition chat_gguf.h:13
ChatSetup make_chat_setup(ChatVocab v)
std::string utf8_valid(const std::string &s)
Definition utf8_text.h:74
GgufTokenizer read_gguf_tokenizer(const std::string &path)
static std::vector< int > prompt_lookup_draft(const std::vector< int > &h, int k)
Definition api.cpp:96
static void debug_dump_topk(const std::vector< float > &logits)
Definition api.cpp:188
static const ember_draft::DraftTable * corpus_draft_table(int vocab_size)
Definition api.cpp:109
static std::string read_file_api(const std::string &path)
Definition api.cpp:76
static bool logits_debug_on()
Definition api.cpp:216
static int sample_top_k_top_p_temperature(const std::vector< float > &logits, float temperature, int top_k, float top_p, std::mt19937 &rng)
Definition api.cpp:222
static int argmax(const std::vector< float > &v)
Definition api.cpp:84
static void log_vector_summary_detailed(const std::string &name, const std::vector< float > &v, int current_pos, int current_layer, int N)
Definition api.cpp:43
static void apply_repetition_penalty(std::vector< float > &logits, const std::vector< int > &generated, float penalty, size_t window=64)
Definition api.cpp:152
static void apply_contract_penalty(std::vector< float > &lg, const std::vector< int > &a, const std::vector< int > &b, float p, int last_n, IsStop is_stop, std::vector< char > &seen)
Definition api.cpp:170
SafeTensors format loader for efficient tensor loading, supporting single and sharded models.
Complete representation of a GGUF file's contents.
void initialize_batch(int batch_size)
Initialize batch mode with specified number of sequences.
Definition model.h:216
void initialize(const ModelConfig &config, int total_num_model_layers, int num_gpu_layers_to_allocate, int max_seq_len_arg, int num_kv_heads, int head_dim, int max_batch_size_arg=1)
Initializes the KV cache with given dimensions.
Definition kv_cache.cpp:10
void clear_data()
Definition model.h:186
int seq_len
Definition model.h:161
Model configuration structure holding architecture and hyperparameters.
Definition model.h:82
int hidden_size
Definition model.h:83
int vocab_size
Definition model.h:88
int num_attention_heads
Definition model.h:85
bool use_mmap_for_gguf
Definition model.h:104
int eos_token_id
Definition model.h:95
int num_cpu_offload_layers
Definition model.h:106
bool is_gguf_file_loaded
Definition model.h:103
bool use_kvcache_quantization
Definition model.h:105
int num_hidden_layers
Definition model.h:87
int num_key_value_heads
Definition model.h:86
TokenizerFamily tokenizer_family
Definition model.h:123
int max_position_embeddings
Definition model.h:89
chat::Template tmpl
std::unique_ptr< ChatTokenizer > tok
std::string id() const
Where the time went between generate() being called and its first output token being emitted (docs/TT...
Definition api.h:37
std::chrono::steady_clock::time_point t_first_token
Definition api.h:39
double prefix_hit_rate
Definition api.h:59
int prefix_prev_cached_tokens
Definition api.h:57
std::map< std::string, double > op_ms
Definition api.h:74
std::chrono::steady_clock::time_point t_entry
Definition api.h:38
double prefill_gpu_span_ms
Definition api.h:48
double prefill_wall_ms
Definition api.h:46