191 lines
6.0 KiB
C
191 lines
6.0 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file chat_corpus.c
|
|
* @brief Implementation of the corpus-to-token-ids conversion declared in chat_corpus.h.
|
|
*
|
|
* @status Working
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "chat_corpus.h"
|
|
|
|
#include "../src/core/bpe_pretokenize.h"
|
|
#include "../src/core/chat.h"
|
|
#include "../src/core/engine.h"
|
|
#include "tokenizer/tokenizer.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
void chat_document_free(chat_document *doc)
|
|
{
|
|
for (int i = 0; i < doc->n_turns; i++) {
|
|
free(doc->text[i]);
|
|
doc->text[i] = NULL;
|
|
}
|
|
doc->n_turns = 0;
|
|
}
|
|
|
|
int chat_document_read(FILE *in, chat_document *doc)
|
|
{
|
|
static char line[CHAT_CORPUS_MAX_TEXT_BYTES];
|
|
|
|
doc->n_turns = 0;
|
|
while (fgets(line, sizeof(line), in)) {
|
|
size_t len = strlen(line);
|
|
char *body;
|
|
|
|
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
|
|
line[--len] = '\0';
|
|
if (len == 0) {
|
|
if (doc->n_turns > 0)
|
|
return 1;
|
|
continue;
|
|
}
|
|
if (line[0] == '#')
|
|
continue;
|
|
if ((line[0] != 'U' && line[0] != 'A' && line[0] != 'S') || line[1] != '\t') {
|
|
fprintf(stderr, "chat_corpus: line is not S<TAB>, U<TAB> or A<TAB>: %.40s\n", line);
|
|
continue;
|
|
}
|
|
if (doc->n_turns >= CHAT_CORPUS_MAX_TURNS) {
|
|
fprintf(stderr, "chat_corpus: more than %d turns in one document\n",
|
|
CHAT_CORPUS_MAX_TURNS);
|
|
continue;
|
|
}
|
|
body = strdup(line + 2);
|
|
if (!body)
|
|
return doc->n_turns > 0;
|
|
doc->role[doc->n_turns] = line[0];
|
|
doc->text[doc->n_turns] = body;
|
|
doc->n_turns++;
|
|
}
|
|
return doc->n_turns > 0;
|
|
}
|
|
|
|
/** Append `n` ids, refusing rather than truncating silently. */
|
|
static int ids_append(uint32_t *ids, int *n_ids, int max_ids, const uint32_t *add, int n)
|
|
{
|
|
if (*n_ids + n > max_ids) {
|
|
fprintf(stderr, "chat_corpus: document exceeds %d ids\n", max_ids);
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < n; i++)
|
|
ids[(*n_ids)++] = add[i];
|
|
return 0;
|
|
}
|
|
|
|
/** BPE-encode `text` and append its ids. The chat arm needs this for the assistant BODY,
|
|
* which the template functions do not carry — they emit the role frame only. */
|
|
static int ids_append_text(pn_model *model, uint32_t *ids, int *n_ids, int max_ids,
|
|
const char *text)
|
|
{
|
|
int n = 0;
|
|
int *enc = pn_bpe_encode(model->tokenizer, text, &n);
|
|
int rc = 0;
|
|
|
|
if (!enc || n <= 0) {
|
|
fprintf(stderr, "chat_corpus: encode failed for %.40s\n", text);
|
|
free(enc);
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < n && rc == 0; i++) {
|
|
uint32_t id = (uint32_t)enc[i];
|
|
|
|
rc = ids_append(ids, n_ids, max_ids, &id, 1);
|
|
}
|
|
free(enc);
|
|
return rc;
|
|
}
|
|
|
|
int chat_document_to_chat_ids(pn_model *model, const chat_document *doc, uint32_t *ids,
|
|
int max_ids)
|
|
{
|
|
pn_chat_style style = (pn_chat_style)model->chat_style;
|
|
const char *system = NULL;
|
|
int n_ids = 0;
|
|
int first_user = 1;
|
|
|
|
/* A leading S turn is the SYSTEM prompt, and it belongs in the template's own system
|
|
* block rather than as another user turn. That is not cosmetic: framed as a system block
|
|
* the defect corpus costs +22.1% perplexity on this card, and the SAME text framed as a
|
|
* second user turn (which also leaves an empty assistant turn between them) does not
|
|
* reproduce it at all. A gate that cannot emit a system block is not scoring what a serve
|
|
* sends. */
|
|
for (int t = 0; t < doc->n_turns; t++)
|
|
if (doc->role[t] == 'S' && !system)
|
|
system = doc->text[t];
|
|
|
|
for (int t = 0; t < doc->n_turns; t++) {
|
|
if (doc->role[t] == 'S') {
|
|
continue;
|
|
} else if (doc->role[t] == 'U') {
|
|
pn_token_seq seq = first_user
|
|
? pn_chat_format_first(style, model->tokenizer, system,
|
|
doc->text[t])
|
|
: pn_chat_format_turn(style, model->tokenizer, doc->text[t]);
|
|
int rc;
|
|
|
|
if (!seq.tokens || seq.n_tokens <= 0) {
|
|
fprintf(stderr, "chat_corpus: chat formatting failed at turn %d\n", t);
|
|
free(seq.tokens);
|
|
return -1;
|
|
}
|
|
rc = ids_append(ids, &n_ids, max_ids, seq.tokens, seq.n_tokens);
|
|
free(seq.tokens);
|
|
if (rc != 0)
|
|
return -1;
|
|
first_user = 0;
|
|
} else if (ids_append_text(model, ids, &n_ids, max_ids, doc->text[t]) != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
return n_ids;
|
|
}
|
|
|
|
int chat_document_to_plain_ids(pn_model *model, const chat_document *doc, uint32_t *ids,
|
|
int max_ids)
|
|
{
|
|
static char joined[CHAT_CORPUS_MAX_TEXT_BYTES];
|
|
int n_ids = 0;
|
|
size_t used = 0;
|
|
|
|
joined[0] = '\0';
|
|
for (int t = 0; t < doc->n_turns; t++) {
|
|
int n = snprintf(joined + used, sizeof(joined) - used, "%s%s",
|
|
t == 0 ? "" : "\n\n", doc->text[t]);
|
|
|
|
if (n < 0 || (size_t)n >= sizeof(joined) - used) {
|
|
fprintf(stderr, "chat_corpus: joined document exceeds %zu bytes\n", sizeof(joined));
|
|
return -1;
|
|
}
|
|
used += (size_t)n;
|
|
}
|
|
if (ids_append_text(model, ids, &n_ids, max_ids, joined) != 0)
|
|
return -1;
|
|
return n_ids;
|
|
}
|
|
|
|
char *chat_ids_to_text(pn_model *model, const uint32_t *ids, int n_ids)
|
|
{
|
|
int is_bpe = ft_get_model_type(model->tokenizer) == 0;
|
|
int *signed_ids = malloc((size_t)n_ids * sizeof(int));
|
|
char *raw, *text;
|
|
|
|
if (!signed_ids)
|
|
return NULL;
|
|
for (int i = 0; i < n_ids; i++)
|
|
signed_ids[i] = (int)ids[i];
|
|
raw = ft_decode(model->tokenizer, signed_ids, n_ids);
|
|
free(signed_ids);
|
|
if (!raw)
|
|
return NULL;
|
|
text = is_bpe ? gpt2_bytes_decode(raw) : NULL;
|
|
if (!text)
|
|
return raw;
|
|
free(raw);
|
|
return text;
|
|
}
|