mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 01:54:43 +00:00
Formatted src/data_generation/llama
This commit is contained in:
@@ -18,144 +18,144 @@
|
|||||||
|
|
||||||
static std::string FormatLocalLanguageCodes(
|
static std::string FormatLocalLanguageCodes(
|
||||||
const std::vector<std::string>& codes) {
|
const std::vector<std::string>& codes) {
|
||||||
if (codes.empty()) {
|
if (codes.empty()) {
|
||||||
return "Not provided";
|
return "Not provided";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string formatted;
|
std::string formatted;
|
||||||
for (const std::string& code : codes) {
|
for (const std::string& code : codes) {
|
||||||
if (!formatted.empty()) {
|
if (!formatted.empty()) {
|
||||||
formatted += ", ";
|
formatted += ", ";
|
||||||
}
|
}
|
||||||
formatted += code;
|
formatted += code;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr int kBreweryInitialMaxTokens = 2800;
|
static constexpr int kBreweryInitialMaxTokens = 2800;
|
||||||
|
|
||||||
BreweryResult LlamaGenerator::GenerateBrewery(
|
BreweryResult LlamaGenerator::GenerateBrewery(
|
||||||
const EnrichedCity& enriched_city) {
|
const EnrichedCity& enriched_city) {
|
||||||
const City& location = enriched_city.location;
|
const City& location = enriched_city.location;
|
||||||
const std::string& region_context = enriched_city.region_context;
|
const std::string& region_context = enriched_city.region_context;
|
||||||
/**
|
/**
|
||||||
* Preprocess and truncate region context to manageable size
|
* Preprocess and truncate region context to manageable size
|
||||||
*/
|
*/
|
||||||
const std::string safe_region_context = PrepareRegionContext(region_context);
|
const std::string safe_region_context = PrepareRegionContext(region_context);
|
||||||
|
|
||||||
const std::string local_language_codes =
|
const std::string local_language_codes =
|
||||||
FormatLocalLanguageCodes(location.local_languages);
|
FormatLocalLanguageCodes(location.local_languages);
|
||||||
|
|
||||||
const std::string country_suffix =
|
const std::string country_suffix =
|
||||||
location.country.empty() ? std::string{}
|
location.country.empty() ? std::string{}
|
||||||
: std::format(", {}", location.country);
|
: std::format(", {}", location.country);
|
||||||
/**
|
/**
|
||||||
* Load brewery system prompt via the injected prompt directory.
|
* Load brewery system prompt via the injected prompt directory.
|
||||||
* The key "BREWERY_GENERATION" resolves to BREWERY_GENERATION.md inside
|
* The key "BREWERY_GENERATION" resolves to BREWERY_GENERATION.md inside
|
||||||
* the configured --prompt-dir. Throws on missing or empty file.
|
* the configured --prompt-dir. Throws on missing or empty file.
|
||||||
*/
|
*/
|
||||||
const std::string system_prompt =
|
const std::string system_prompt =
|
||||||
prompt_directory_->Load("BREWERY_GENERATION");
|
prompt_directory_->Load("BREWERY_GENERATION");
|
||||||
|
|
||||||
std::string user_prompt = std::format(
|
std::string user_prompt = std::format(
|
||||||
"## CITY:\n{}\n\n## COUNTRY:\n{}\n\n## LOCAL LANGUAGE CODES:\n{}\n\n## "
|
"## CITY:\n{}\n\n## COUNTRY:\n{}\n\n## LOCAL LANGUAGE CODES:\n{}\n\n## "
|
||||||
"CONTEXT:\n{}",
|
"CONTEXT:\n{}",
|
||||||
location.city, location.country, local_language_codes,
|
location.city, location.country, local_language_codes,
|
||||||
safe_region_context);
|
safe_region_context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store location context for retry prompts (without repeating full context)
|
* Store location context for retry prompts (without repeating full context)
|
||||||
*/
|
*/
|
||||||
const std::string retry_location =
|
const std::string retry_location =
|
||||||
std::format("City: {}{}\nLocal language codes: {}", location.city,
|
std::format("City: {}{}\nLocal language codes: {}", location.city,
|
||||||
country_suffix, local_language_codes);
|
country_suffix, local_language_codes);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RETRY LOOP with validation and error correction
|
* RETRY LOOP with validation and error correction
|
||||||
* Attempts to generate valid brewery data up to 3 times, with
|
* Attempts to generate valid brewery data up to 3 times, with
|
||||||
* feedback-based refinement
|
* feedback-based refinement
|
||||||
*/
|
*/
|
||||||
constexpr int max_attempts = 3;
|
constexpr int max_attempts = 3;
|
||||||
std::string raw;
|
std::string raw;
|
||||||
std::string last_error;
|
std::string last_error;
|
||||||
|
|
||||||
// Token budget: too small risks truncating valid JSON mid-string.
|
// Token budget: too small risks truncating valid JSON mid-string.
|
||||||
int max_tokens = kBreweryInitialMaxTokens;
|
int max_tokens = kBreweryInitialMaxTokens;
|
||||||
|
|
||||||
// Limit output length to keep it concise and focused
|
|
||||||
for (int attempt = 0; attempt < max_attempts; ++attempt) {
|
|
||||||
// Generate brewery data from LLM
|
|
||||||
raw = this->Infer(system_prompt, user_prompt, max_tokens,
|
|
||||||
kBreweryJsonGrammar);
|
|
||||||
if (logger_) {
|
|
||||||
logger_->Log(
|
|
||||||
{.level = LogLevel::Debug,
|
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
|
||||||
.message = std::format("LlamaGenerator: raw output (attempt {}): {}",
|
|
||||||
attempt + 1, raw)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate output: parse JSON and check required fields
|
|
||||||
|
|
||||||
BreweryResult brewery;
|
|
||||||
const std::optional<std::string> validation_error =
|
|
||||||
ValidateBreweryJson(raw, brewery);
|
|
||||||
|
|
||||||
if (!validation_error.has_value()) {
|
|
||||||
// Success: return parsed brewery data
|
|
||||||
|
|
||||||
|
// Limit output length to keep it concise and focused
|
||||||
|
for (int attempt = 0; attempt < max_attempts; ++attempt) {
|
||||||
|
// Generate brewery data from LLM
|
||||||
|
raw = this->Infer(system_prompt, user_prompt, max_tokens,
|
||||||
|
kBreweryJsonGrammar);
|
||||||
if (logger_) {
|
if (logger_) {
|
||||||
logger_->Log(
|
logger_->Log({.level = LogLevel::Debug,
|
||||||
{.level = LogLevel::Info,
|
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
.message = std::format(
|
||||||
.message = std::format("LlamaGenerator: successfully generated "
|
"LlamaGenerator: raw output (attempt {}): {}",
|
||||||
"brewery data on attempt {}",
|
attempt + 1, raw)});
|
||||||
attempt + 1)});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return brewery;
|
// Validate output: parse JSON and check required fields
|
||||||
}
|
|
||||||
|
|
||||||
// Validation failed: log error and prepare corrective feedback
|
BreweryResult brewery;
|
||||||
|
const std::optional<std::string> validation_error =
|
||||||
|
ValidateBreweryJson(raw, brewery);
|
||||||
|
|
||||||
last_error = *validation_error;
|
if (!validation_error.has_value()) {
|
||||||
if (logger_) {
|
// Success: return parsed brewery data
|
||||||
logger_->Log(
|
|
||||||
{.level = LogLevel::Warn,
|
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
|
||||||
.message = std::format(
|
|
||||||
"LlamaGenerator: malformed brewery JSON (attempt {}): {}",
|
|
||||||
attempt + 1, *validation_error)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update prompt with error details to guide LLM toward correct output.
|
if (logger_) {
|
||||||
user_prompt = std::format(
|
logger_->Log({.level = LogLevel::Info,
|
||||||
"Your previous response was invalid. Error: {}\nReturn the thought "
|
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
||||||
"process before the JSON if needed, then return ONLY valid JSON "
|
.message = std::format(
|
||||||
"with "
|
"LlamaGenerator: successfully generated "
|
||||||
"exactly these keys, in this exact order: {{\"name_en\": "
|
"brewery data on attempt {}",
|
||||||
"\"<English "
|
attempt + 1)});
|
||||||
"brewery name>\", \"description_en\": \"<English single-paragraph "
|
}
|
||||||
"description>\", \"name_local\": \"<local-language brewery "
|
|
||||||
"name>\", "
|
|
||||||
"\"description_local\": \"<local-language single-paragraph "
|
|
||||||
"description>\"}}.\nDo not include markdown, comments, extra keys, "
|
|
||||||
"or "
|
|
||||||
"literal placeholder values.\n\nKeep the JSON strings concise "
|
|
||||||
"enough "
|
|
||||||
"to fit within the token budget.\n\n{}",
|
|
||||||
*validation_error, retry_location);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All retry attempts exhausted: log failure and throw exception
|
return brewery;
|
||||||
if (logger_) {
|
}
|
||||||
logger_->Log({.level = LogLevel::Error,
|
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
// Validation failed: log error and prepare corrective feedback
|
||||||
.message = std::format(
|
|
||||||
"LlamaGenerator: malformed brewery "
|
last_error = *validation_error;
|
||||||
"response after {} attempts: {}",
|
if (logger_) {
|
||||||
max_attempts, last_error.empty() ? raw : last_error)});
|
logger_->Log(
|
||||||
}
|
{.level = LogLevel::Warn,
|
||||||
throw std::runtime_error("LlamaGenerator: malformed brewery response");
|
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
||||||
|
.message = std::format(
|
||||||
|
"LlamaGenerator: malformed brewery JSON (attempt {}): {}",
|
||||||
|
attempt + 1, *validation_error)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update prompt with error details to guide LLM toward correct output.
|
||||||
|
user_prompt = std::format(
|
||||||
|
"Your previous response was invalid. Error: {}\nReturn the thought "
|
||||||
|
"process before the JSON if needed, then return ONLY valid JSON "
|
||||||
|
"with "
|
||||||
|
"exactly these keys, in this exact order: {{\"name_en\": "
|
||||||
|
"\"<English "
|
||||||
|
"brewery name>\", \"description_en\": \"<English single-paragraph "
|
||||||
|
"description>\", \"name_local\": \"<local-language brewery "
|
||||||
|
"name>\", "
|
||||||
|
"\"description_local\": \"<local-language single-paragraph "
|
||||||
|
"description>\"}}.\nDo not include markdown, comments, extra keys, "
|
||||||
|
"or "
|
||||||
|
"literal placeholder values.\n\nKeep the JSON strings concise "
|
||||||
|
"enough "
|
||||||
|
"to fit within the token budget.\n\n{}",
|
||||||
|
*validation_error, retry_location);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All retry attempts exhausted: log failure and throw exception
|
||||||
|
if (logger_) {
|
||||||
|
logger_->Log({.level = LogLevel::Error,
|
||||||
|
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
||||||
|
.message = std::format(
|
||||||
|
"LlamaGenerator: malformed brewery "
|
||||||
|
"response after {} attempts: {}",
|
||||||
|
max_attempts, last_error.empty() ? raw : last_error)});
|
||||||
|
}
|
||||||
|
throw std::runtime_error("LlamaGenerator: malformed brewery response");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,97 +19,99 @@ static constexpr int kUserInitialMaxTokens = 1200;
|
|||||||
UserResult LlamaGenerator::GenerateUser(const EnrichedCity& city,
|
UserResult LlamaGenerator::GenerateUser(const EnrichedCity& city,
|
||||||
const UserPersona& persona,
|
const UserPersona& persona,
|
||||||
const Name& name) {
|
const Name& name) {
|
||||||
std::string style_affinities;
|
std::string style_affinities;
|
||||||
for (const std::string& style : persona.style_affinities) {
|
for (const std::string& style : persona.style_affinities) {
|
||||||
if (!style_affinities.empty()) {
|
if (!style_affinities.empty()) {
|
||||||
style_affinities += ", ";
|
style_affinities += ", ";
|
||||||
}
|
}
|
||||||
style_affinities += style;
|
style_affinities += style;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string system_prompt = prompt_directory_->Load("USER_GENERATION");
|
const std::string system_prompt = prompt_directory_->Load("USER_GENERATION");
|
||||||
|
|
||||||
std::string user_prompt = std::format(
|
std::string user_prompt = std::format(
|
||||||
"## NAME:\n\n{} {}\n\n"
|
"## NAME:\n\n{} {}\n\n"
|
||||||
"## GENDER:\n\n{}\n\n"
|
"## GENDER:\n\n{}\n\n"
|
||||||
"## CITY:\n\n{}\n\n"
|
"## CITY:\n\n{}\n\n"
|
||||||
"## COUNTRY:\n\n{}\n\n"
|
"## COUNTRY:\n\n{}\n\n"
|
||||||
"## PERSONA:\n\n{}\n\n"
|
"## PERSONA:\n\n{}\n\n"
|
||||||
"## PERSONA DESCRIPTION:\n\n{}\n\n"
|
"## PERSONA DESCRIPTION:\n\n{}\n\n"
|
||||||
"## STYLE AFFINITIES:\n\n{}",
|
"## STYLE AFFINITIES:\n\n{}",
|
||||||
name.first_name, name.last_name, name.gender, city.location.city,
|
name.first_name, name.last_name, name.gender, city.location.city,
|
||||||
city.location.country, persona.name, persona.description,
|
city.location.country, persona.name, persona.description,
|
||||||
style_affinities);
|
style_affinities);
|
||||||
|
|
||||||
const std::string retry_context = std::format(
|
const std::string retry_context = std::format(
|
||||||
"Name: {} {}\nCity: {}, {}\nPersona: {}", name.first_name, name.last_name,
|
"Name: {} {}\nCity: {}, {}\nPersona: {}", name.first_name,
|
||||||
city.location.city, city.location.country, persona.name);
|
name.last_name, city.location.city, city.location.country, persona.name);
|
||||||
|
|
||||||
constexpr int max_attempts = 3;
|
constexpr int max_attempts = 3;
|
||||||
std::string raw;
|
std::string raw;
|
||||||
std::string last_error;
|
std::string last_error;
|
||||||
int max_tokens = kUserInitialMaxTokens;
|
int max_tokens = kUserInitialMaxTokens;
|
||||||
|
|
||||||
for (int attempt = 0; attempt < max_attempts; ++attempt) {
|
for (int attempt = 0; attempt < max_attempts; ++attempt) {
|
||||||
raw = this->Infer(system_prompt, user_prompt, max_tokens, kUserJsonGrammar);
|
raw =
|
||||||
if (logger_) {
|
this->Infer(system_prompt, user_prompt, max_tokens, kUserJsonGrammar);
|
||||||
logger_->Log(
|
|
||||||
{.level = LogLevel::Debug,
|
|
||||||
.phase = PipelinePhase::UserGeneration,
|
|
||||||
.message = std::format("LlamaGenerator: raw output (attempt {}): {}",
|
|
||||||
attempt + 1, raw)});
|
|
||||||
}
|
|
||||||
|
|
||||||
UserResult user;
|
|
||||||
const std::optional<std::string> validation_error =
|
|
||||||
ValidateUserJson(raw, user);
|
|
||||||
|
|
||||||
if (!validation_error.has_value()) {
|
|
||||||
if (logger_) {
|
if (logger_) {
|
||||||
logger_->Log(
|
logger_->Log({.level = LogLevel::Debug,
|
||||||
{.level = LogLevel::Info,
|
.phase = PipelinePhase::UserGeneration,
|
||||||
.phase = PipelinePhase::UserGeneration,
|
.message = std::format(
|
||||||
.message = std::format("LlamaGenerator: successfully "
|
"LlamaGenerator: raw output (attempt {}): {}",
|
||||||
"generated user data on attempt {}",
|
attempt + 1, raw)});
|
||||||
attempt + 1)});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.first_name = name.first_name;
|
UserResult user;
|
||||||
user.last_name = name.last_name;
|
const std::optional<std::string> validation_error =
|
||||||
user.gender = name.gender;
|
ValidateUserJson(raw, user);
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
last_error = *validation_error;
|
if (!validation_error.has_value()) {
|
||||||
if (logger_) {
|
if (logger_) {
|
||||||
logger_->Log({.level = LogLevel::Warn,
|
logger_->Log(
|
||||||
|
{.level = LogLevel::Info,
|
||||||
|
.phase = PipelinePhase::UserGeneration,
|
||||||
|
.message = std::format("LlamaGenerator: successfully "
|
||||||
|
"generated user data on attempt {}",
|
||||||
|
attempt + 1)});
|
||||||
|
}
|
||||||
|
|
||||||
|
user.first_name = name.first_name;
|
||||||
|
user.last_name = name.last_name;
|
||||||
|
user.gender = name.gender;
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_error = *validation_error;
|
||||||
|
if (logger_) {
|
||||||
|
logger_->Log(
|
||||||
|
{.level = LogLevel::Warn,
|
||||||
|
.phase = PipelinePhase::UserGeneration,
|
||||||
|
.message = std::format(
|
||||||
|
"LlamaGenerator: malformed user JSON (attempt {}): {}",
|
||||||
|
attempt + 1, *validation_error)});
|
||||||
|
}
|
||||||
|
|
||||||
|
user_prompt = std::format(
|
||||||
|
"Your previous response was invalid. Error: {}\nReturn the thought "
|
||||||
|
"process before the JSON if needed, then return ONLY valid JSON "
|
||||||
|
"with "
|
||||||
|
"exactly these keys, in this exact order: {{\"username\": "
|
||||||
|
"\"<handle "
|
||||||
|
"derived from the given name>\", \"bio\": \"<first-person bio "
|
||||||
|
"grounded in the persona>\", \"activity_weight\": <number between "
|
||||||
|
"0 "
|
||||||
|
"and 1>}}.\nDo not include markdown, comments, extra keys, or "
|
||||||
|
"literal placeholder values.\n\n{}",
|
||||||
|
*validation_error, retry_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logger_) {
|
||||||
|
logger_->Log({.level = LogLevel::Error,
|
||||||
.phase = PipelinePhase::UserGeneration,
|
.phase = PipelinePhase::UserGeneration,
|
||||||
.message = std::format(
|
.message = std::format(
|
||||||
"LlamaGenerator: malformed user JSON (attempt {}): {}",
|
"LlamaGenerator: malformed user response "
|
||||||
attempt + 1, *validation_error)});
|
"after {} attempts: {}",
|
||||||
}
|
max_attempts, last_error.empty() ? raw : last_error)});
|
||||||
|
}
|
||||||
user_prompt = std::format(
|
throw std::runtime_error("LlamaGenerator: malformed user response");
|
||||||
"Your previous response was invalid. Error: {}\nReturn the thought "
|
|
||||||
"process before the JSON if needed, then return ONLY valid JSON "
|
|
||||||
"with "
|
|
||||||
"exactly these keys, in this exact order: {{\"username\": "
|
|
||||||
"\"<handle "
|
|
||||||
"derived from the given name>\", \"bio\": \"<first-person bio "
|
|
||||||
"grounded in the persona>\", \"activity_weight\": <number between "
|
|
||||||
"0 "
|
|
||||||
"and 1>}}.\nDo not include markdown, comments, extra keys, or "
|
|
||||||
"literal placeholder values.\n\n{}",
|
|
||||||
*validation_error, retry_context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger_) {
|
|
||||||
logger_->Log({.level = LogLevel::Error,
|
|
||||||
.phase = PipelinePhase::UserGeneration,
|
|
||||||
.message = std::format(
|
|
||||||
"LlamaGenerator: malformed user response "
|
|
||||||
"after {} attempts: {}",
|
|
||||||
max_attempts, last_error.empty() ? raw : last_error)});
|
|
||||||
}
|
|
||||||
throw std::runtime_error("LlamaGenerator: malformed user response");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* parsing, token decoding, and JSON validation helpers for Llama modules.
|
* parsing, token decoding, and JSON validation helpers for Llama modules.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <llama.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <boost/json.hpp>
|
#include <boost/json.hpp>
|
||||||
@@ -15,8 +17,6 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <llama.h>
|
|
||||||
|
|
||||||
#include "data_generation/llama_generator_helpers.h"
|
#include "data_generation/llama_generator_helpers.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -24,14 +24,14 @@ namespace {
|
|||||||
* String trimming: removes leading and trailing whitespace
|
* String trimming: removes leading and trailing whitespace
|
||||||
*/
|
*/
|
||||||
std::string Trim(std::string_view value) {
|
std::string Trim(std::string_view value) {
|
||||||
constexpr std::string_view whitespace = " \t\n\r\f\v";
|
constexpr std::string_view whitespace = " \t\n\r\f\v";
|
||||||
const size_t first_index = value.find_first_not_of(whitespace);
|
const size_t first_index = value.find_first_not_of(whitespace);
|
||||||
if (first_index == std::string_view::npos) {
|
if (first_index == std::string_view::npos) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t last_index = value.find_last_not_of(whitespace);
|
const size_t last_index = value.find_last_not_of(whitespace);
|
||||||
return std::string(value.substr(first_index, last_index - first_index + 1));
|
return std::string(value.substr(first_index, last_index - first_index + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,26 +39,26 @@ std::string Trim(std::string_view value) {
|
|||||||
* spaces
|
* spaces
|
||||||
*/
|
*/
|
||||||
std::string CondenseWhitespace(std::string_view text) {
|
std::string CondenseWhitespace(std::string_view text) {
|
||||||
std::string out;
|
std::string out;
|
||||||
out.reserve(text.size());
|
out.reserve(text.size());
|
||||||
|
|
||||||
bool pending_space = false;
|
bool pending_space = false;
|
||||||
for (const char chr : text) {
|
for (const char chr : text) {
|
||||||
if (std::isspace(static_cast<unsigned char>(chr)) != 0) {
|
if (std::isspace(static_cast<unsigned char>(chr)) != 0) {
|
||||||
if (!out.empty()) {
|
if (!out.empty()) {
|
||||||
pending_space = true;
|
pending_space = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pending_space) {
|
if (pending_space) {
|
||||||
out.push_back(' ');
|
out.push_back(' ');
|
||||||
pending_space = false;
|
pending_space = false;
|
||||||
}
|
}
|
||||||
out.push_back(chr);
|
out.push_back(chr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guard against truncating in the first half of the string.
|
// Guard against truncating in the first half of the string.
|
||||||
@@ -69,30 +69,30 @@ constexpr size_t kTruncationGuardDivisor = 2;
|
|||||||
bool ReadRequiredTrimmedStringField(const boost::json::object& obj,
|
bool ReadRequiredTrimmedStringField(const boost::json::object& obj,
|
||||||
std::string_view key, std::string& out,
|
std::string_view key, std::string& out,
|
||||||
std::string* error_out) {
|
std::string* error_out) {
|
||||||
const boost::json::value* field = obj.if_contains(key);
|
const boost::json::value* field = obj.if_contains(key);
|
||||||
if (field == nullptr || !field->is_string()) {
|
if (field == nullptr || !field->is_string()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& string_value = field->as_string();
|
const auto& string_value = field->as_string();
|
||||||
out = Trim(std::string_view(string_value.data(), string_value.size()));
|
out = Trim(std::string_view(string_value.data(), string_value.size()));
|
||||||
return !out.empty();
|
return !out.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HasSchemaPlaceholder(std::span<std::string* const> values) {
|
bool HasSchemaPlaceholder(std::span<std::string* const> values) {
|
||||||
for (const std::string* value : values) {
|
for (const std::string* value : values) {
|
||||||
std::string lowered = *value;
|
std::string lowered = *value;
|
||||||
std::ranges::transform(lowered, lowered.begin(),
|
std::ranges::transform(
|
||||||
[](const unsigned char character) {
|
lowered, lowered.begin(), [](const unsigned char character) {
|
||||||
return static_cast<char>(std::tolower(character));
|
return static_cast<char>(std::tolower(character));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (lowered == "string") {
|
if (lowered == "string") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -102,166 +102,167 @@ bool HasSchemaPlaceholder(std::span<std::string* const> values) {
|
|||||||
*/
|
*/
|
||||||
std::string PrepareRegionContext(std::string_view region_context,
|
std::string PrepareRegionContext(std::string_view region_context,
|
||||||
const size_t max_chars) {
|
const size_t max_chars) {
|
||||||
std::string normalized = CondenseWhitespace(region_context);
|
std::string normalized = CondenseWhitespace(region_context);
|
||||||
if (normalized.size() <= max_chars) {
|
if (normalized.size() <= max_chars) {
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized.resize(max_chars);
|
normalized.resize(max_chars);
|
||||||
const size_t last_space = normalized.find_last_of(' ');
|
const size_t last_space = normalized.find_last_of(' ');
|
||||||
if (last_space != std::string::npos &&
|
if (last_space != std::string::npos &&
|
||||||
last_space > max_chars / kTruncationGuardDivisor) {
|
last_space > max_chars / kTruncationGuardDivisor) {
|
||||||
normalized.resize(last_space);
|
normalized.resize(last_space);
|
||||||
}
|
}
|
||||||
|
|
||||||
normalized += "...";
|
normalized += "...";
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppendTokenPiece(const llama_vocab* vocab, llama_token token,
|
void AppendTokenPiece(const llama_vocab* vocab, llama_token token,
|
||||||
std::string& output) {
|
std::string& output) {
|
||||||
constexpr size_t initial_buffer_size = 256;
|
constexpr size_t initial_buffer_size = 256;
|
||||||
|
|
||||||
std::array<char, initial_buffer_size> buffer{};
|
std::array<char, initial_buffer_size> buffer{};
|
||||||
|
|
||||||
// serialize the sampled token into UTF-8 bytes
|
// serialize the sampled token into UTF-8 bytes
|
||||||
|
|
||||||
auto buffer_too_small = [](int32_t result) -> bool { return result < 0; };
|
auto buffer_too_small = [](int32_t result) -> bool { return result < 0; };
|
||||||
|
|
||||||
int32_t bytes =
|
int32_t bytes = llama_token_to_piece(vocab, token, buffer.data(),
|
||||||
llama_token_to_piece(vocab, token, buffer.data(), buffer.size(), 0, true);
|
buffer.size(), 0, true);
|
||||||
|
|
||||||
if (!buffer_too_small(bytes)) {
|
if (!buffer_too_small(bytes)) {
|
||||||
// Append the decoded bytes from the stack buffer.
|
// Append the decoded bytes from the stack buffer.
|
||||||
output.append(buffer.data(), static_cast<size_t>(bytes));
|
output.append(buffer.data(), static_cast<size_t>(bytes));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32_t required_size = -bytes;
|
const int32_t required_size = -bytes;
|
||||||
std::vector<char> dynamic_buffer(static_cast<size_t>(required_size));
|
std::vector<char> dynamic_buffer(static_cast<size_t>(required_size));
|
||||||
|
|
||||||
// Retry token decoding against the larger heap buffer.
|
// Retry token decoding against the larger heap buffer.
|
||||||
bytes = llama_token_to_piece(vocab, token, dynamic_buffer.data(),
|
bytes = llama_token_to_piece(vocab, token, dynamic_buffer.data(),
|
||||||
static_cast<int32_t>(dynamic_buffer.size()), 0,
|
static_cast<int32_t>(dynamic_buffer.size()), 0,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
if (!buffer_too_small(bytes)) {
|
if (!buffer_too_small(bytes)) {
|
||||||
output.append(dynamic_buffer.data(), static_cast<size_t>(bytes));
|
output.append(dynamic_buffer.data(), static_cast<size_t>(bytes));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: failed to decode sampled token piece");
|
"LlamaGenerator: failed to decode sampled token piece");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> ValidateBreweryJson(const std::string& raw,
|
std::optional<std::string> ValidateBreweryJson(const std::string& raw,
|
||||||
BreweryResult& brewery_out) {
|
BreweryResult& brewery_out) {
|
||||||
boost::system::error_code error_code;
|
boost::system::error_code error_code;
|
||||||
const std::string_view raw_view(raw);
|
const std::string_view raw_view(raw);
|
||||||
const size_t opening_brace = raw_view.find('{');
|
const size_t opening_brace = raw_view.find('{');
|
||||||
if (opening_brace == std::string_view::npos) {
|
if (opening_brace == std::string_view::npos) {
|
||||||
return "JSON parse error: missing opening brace '{'";
|
return "JSON parse error: missing opening brace '{'";
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string_view json_payload = raw_view.substr(opening_brace);
|
const std::string_view json_payload = raw_view.substr(opening_brace);
|
||||||
boost::json::value json_value = boost::json::parse(json_payload, error_code);
|
boost::json::value json_value = boost::json::parse(json_payload, error_code);
|
||||||
if (error_code) {
|
if (error_code) {
|
||||||
return "JSON parse error: " + error_code.message();
|
return "JSON parse error: " + error_code.message();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!json_value.is_object()) {
|
if (!json_value.is_object()) {
|
||||||
return "JSON root must be an object";
|
return "JSON root must be an object";
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& obj = json_value.get_object();
|
const auto& obj = json_value.get_object();
|
||||||
if (obj.size() != 4) {
|
if (obj.size() != 4) {
|
||||||
return "JSON object must contain exactly four keys";
|
return "JSON object must contain exactly four keys";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string validation_error;
|
std::string validation_error;
|
||||||
if (!ReadRequiredTrimmedStringField(obj, "name_en", brewery_out.name_en,
|
if (!ReadRequiredTrimmedStringField(obj, "name_en", brewery_out.name_en,
|
||||||
&validation_error)) {
|
&validation_error)) {
|
||||||
return validation_error;
|
return validation_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ReadRequiredTrimmedStringField(obj, "description_en",
|
if (!ReadRequiredTrimmedStringField(obj, "description_en",
|
||||||
brewery_out.description_en,
|
brewery_out.description_en,
|
||||||
&validation_error)) {
|
&validation_error)) {
|
||||||
return validation_error;
|
return validation_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ReadRequiredTrimmedStringField(obj, "name_local", brewery_out.name_local,
|
if (!ReadRequiredTrimmedStringField(
|
||||||
&validation_error)) {
|
obj, "name_local", brewery_out.name_local, &validation_error)) {
|
||||||
return validation_error;
|
return validation_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ReadRequiredTrimmedStringField(obj, "description_local",
|
if (!ReadRequiredTrimmedStringField(obj, "description_local",
|
||||||
brewery_out.description_local,
|
brewery_out.description_local,
|
||||||
&validation_error)) {
|
&validation_error)) {
|
||||||
return validation_error;
|
return validation_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::array schema_placeholders = {
|
const std::array schema_placeholders = {
|
||||||
&brewery_out.name_en, &brewery_out.description_en,
|
&brewery_out.name_en, &brewery_out.description_en,
|
||||||
&brewery_out.name_local, &brewery_out.description_local};
|
&brewery_out.name_local, &brewery_out.description_local};
|
||||||
if (HasSchemaPlaceholder(schema_placeholders)) {
|
if (HasSchemaPlaceholder(schema_placeholders)) {
|
||||||
return "JSON appears to be a schema placeholder, not content";
|
return "JSON appears to be a schema placeholder, not content";
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> ValidateUserJson(const std::string& raw,
|
std::optional<std::string> ValidateUserJson(const std::string& raw,
|
||||||
UserResult& user_out) {
|
UserResult& user_out) {
|
||||||
boost::system::error_code error_code;
|
boost::system::error_code error_code;
|
||||||
const std::string_view raw_view(raw);
|
const std::string_view raw_view(raw);
|
||||||
const size_t opening_brace = raw_view.find('{');
|
const size_t opening_brace = raw_view.find('{');
|
||||||
if (opening_brace == std::string_view::npos) {
|
if (opening_brace == std::string_view::npos) {
|
||||||
return "JSON parse error: missing opening brace '{'";
|
return "JSON parse error: missing opening brace '{'";
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string_view json_payload = raw_view.substr(opening_brace);
|
const std::string_view json_payload = raw_view.substr(opening_brace);
|
||||||
boost::json::value json_value = boost::json::parse(json_payload, error_code);
|
boost::json::value json_value = boost::json::parse(json_payload, error_code);
|
||||||
if (error_code) {
|
if (error_code) {
|
||||||
return "JSON parse error: " + error_code.message();
|
return "JSON parse error: " + error_code.message();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!json_value.is_object()) {
|
if (!json_value.is_object()) {
|
||||||
return "JSON root must be an object";
|
return "JSON root must be an object";
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& obj = json_value.get_object();
|
const auto& obj = json_value.get_object();
|
||||||
if (obj.size() != 3) {
|
if (obj.size() != 3) {
|
||||||
return "JSON object must contain exactly three keys";
|
return "JSON object must contain exactly three keys";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string validation_error;
|
std::string validation_error;
|
||||||
if (!ReadRequiredTrimmedStringField(obj, "username", user_out.username,
|
if (!ReadRequiredTrimmedStringField(obj, "username", user_out.username,
|
||||||
&validation_error)) {
|
&validation_error)) {
|
||||||
return validation_error;
|
return validation_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ReadRequiredTrimmedStringField(obj, "bio", user_out.bio,
|
if (!ReadRequiredTrimmedStringField(obj, "bio", user_out.bio,
|
||||||
&validation_error)) {
|
&validation_error)) {
|
||||||
return validation_error;
|
return validation_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boost::json::value* activity_weight_field =
|
const boost::json::value* activity_weight_field =
|
||||||
obj.if_contains("activity_weight");
|
obj.if_contains("activity_weight");
|
||||||
if (activity_weight_field == nullptr || !activity_weight_field->is_number()) {
|
if (activity_weight_field == nullptr ||
|
||||||
return "Missing or invalid numeric field: activity_weight";
|
!activity_weight_field->is_number()) {
|
||||||
}
|
return "Missing or invalid numeric field: activity_weight";
|
||||||
|
}
|
||||||
|
|
||||||
const double activity_weight = activity_weight_field->to_number<double>();
|
const double activity_weight = activity_weight_field->to_number<double>();
|
||||||
if (activity_weight < 0.0 || activity_weight > 1.0) {
|
if (activity_weight < 0.0 || activity_weight > 1.0) {
|
||||||
return "activity_weight must be between 0 and 1";
|
return "activity_weight must be between 0 and 1";
|
||||||
}
|
}
|
||||||
user_out.activity_weight = static_cast<float>(activity_weight);
|
user_out.activity_weight = static_cast<float>(activity_weight);
|
||||||
|
|
||||||
const std::array schema_placeholders = {&user_out.username, &user_out.bio};
|
const std::array schema_placeholders = {&user_out.username, &user_out.bio};
|
||||||
if (HasSchemaPlaceholder(schema_placeholders)) {
|
if (HasSchemaPlaceholder(schema_placeholders)) {
|
||||||
return "JSON appears to be a schema placeholder, not content";
|
return "JSON appears to be a schema placeholder, not content";
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* output tokens back to text for system+user chat prompts.
|
* output tokens back to text for system+user chat prompts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <llama.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <format>
|
#include <format>
|
||||||
@@ -14,8 +16,6 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <llama.h>
|
|
||||||
|
|
||||||
#include "data_generation/llama_generator.h"
|
#include "data_generation/llama_generator.h"
|
||||||
#include "data_generation/llama_generator_helpers.h"
|
#include "data_generation/llama_generator_helpers.h"
|
||||||
|
|
||||||
@@ -30,48 +30,49 @@ using SamplerHandle =
|
|||||||
std::unique_ptr<llama_sampler, decltype(&llama_sampler_free)>;
|
std::unique_ptr<llama_sampler, decltype(&llama_sampler_free)>;
|
||||||
|
|
||||||
struct SamplerConfig {
|
struct SamplerConfig {
|
||||||
float temperature;
|
float temperature;
|
||||||
uint32_t top_k;
|
uint32_t top_k;
|
||||||
float top_p;
|
float top_p;
|
||||||
uint32_t seed;
|
uint32_t seed;
|
||||||
};
|
};
|
||||||
|
|
||||||
SamplerHandle MakeSamplerChain(const llama_vocab* vocab,
|
SamplerHandle MakeSamplerChain(const llama_vocab* vocab,
|
||||||
const SamplerConfig& config,
|
const SamplerConfig& config,
|
||||||
std::string_view grammar) {
|
std::string_view grammar) {
|
||||||
const llama_sampler_chain_params sampler_params =
|
const llama_sampler_chain_params sampler_params =
|
||||||
llama_sampler_chain_default_params();
|
llama_sampler_chain_default_params();
|
||||||
|
|
||||||
SamplerHandle chain(llama_sampler_chain_init(sampler_params),
|
SamplerHandle chain(llama_sampler_chain_init(sampler_params),
|
||||||
llama_sampler_free);
|
llama_sampler_free);
|
||||||
if (!chain) {
|
if (!chain) {
|
||||||
throw std::runtime_error("LlamaGenerator: failed to initialize sampler");
|
throw std::runtime_error("LlamaGenerator: failed to initialize sampler");
|
||||||
}
|
}
|
||||||
|
|
||||||
auto add_sampler = [&](llama_sampler* sampler, const char* error_message) {
|
auto add_sampler = [&](llama_sampler* sampler, const char* error_message) {
|
||||||
if (sampler == nullptr) {
|
if (sampler == nullptr) {
|
||||||
throw std::runtime_error(error_message);
|
throw std::runtime_error(error_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
llama_sampler_chain_add(chain.get(), sampler);
|
llama_sampler_chain_add(chain.get(), sampler);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!grammar.empty()) {
|
if (!grammar.empty()) {
|
||||||
const std::string grammar_text(grammar);
|
const std::string grammar_text(grammar);
|
||||||
add_sampler(llama_sampler_init_grammar(vocab, grammar_text.c_str(), "root"),
|
add_sampler(
|
||||||
"LlamaGenerator: failed to initialize grammar sampler");
|
llama_sampler_init_grammar(vocab, grammar_text.c_str(), "root"),
|
||||||
}
|
"LlamaGenerator: failed to initialize grammar sampler");
|
||||||
|
}
|
||||||
|
|
||||||
add_sampler(llama_sampler_init_temp(config.temperature),
|
add_sampler(llama_sampler_init_temp(config.temperature),
|
||||||
"LlamaGenerator: failed to initialize temperature sampler");
|
"LlamaGenerator: failed to initialize temperature sampler");
|
||||||
add_sampler(llama_sampler_init_top_k(static_cast<int32_t>(config.top_k)),
|
add_sampler(llama_sampler_init_top_k(static_cast<int32_t>(config.top_k)),
|
||||||
"LlamaGenerator: failed to initialize top-k sampler");
|
"LlamaGenerator: failed to initialize top-k sampler");
|
||||||
add_sampler(llama_sampler_init_top_p(config.top_p, kTopPMinKeep),
|
add_sampler(llama_sampler_init_top_p(config.top_p, kTopPMinKeep),
|
||||||
"LlamaGenerator: failed to initialize top-p sampler");
|
"LlamaGenerator: failed to initialize top-p sampler");
|
||||||
add_sampler(llama_sampler_init_dist(config.seed),
|
add_sampler(llama_sampler_init_dist(config.seed),
|
||||||
"LlamaGenerator: failed to initialize distribution sampler");
|
"LlamaGenerator: failed to initialize distribution sampler");
|
||||||
|
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -80,163 +81,164 @@ std::string LlamaGenerator::Infer(const std::string& system_prompt,
|
|||||||
const std::string& prompt,
|
const std::string& prompt,
|
||||||
const int max_tokens,
|
const int max_tokens,
|
||||||
std::string_view grammar) {
|
std::string_view grammar) {
|
||||||
return InferFormatted(prompt_formatter_->Format(system_prompt, prompt),
|
return InferFormatted(prompt_formatter_->Format(system_prompt, prompt),
|
||||||
max_tokens, grammar);
|
max_tokens, grammar);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string LlamaGenerator::InferFormatted(const std::string& formatted_prompt,
|
std::string LlamaGenerator::InferFormatted(const std::string& formatted_prompt,
|
||||||
const int max_tokens,
|
const int max_tokens,
|
||||||
std::string_view grammar) {
|
std::string_view grammar) {
|
||||||
/**
|
/**
|
||||||
* Validate that model and context are loaded
|
* Validate that model and context are loaded
|
||||||
*/
|
*/
|
||||||
if (!model_ || !context_) {
|
if (!model_ || !context_) {
|
||||||
throw std::runtime_error("LlamaGenerator: model not loaded");
|
throw std::runtime_error("LlamaGenerator: model not loaded");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get vocabulary for tokenization and token-to-text conversion
|
* Get vocabulary for tokenization and token-to-text conversion
|
||||||
*/
|
*/
|
||||||
const llama_vocab* vocab = llama_model_get_vocab(model_.get());
|
const llama_vocab* vocab = llama_model_get_vocab(model_.get());
|
||||||
if (vocab == nullptr) {
|
if (vocab == nullptr) {
|
||||||
throw std::runtime_error("LlamaGenerator: vocab unavailable");
|
throw std::runtime_error("LlamaGenerator: vocab unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
const SamplerConfig sampler_config{
|
const SamplerConfig sampler_config{
|
||||||
.temperature = sampling_temperature_,
|
.temperature = sampling_temperature_,
|
||||||
.top_k = sampling_top_k_,
|
.top_k = sampling_top_k_,
|
||||||
.top_p = sampling_top_p_,
|
.top_p = sampling_top_p_,
|
||||||
.seed = static_cast<uint32_t>(rng_()),
|
.seed = static_cast<uint32_t>(rng_()),
|
||||||
};
|
};
|
||||||
const auto sampler = MakeSamplerChain(vocab, sampler_config, grammar);
|
const auto sampler = MakeSamplerChain(vocab, sampler_config, grammar);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear KV cache to ensure clean inference state (no residual context)
|
* Clear KV cache to ensure clean inference state (no residual context)
|
||||||
*/
|
*/
|
||||||
llama_memory_clear(llama_get_memory(context_.get()), true);
|
llama_memory_clear(llama_get_memory(context_.get()), true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TOKENIZATION PHASE
|
* TOKENIZATION PHASE
|
||||||
* Convert text prompt into token IDs (integers) that the model understands
|
* Convert text prompt into token IDs (integers) that the model understands
|
||||||
*/
|
*/
|
||||||
std::vector<llama_token> prompt_tokens(formatted_prompt.size() +
|
std::vector<llama_token> prompt_tokens(formatted_prompt.size() +
|
||||||
kPromptTokenSlack);
|
kPromptTokenSlack);
|
||||||
|
|
||||||
int32_t token_count = llama_tokenize(
|
int32_t token_count = llama_tokenize(
|
||||||
vocab, formatted_prompt.c_str(),
|
vocab, formatted_prompt.c_str(),
|
||||||
static_cast<int32_t>(formatted_prompt.size()), prompt_tokens.data(),
|
static_cast<int32_t>(formatted_prompt.size()), prompt_tokens.data(),
|
||||||
static_cast<int32_t>(prompt_tokens.size()), true, true);
|
static_cast<int32_t>(prompt_tokens.size()), true, true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If buffer too small, negative return indicates required size
|
* If buffer too small, negative return indicates required size
|
||||||
*/
|
*/
|
||||||
if (token_count < 0) {
|
if (token_count < 0) {
|
||||||
prompt_tokens.resize(static_cast<size_t>(-token_count));
|
prompt_tokens.resize(static_cast<size_t>(-token_count));
|
||||||
|
|
||||||
token_count = llama_tokenize(
|
token_count = llama_tokenize(
|
||||||
vocab, formatted_prompt.c_str(),
|
vocab, formatted_prompt.c_str(),
|
||||||
static_cast<int32_t>(formatted_prompt.size()), prompt_tokens.data(),
|
static_cast<int32_t>(formatted_prompt.size()), prompt_tokens.data(),
|
||||||
static_cast<int32_t>(prompt_tokens.size()), true, true);
|
static_cast<int32_t>(prompt_tokens.size()), true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token_count < 0) {
|
if (token_count < 0) {
|
||||||
throw std::runtime_error("LlamaGenerator: prompt tokenization failed");
|
throw std::runtime_error("LlamaGenerator: prompt tokenization failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CONTEXT SIZE VALIDATION
|
* CONTEXT SIZE VALIDATION
|
||||||
* Validate and compute effective token budgets based on context window
|
* Validate and compute effective token budgets based on context window
|
||||||
* constraints
|
* constraints
|
||||||
*/
|
*/
|
||||||
const auto n_ctx = static_cast<int32_t>(llama_n_ctx(context_.get()));
|
const auto n_ctx = static_cast<int32_t>(llama_n_ctx(context_.get()));
|
||||||
const auto n_batch = static_cast<int32_t>(llama_n_batch(context_.get()));
|
const auto n_batch = static_cast<int32_t>(llama_n_batch(context_.get()));
|
||||||
if (n_ctx <= 1 || n_batch <= 0) {
|
if (n_ctx <= 1 || n_batch <= 0) {
|
||||||
throw std::runtime_error("LlamaGenerator: invalid context or batch size");
|
throw std::runtime_error("LlamaGenerator: invalid context or batch size");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clamp generation limit to available context window, reserve space for
|
* Clamp generation limit to available context window, reserve space for
|
||||||
* output
|
* output
|
||||||
*/
|
*/
|
||||||
const int32_t effective_max_tokens =
|
const int32_t effective_max_tokens =
|
||||||
std::max(1, std::min(max_tokens, n_ctx - 1));
|
std::max(1, std::min(max_tokens, n_ctx - 1));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt can use remaining context after reserving space for generation
|
* Prompt can use remaining context after reserving space for generation
|
||||||
*/
|
*/
|
||||||
int32_t prompt_budget = std::min(n_batch, n_ctx - effective_max_tokens);
|
int32_t prompt_budget = std::min(n_batch, n_ctx - effective_max_tokens);
|
||||||
prompt_budget = std::max<int32_t>(1, prompt_budget);
|
prompt_budget = std::max<int32_t>(1, prompt_budget);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate prompt if necessary to fit within constraints
|
* Truncate prompt if necessary to fit within constraints
|
||||||
*/
|
*/
|
||||||
prompt_tokens.resize(static_cast<size_t>(token_count));
|
prompt_tokens.resize(static_cast<size_t>(token_count));
|
||||||
if (token_count > prompt_budget) {
|
if (token_count > prompt_budget) {
|
||||||
if (logger_) {
|
if (logger_) {
|
||||||
logger_->Log({.level = LogLevel::Warn,
|
logger_->Log(
|
||||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
{.level = LogLevel::Warn,
|
||||||
.message = std::format(
|
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
||||||
"LlamaGenerator: prompt too long ({} tokens), "
|
.message = std::format(
|
||||||
"truncating to {} tokens to fit n_batch/n_ctx limits",
|
"LlamaGenerator: prompt too long ({} tokens), "
|
||||||
token_count, prompt_budget)});
|
"truncating to {} tokens to fit n_batch/n_ctx limits",
|
||||||
}
|
token_count, prompt_budget)});
|
||||||
prompt_tokens.resize(static_cast<size_t>(prompt_budget));
|
}
|
||||||
token_count = prompt_budget;
|
prompt_tokens.resize(static_cast<size_t>(prompt_budget));
|
||||||
}
|
token_count = prompt_budget;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PROMPT PROCESSING PHASE
|
* PROMPT PROCESSING PHASE
|
||||||
* Create a batch containing all prompt tokens and feed through the model
|
* Create a batch containing all prompt tokens and feed through the model
|
||||||
* This computes internal representations and fills the KV cache
|
* This computes internal representations and fills the KV cache
|
||||||
*/
|
*/
|
||||||
const llama_batch prompt_batch = llama_batch_get_one(
|
const llama_batch prompt_batch = llama_batch_get_one(
|
||||||
prompt_tokens.data(), static_cast<int32_t>(prompt_tokens.size()));
|
prompt_tokens.data(), static_cast<int32_t>(prompt_tokens.size()));
|
||||||
if (llama_decode(context_.get(), prompt_batch) != 0) {
|
if (llama_decode(context_.get(), prompt_batch) != 0) {
|
||||||
throw std::runtime_error("LlamaGenerator: prompt decode failed");
|
throw std::runtime_error("LlamaGenerator: prompt decode failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TOKEN GENERATION LOOP
|
* TOKEN GENERATION LOOP
|
||||||
* Iteratively generate tokens one at a time until max_tokens or
|
* Iteratively generate tokens one at a time until max_tokens or
|
||||||
* end-of-sequence
|
* end-of-sequence
|
||||||
*/
|
*/
|
||||||
std::vector<llama_token> generated_tokens;
|
std::vector<llama_token> generated_tokens;
|
||||||
generated_tokens.reserve(static_cast<size_t>(effective_max_tokens));
|
generated_tokens.reserve(static_cast<size_t>(effective_max_tokens));
|
||||||
|
|
||||||
for (int i = 0; i < effective_max_tokens; ++i) {
|
for (int i = 0; i < effective_max_tokens; ++i) {
|
||||||
/**
|
/**
|
||||||
* Sample next token using configured sampler chain and model logits
|
* Sample next token using configured sampler chain and model logits
|
||||||
* Index -1 means use the last output position from previous batch
|
* Index -1 means use the last output position from previous batch
|
||||||
*/
|
*/
|
||||||
const llama_token next =
|
const llama_token next =
|
||||||
llama_sampler_sample(sampler.get(), context_.get(), -1);
|
llama_sampler_sample(sampler.get(), context_.get(), -1);
|
||||||
/**
|
/**
|
||||||
* Stop if model predicts end-of-generation token (EOS/EOT)
|
* Stop if model predicts end-of-generation token (EOS/EOT)
|
||||||
*/
|
*/
|
||||||
if (llama_vocab_is_eog(vocab, next)) {
|
if (llama_vocab_is_eog(vocab, next)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
generated_tokens.push_back(next);
|
generated_tokens.push_back(next);
|
||||||
/**
|
/**
|
||||||
* Feed the sampled token back into model for next iteration
|
* Feed the sampled token back into model for next iteration
|
||||||
* (autoregressive)
|
* (autoregressive)
|
||||||
*/
|
*/
|
||||||
llama_token decode_token = next;
|
llama_token decode_token = next;
|
||||||
const llama_batch one_token_batch = llama_batch_get_one(&decode_token, 1);
|
const llama_batch one_token_batch = llama_batch_get_one(&decode_token, 1);
|
||||||
if (llama_decode(context_.get(), one_token_batch) != 0) {
|
if (llama_decode(context_.get(), one_token_batch) != 0) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: decode failed during generation");
|
"LlamaGenerator: decode failed during generation");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DETOKENIZATION PHASE
|
* DETOKENIZATION PHASE
|
||||||
* Convert generated token IDs back to text using vocabulary
|
* Convert generated token IDs back to text using vocabulary
|
||||||
*/
|
*/
|
||||||
std::string output;
|
std::string output;
|
||||||
for (const llama_token token : generated_tokens) {
|
for (const llama_token token : generated_tokens) {
|
||||||
AppendTokenPiece(vocab, token, output);
|
AppendTokenPiece(vocab, token, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,30 +5,30 @@
|
|||||||
|
|
||||||
#include "data_generation/llama_generator.h"
|
#include "data_generation/llama_generator.h"
|
||||||
|
|
||||||
|
#include <llama.h>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <llama.h>
|
|
||||||
|
|
||||||
#include "data_model/models.h"
|
#include "data_model/models.h"
|
||||||
|
|
||||||
static constexpr uint32_t kMaxContextSize = 32768U;
|
static constexpr uint32_t kMaxContextSize = 32768U;
|
||||||
|
|
||||||
void LlamaGenerator::ModelDeleter::operator()(
|
void LlamaGenerator::ModelDeleter::operator()(
|
||||||
llama_model* model) const noexcept {
|
llama_model* model) const noexcept {
|
||||||
if (model != nullptr) {
|
if (model != nullptr) {
|
||||||
llama_model_free(model);
|
llama_model_free(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LlamaGenerator::ContextDeleter::operator()(
|
void LlamaGenerator::ContextDeleter::operator()(
|
||||||
llama_context* context) const noexcept {
|
llama_context* context) const noexcept {
|
||||||
if (context != nullptr) {
|
if (context != nullptr) {
|
||||||
llama_free(context);
|
llama_free(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LlamaGenerator::LlamaGenerator(
|
LlamaGenerator::LlamaGenerator(
|
||||||
@@ -40,61 +40,61 @@ LlamaGenerator::LlamaGenerator(
|
|||||||
logger_(std::move(logger)),
|
logger_(std::move(logger)),
|
||||||
prompt_formatter_(std::move(prompt_formatter)),
|
prompt_formatter_(std::move(prompt_formatter)),
|
||||||
prompt_directory_(std::move(prompt_directory)) {
|
prompt_directory_(std::move(prompt_directory)) {
|
||||||
if (model_path.empty()) {
|
if (model_path.empty()) {
|
||||||
throw std::runtime_error("LlamaGenerator: model path must not be empty");
|
throw std::runtime_error("LlamaGenerator: model path must not be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prompt_formatter_) {
|
if (!prompt_formatter_) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: prompt formatter dependency must not be null");
|
"LlamaGenerator: prompt formatter dependency must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prompt_directory_) {
|
if (!prompt_directory_) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: prompt directory dependency must not be null");
|
"LlamaGenerator: prompt directory dependency must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto sampling = options.generator.sampling.value_or(SamplingOptions{});
|
const auto sampling = options.generator.sampling.value_or(SamplingOptions{});
|
||||||
|
|
||||||
if (sampling.temperature < 0.0F) {
|
if (sampling.temperature < 0.0F) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: sampling temperature must be >= 0");
|
"LlamaGenerator: sampling temperature must be >= 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.top_p <= 0.0F || sampling.top_p > 1.0F) {
|
if (sampling.top_p <= 0.0F || sampling.top_p > 1.0F) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: sampling top-p must be in (0, 1]");
|
"LlamaGenerator: sampling top-p must be in (0, 1]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.top_k == 0U) {
|
if (sampling.top_k == 0U) {
|
||||||
throw std::runtime_error("LlamaGenerator: sampling top-k must be > 0");
|
throw std::runtime_error("LlamaGenerator: sampling top-k must be > 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.seed < -1) {
|
if (sampling.seed < -1) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: seed must be >= 0, or -1 for random");
|
"LlamaGenerator: seed must be >= 0, or -1 for random");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampling.n_ctx == 0 || sampling.n_ctx > kMaxContextSize) {
|
if (sampling.n_ctx == 0 || sampling.n_ctx > kMaxContextSize) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: context size must be in range [1, 32768]");
|
"LlamaGenerator: context size must be in range [1, 32768]");
|
||||||
}
|
}
|
||||||
|
|
||||||
sampling_temperature_ = sampling.temperature;
|
sampling_temperature_ = sampling.temperature;
|
||||||
sampling_top_p_ = sampling.top_p;
|
sampling_top_p_ = sampling.top_p;
|
||||||
sampling_top_k_ = sampling.top_k;
|
sampling_top_k_ = sampling.top_k;
|
||||||
|
|
||||||
if (sampling.seed == -1) {
|
if (sampling.seed == -1) {
|
||||||
std::random_device random_device;
|
std::random_device random_device;
|
||||||
rng_.seed(random_device());
|
rng_.seed(random_device());
|
||||||
} else {
|
} else {
|
||||||
rng_.seed(static_cast<uint32_t>(sampling.seed));
|
rng_.seed(static_cast<uint32_t>(sampling.seed));
|
||||||
}
|
}
|
||||||
|
|
||||||
n_ctx_ = sampling.n_ctx;
|
n_ctx_ = sampling.n_ctx;
|
||||||
n_gpu_layers_ = sampling.n_gpu_layers;
|
n_gpu_layers_ = sampling.n_gpu_layers;
|
||||||
|
|
||||||
this->Load(model_path);
|
this->Load(model_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
LlamaGenerator::~LlamaGenerator() = default;
|
LlamaGenerator::~LlamaGenerator() = default;
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
* context, and resets prior resources during model initialization.
|
* context, and resets prior resources during model initialization.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include <ggml-backend.h>
|
||||||
|
#include <llama.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <ggml-backend.h>
|
|
||||||
#include <llama.h>
|
|
||||||
|
|
||||||
#include "data_generation/llama_generator.h"
|
#include "data_generation/llama_generator.h"
|
||||||
|
|
||||||
// Maximum batch size for decode operations. Capping the batch prevents
|
// Maximum batch size for decode operations. Capping the batch prevents
|
||||||
@@ -20,42 +20,42 @@
|
|||||||
static constexpr uint32_t kMaxBatchSize = 5000U;
|
static constexpr uint32_t kMaxBatchSize = 5000U;
|
||||||
|
|
||||||
void LlamaGenerator::Load(const std::string& model_path) {
|
void LlamaGenerator::Load(const std::string& model_path) {
|
||||||
context_.reset();
|
context_.reset();
|
||||||
model_.reset();
|
model_.reset();
|
||||||
|
|
||||||
// Specifically load dynamic ggml backends (like CUDA) that are provided
|
// Specifically load dynamic ggml backends (like CUDA) that are provided
|
||||||
// externally before attempting to load a model.
|
// externally before attempting to load a model.
|
||||||
ggml_backend_load_all();
|
ggml_backend_load_all();
|
||||||
|
|
||||||
llama_model_params model_params = llama_model_default_params();
|
llama_model_params model_params = llama_model_default_params();
|
||||||
model_params.n_gpu_layers = n_gpu_layers_;
|
model_params.n_gpu_layers = n_gpu_layers_;
|
||||||
|
|
||||||
ModelHandle loaded_model(
|
ModelHandle loaded_model(
|
||||||
llama_model_load_from_file(model_path.c_str(), model_params));
|
llama_model_load_from_file(model_path.c_str(), model_params));
|
||||||
|
|
||||||
if (!loaded_model) {
|
if (!loaded_model) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"LlamaGenerator: failed to load model from path: " + model_path);
|
"LlamaGenerator: failed to load model from path: " + model_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
llama_context_params context_params = llama_context_default_params();
|
llama_context_params context_params = llama_context_default_params();
|
||||||
context_params.n_ctx = n_ctx_;
|
context_params.n_ctx = n_ctx_;
|
||||||
context_params.n_batch = std::min(n_ctx_, kMaxBatchSize);
|
context_params.n_batch = std::min(n_ctx_, kMaxBatchSize);
|
||||||
|
|
||||||
ContextHandle loaded_context(
|
ContextHandle loaded_context(
|
||||||
llama_init_from_model(loaded_model.get(), context_params));
|
llama_init_from_model(loaded_model.get(), context_params));
|
||||||
|
|
||||||
if (!loaded_context) {
|
if (!loaded_context) {
|
||||||
throw std::runtime_error("LlamaGenerator: failed to create context");
|
throw std::runtime_error("LlamaGenerator: failed to create context");
|
||||||
}
|
}
|
||||||
|
|
||||||
model_ = std::move(loaded_model);
|
model_ = std::move(loaded_model);
|
||||||
context_ = std::move(loaded_context);
|
context_ = std::move(loaded_context);
|
||||||
|
|
||||||
if (logger_) {
|
if (logger_) {
|
||||||
logger_->Log({.level = LogLevel::Info,
|
logger_->Log({.level = LogLevel::Info,
|
||||||
.phase = PipelinePhase::Startup,
|
.phase = PipelinePhase::Startup,
|
||||||
.message = std::format("[LlamaGenerator] Loaded model: {} ",
|
.message = std::format("[LlamaGenerator] Loaded model: {} ",
|
||||||
model_path)});
|
model_path)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user