mirror of
https://github.com/zenlm/zen5-engine.git
synced 2026-10-30 14:57:31 +00:00
Replace the default long-context continuation check with a deterministic prose-story retrieval test. The fixture embeds spelled-out person-number assignments in a long rendered prompt, and ds4_test now validates the generated Name=number list instead of brittle sampled prose.
742 lines
23 KiB
C
742 lines
23 KiB
C
#define DS4_SERVER_TEST
|
|
#define DS4_SERVER_TEST_NO_MAIN
|
|
#include "../ds4_server.c"
|
|
#ifndef DS4_NO_GPU
|
|
#include "../ds4_gpu.h"
|
|
#include <math.h>
|
|
|
|
static ds4_engine *test_engine_fast;
|
|
static ds4_engine *test_engine_quality;
|
|
|
|
static const char *test_model_path(void) {
|
|
const char *model_path = getenv("DS4_TEST_MODEL");
|
|
return (model_path && model_path[0]) ? model_path : "ds4flash.gguf";
|
|
}
|
|
|
|
static ds4_engine *test_get_engine(bool quality) {
|
|
ds4_engine **slot = quality ? &test_engine_quality : &test_engine_fast;
|
|
if (*slot) return *slot;
|
|
|
|
ds4_engine_options opt = {
|
|
.model_path = test_model_path(),
|
|
#ifdef __APPLE__
|
|
.backend = DS4_BACKEND_METAL,
|
|
#else
|
|
.backend = DS4_BACKEND_CUDA,
|
|
#endif
|
|
.quality = quality,
|
|
};
|
|
TEST_ASSERT(ds4_engine_open(slot, &opt) == 0);
|
|
return *slot;
|
|
}
|
|
|
|
static void test_close_engines(void) {
|
|
ds4_engine_close(test_engine_fast);
|
|
ds4_engine_close(test_engine_quality);
|
|
test_engine_fast = NULL;
|
|
test_engine_quality = NULL;
|
|
}
|
|
|
|
static void test_close_engine(bool quality) {
|
|
ds4_engine **slot = quality ? &test_engine_quality : &test_engine_fast;
|
|
ds4_engine_close(*slot);
|
|
*slot = NULL;
|
|
}
|
|
|
|
static uint64_t test_round_up_u64(uint64_t n, uint64_t align) {
|
|
return (n + align - 1) & ~(align - 1);
|
|
}
|
|
|
|
static uint16_t test_float_to_f16(float f) {
|
|
union {
|
|
float f;
|
|
uint32_t u;
|
|
} v = { .f = f };
|
|
|
|
uint32_t sign = (v.u >> 16) & 0x8000u;
|
|
int32_t exp = (int32_t)((v.u >> 23) & 0xffu) - 127 + 15;
|
|
uint32_t mant = v.u & 0x7fffffu;
|
|
|
|
if (exp <= 0) {
|
|
if (exp < -10) return (uint16_t)sign;
|
|
mant |= 0x800000u;
|
|
uint32_t shift = (uint32_t)(14 - exp);
|
|
uint32_t half_mant = mant >> shift;
|
|
if ((mant >> (shift - 1)) & 1u) half_mant++;
|
|
return (uint16_t)(sign | half_mant);
|
|
}
|
|
if (exp >= 31) return (uint16_t)(sign | 0x7c00u);
|
|
|
|
uint32_t half = sign | ((uint32_t)exp << 10) | (mant >> 13);
|
|
if (mant & 0x1000u) half++;
|
|
return (uint16_t)half;
|
|
}
|
|
|
|
static void test_metal_f16_matvec_fast_nr0_4(void) {
|
|
/*
|
|
* This is the short regression for the long-context repetition failure.
|
|
* Decode uses one-token F16 matvecs for several DS4 projections; the fast
|
|
* nr0=4 variant must be numerically equivalent to the plain kernel.
|
|
*/
|
|
const uint32_t in_dim = 4096;
|
|
const uint32_t out_dim = 512;
|
|
const uint64_t weight_bytes = (uint64_t)in_dim * out_dim * sizeof(uint16_t);
|
|
const uint64_t weight_alloc = test_round_up_u64(weight_bytes, (uint64_t)getpagesize());
|
|
|
|
void *weights_raw = NULL;
|
|
TEST_ASSERT(posix_memalign(&weights_raw, (size_t)getpagesize(), (size_t)weight_alloc) == 0);
|
|
if (!weights_raw) return;
|
|
|
|
uint16_t *weights = weights_raw;
|
|
memset(weights, 0, (size_t)weight_alloc);
|
|
for (uint32_t o = 0; o < out_dim; o++) {
|
|
for (uint32_t i = 0; i < in_dim; i++) {
|
|
float w = (float)((int)((o * 3u + i * 5u) % 23u) - 11) / 64.0f;
|
|
weights[(uint64_t)o * in_dim + i] = test_float_to_f16(w);
|
|
}
|
|
}
|
|
|
|
ds4_gpu_tensor *x = ds4_gpu_tensor_alloc((uint64_t)in_dim * sizeof(float));
|
|
ds4_gpu_tensor *out = ds4_gpu_tensor_alloc((uint64_t)out_dim * sizeof(float));
|
|
TEST_ASSERT(x != NULL);
|
|
TEST_ASSERT(out != NULL);
|
|
if (!x || !out) {
|
|
ds4_gpu_tensor_free(x);
|
|
ds4_gpu_tensor_free(out);
|
|
free(weights_raw);
|
|
return;
|
|
}
|
|
|
|
float *x_host = malloc((size_t)in_dim * sizeof(float));
|
|
float *out_host = malloc((size_t)out_dim * sizeof(float));
|
|
TEST_ASSERT(x_host != NULL);
|
|
TEST_ASSERT(out_host != NULL);
|
|
if (!x_host || !out_host) {
|
|
free(x_host);
|
|
free(out_host);
|
|
ds4_gpu_tensor_free(x);
|
|
ds4_gpu_tensor_free(out);
|
|
free(weights_raw);
|
|
return;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < in_dim; i++) {
|
|
x_host[i] = (float)((int)(i % 31u) - 15) / 32.0f;
|
|
}
|
|
|
|
TEST_ASSERT(ds4_gpu_tensor_write(x, 0, x_host, (uint64_t)in_dim * sizeof(float)) != 0);
|
|
TEST_ASSERT(ds4_gpu_set_model_map(weights_raw, weight_alloc) != 0);
|
|
ds4_gpu_set_quality(false);
|
|
TEST_ASSERT(ds4_gpu_matmul_f16_tensor(out, weights_raw, weight_alloc, 0,
|
|
in_dim, out_dim, x, 1) != 0);
|
|
TEST_ASSERT(ds4_gpu_tensor_read(out, 0, out_host, (uint64_t)out_dim * sizeof(float)) != 0);
|
|
|
|
float max_abs = 0.0f;
|
|
for (uint32_t o = 0; o < out_dim; o++) {
|
|
float ref = 0.0f;
|
|
for (uint32_t i = 0; i < in_dim; i++) {
|
|
float w = (float)((int)((o * 3u + i * 5u) % 23u) - 11) / 64.0f;
|
|
ref += w * x_host[i];
|
|
}
|
|
float err = fabsf(out_host[o] - ref);
|
|
if (err > max_abs) max_abs = err;
|
|
}
|
|
TEST_ASSERT(max_abs < 0.02f);
|
|
|
|
free(x_host);
|
|
free(out_host);
|
|
ds4_gpu_tensor_free(x);
|
|
ds4_gpu_tensor_free(out);
|
|
free(weights_raw);
|
|
}
|
|
|
|
static char *test_read_file(const char *path) {
|
|
FILE *fp = fopen(path, "rb");
|
|
if (!fp) return NULL;
|
|
if (fseek(fp, 0, SEEK_END) != 0) {
|
|
fclose(fp);
|
|
return NULL;
|
|
}
|
|
long len = ftell(fp);
|
|
if (len < 0) {
|
|
fclose(fp);
|
|
return NULL;
|
|
}
|
|
rewind(fp);
|
|
char *s = malloc((size_t)len + 1);
|
|
if (!s) {
|
|
fclose(fp);
|
|
return NULL;
|
|
}
|
|
size_t nread = fread(s, 1, (size_t)len, fp);
|
|
fclose(fp);
|
|
if (nread != (size_t)len) {
|
|
free(s);
|
|
return NULL;
|
|
}
|
|
s[len] = '\0';
|
|
return s;
|
|
}
|
|
|
|
typedef struct {
|
|
const char *name;
|
|
int number;
|
|
} test_long_fact;
|
|
|
|
static const test_long_fact test_long_facts[] = {
|
|
{"Bob", 34},
|
|
{"Alice", 52},
|
|
{"Clara", 71},
|
|
{"Diego", 93},
|
|
{"Elena", 16},
|
|
{"Felix", 88},
|
|
{"Greta", 47},
|
|
{"Hugo", 29},
|
|
{"Iris", 64},
|
|
{"Jonas", 12},
|
|
{"Kira", 81},
|
|
{"Leo", 39},
|
|
{"Marta", 76},
|
|
{"Nadia", 23},
|
|
{"Owen", 58},
|
|
{"Priya", 97},
|
|
};
|
|
|
|
static bool test_is_name_boundary(char c) {
|
|
unsigned char uc = (unsigned char)c;
|
|
return c == '\0' || !(isalnum(uc) || c == '_');
|
|
}
|
|
|
|
static bool test_parse_assignment_value(const char *p, int *value) {
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
if (*p != '=') return false;
|
|
p++;
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
if (!isdigit((unsigned char)*p)) return false;
|
|
|
|
int v = 0;
|
|
while (isdigit((unsigned char)*p)) {
|
|
v = v * 10 + (*p - '0');
|
|
p++;
|
|
}
|
|
*value = v;
|
|
return true;
|
|
}
|
|
|
|
static bool test_output_has_fact(const char *text, const test_long_fact *fact) {
|
|
const size_t name_len = strlen(fact->name);
|
|
const char *p = text;
|
|
bool saw_wrong_assignment = false;
|
|
int wrong_value = -1;
|
|
|
|
while ((p = strstr(p, fact->name)) != NULL) {
|
|
const bool before_ok = p == text || test_is_name_boundary(p[-1]);
|
|
const bool after_ok = test_is_name_boundary(p[name_len]) ||
|
|
p[name_len] == ' ' ||
|
|
p[name_len] == '\t' ||
|
|
p[name_len] == '=';
|
|
if (before_ok && after_ok) {
|
|
int value = 0;
|
|
if (test_parse_assignment_value(p + name_len, &value)) {
|
|
if (value == fact->number) return true;
|
|
saw_wrong_assignment = true;
|
|
wrong_value = value;
|
|
}
|
|
}
|
|
p += name_len;
|
|
}
|
|
|
|
if (saw_wrong_assignment) {
|
|
fprintf(stderr,
|
|
"ds4-test: long-context wrong assignment for %s: got %d expected %d\n",
|
|
fact->name, wrong_value, fact->number);
|
|
} else {
|
|
fprintf(stderr,
|
|
"ds4-test: long-context missing assignment for %s=%d\n",
|
|
fact->name, fact->number);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int test_hex_digit(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
|
|
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
|
|
return -1;
|
|
}
|
|
|
|
static bool test_hex_to_bytes(const char *hex, unsigned char *out, int cap, int *len) {
|
|
int n = 0;
|
|
while (*hex && !isspace((unsigned char)*hex)) {
|
|
int hi = test_hex_digit(hex[0]);
|
|
int lo = test_hex_digit(hex[1]);
|
|
if (hi < 0 || lo < 0 || n >= cap) return false;
|
|
out[n++] = (unsigned char)((hi << 4) | lo);
|
|
hex += 2;
|
|
}
|
|
*len = n;
|
|
return true;
|
|
}
|
|
|
|
static bool test_token_bytes_equal(ds4_engine *engine, int token,
|
|
const unsigned char *want, int want_len) {
|
|
size_t got_len = 0;
|
|
char *got = ds4_token_text(engine, token, &got_len);
|
|
bool eq = got && got_len == (size_t)want_len &&
|
|
memcmp(got, want, (size_t)want_len) == 0;
|
|
free(got);
|
|
return eq;
|
|
}
|
|
|
|
static void test_long_prefill_progress(void *ud, const char *event, int current, int total) {
|
|
(void)ud;
|
|
if (strcmp(event, "prefill_chunk")) return;
|
|
if (current == 0 || current == total || current % 8192 == 0) {
|
|
fprintf(stderr, "ds4-test: long-context prefill %d/%d\n", current, total);
|
|
}
|
|
}
|
|
|
|
static void test_long_story_fact_recall(void) {
|
|
const char *prompt_path = getenv("DS4_TEST_LONG_PROMPT");
|
|
if (!prompt_path || !prompt_path[0]) {
|
|
prompt_path = "tests/long_context_story_prompt.txt";
|
|
}
|
|
char *prompt_text = test_read_file(prompt_path);
|
|
TEST_ASSERT(prompt_text != NULL);
|
|
if (!prompt_text) return;
|
|
|
|
ds4_engine *engine = test_get_engine(false);
|
|
if (!engine) {
|
|
free(prompt_text);
|
|
return;
|
|
}
|
|
|
|
ds4_tokens prompt = {0};
|
|
ds4_tokenize_rendered_chat(engine, prompt_text, &prompt);
|
|
TEST_ASSERT(prompt.len > 30000);
|
|
|
|
ds4_session *session = NULL;
|
|
TEST_ASSERT(ds4_session_create(&session, engine, 100000) == 0);
|
|
if (!session) {
|
|
ds4_tokens_free(&prompt);
|
|
free(prompt_text);
|
|
return;
|
|
}
|
|
|
|
char err[160];
|
|
ds4_session_set_progress(session, test_long_prefill_progress, NULL);
|
|
TEST_ASSERT(ds4_session_sync(session, &prompt, err, sizeof(err)) == 0);
|
|
ds4_session_set_progress(session, NULL, NULL);
|
|
|
|
buf out = {0};
|
|
uint64_t rng = 12345;
|
|
int generated = 0;
|
|
bool decode_ok = true;
|
|
for (; generated < 350; generated++) {
|
|
int token = ds4_session_sample(session, 0.0f, 0, 1.0f, 0.0f, &rng);
|
|
if (token == ds4_token_eos(engine)) break;
|
|
|
|
size_t piece_len = 0;
|
|
char *piece = ds4_token_text(engine, token, &piece_len);
|
|
buf_append(&out, piece, piece_len);
|
|
free(piece);
|
|
|
|
if (ds4_session_eval(session, token, err, sizeof(err)) != 0) {
|
|
decode_ok = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const char *text = out.ptr ? out.ptr : "";
|
|
TEST_ASSERT(decode_ok);
|
|
TEST_ASSERT(generated > 0);
|
|
for (size_t i = 0; i < sizeof(test_long_facts) / sizeof(test_long_facts[0]); i++) {
|
|
TEST_ASSERT(test_output_has_fact(text, &test_long_facts[i]));
|
|
}
|
|
|
|
buf_free(&out);
|
|
ds4_session_free(session);
|
|
ds4_tokens_free(&prompt);
|
|
free(prompt_text);
|
|
}
|
|
|
|
#define TEST_VEC_MAX_STEPS 16
|
|
#define TEST_VEC_MAX_TOP 32
|
|
#define TEST_VEC_MAX_TOKEN_BYTES 128
|
|
|
|
typedef struct {
|
|
unsigned char bytes[TEST_VEC_MAX_TOKEN_BYTES];
|
|
int len;
|
|
float logprob;
|
|
} test_vec_top;
|
|
|
|
typedef struct {
|
|
unsigned char selected[TEST_VEC_MAX_TOKEN_BYTES];
|
|
int selected_len;
|
|
int ntop;
|
|
test_vec_top top[TEST_VEC_MAX_TOP];
|
|
} test_vec_step;
|
|
|
|
typedef struct {
|
|
char id[96];
|
|
char prompt_path[512];
|
|
int ctx;
|
|
int nsteps;
|
|
test_vec_step steps[TEST_VEC_MAX_STEPS];
|
|
} test_vec_case;
|
|
|
|
static char *test_trim_line(char *line) {
|
|
while (*line && isspace((unsigned char)*line)) line++;
|
|
size_t n = strlen(line);
|
|
while (n && isspace((unsigned char)line[n - 1])) line[--n] = '\0';
|
|
return line;
|
|
}
|
|
|
|
static bool test_read_vector_case(FILE *fp, test_vec_case *vc) {
|
|
char line[2048];
|
|
memset(vc, 0, sizeof(*vc));
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
char *p = test_trim_line(line);
|
|
if (!p[0] || p[0] == '#') continue;
|
|
if (sscanf(p, "case %95s %d %d %511s",
|
|
vc->id, &vc->ctx, &vc->nsteps, vc->prompt_path) == 4) {
|
|
TEST_ASSERT(vc->nsteps > 0 && vc->nsteps <= TEST_VEC_MAX_STEPS);
|
|
return true;
|
|
}
|
|
TEST_ASSERT(!"unexpected line before vector case");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool test_fill_vector_case(FILE *fp, test_vec_case *vc) {
|
|
char line[2048];
|
|
int step_index = -1;
|
|
int top_index = 0;
|
|
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
char *p = test_trim_line(line);
|
|
if (!p[0] || p[0] == '#') continue;
|
|
if (!strcmp(p, "end")) return true;
|
|
|
|
if (!strncmp(p, "step ", 5)) {
|
|
char hex[TEST_VEC_MAX_TOKEN_BYTES * 2 + 2];
|
|
int ntop = 0;
|
|
if (sscanf(p, "step %d %257s %d", &step_index, hex, &ntop) != 3) {
|
|
TEST_ASSERT(!"bad vector step line");
|
|
return false;
|
|
}
|
|
TEST_ASSERT(step_index >= 0 && step_index < vc->nsteps);
|
|
TEST_ASSERT(ntop >= 0 && ntop <= TEST_VEC_MAX_TOP);
|
|
vc->steps[step_index].ntop = ntop;
|
|
TEST_ASSERT(test_hex_to_bytes(hex,
|
|
vc->steps[step_index].selected,
|
|
TEST_VEC_MAX_TOKEN_BYTES,
|
|
&vc->steps[step_index].selected_len));
|
|
top_index = 0;
|
|
continue;
|
|
}
|
|
|
|
if (!strncmp(p, "top ", 4)) {
|
|
char hex[TEST_VEC_MAX_TOKEN_BYTES * 2 + 2];
|
|
float lp = 0.0f;
|
|
TEST_ASSERT(step_index >= 0 && step_index < vc->nsteps);
|
|
TEST_ASSERT(top_index < vc->steps[step_index].ntop);
|
|
if (sscanf(p, "top %257s %f", hex, &lp) != 2) {
|
|
TEST_ASSERT(!"bad vector top line");
|
|
return false;
|
|
}
|
|
test_vec_top *top = &vc->steps[step_index].top[top_index++];
|
|
top->logprob = lp;
|
|
TEST_ASSERT(test_hex_to_bytes(hex, top->bytes,
|
|
TEST_VEC_MAX_TOKEN_BYTES, &top->len));
|
|
continue;
|
|
}
|
|
|
|
TEST_ASSERT(!"unexpected vector line");
|
|
return false;
|
|
}
|
|
|
|
TEST_ASSERT(!"unterminated vector case");
|
|
return false;
|
|
}
|
|
|
|
static void test_logprob_vector_case(ds4_engine *engine, const test_vec_case *vc) {
|
|
char *prompt_text = test_read_file(vc->prompt_path);
|
|
TEST_ASSERT(prompt_text != NULL);
|
|
if (!prompt_text) return;
|
|
|
|
ds4_tokens prompt = {0};
|
|
ds4_encode_chat_prompt(engine, "", prompt_text, DS4_THINK_NONE, &prompt);
|
|
free(prompt_text);
|
|
|
|
ds4_session *session = NULL;
|
|
TEST_ASSERT(ds4_session_create(&session, engine, vc->ctx) == 0);
|
|
if (!session) {
|
|
ds4_tokens_free(&prompt);
|
|
return;
|
|
}
|
|
|
|
char err[160];
|
|
TEST_ASSERT(ds4_session_sync(session, &prompt, err, sizeof(err)) == 0);
|
|
|
|
ds4_token_score scores[20];
|
|
for (int i = 0; i < vc->nsteps; i++) {
|
|
const test_vec_step *step = &vc->steps[i];
|
|
int nscore = ds4_session_top_logprobs(session, scores, 20);
|
|
int token = ds4_session_argmax(session);
|
|
if (!test_token_bytes_equal(engine, token, step->selected, step->selected_len)) {
|
|
fprintf(stderr, "ds4-test: vector %s step %d selected token mismatch\n",
|
|
vc->id, i);
|
|
TEST_ASSERT(false);
|
|
}
|
|
|
|
for (int t = 0; t < step->ntop; t++) {
|
|
bool found = false;
|
|
float local_lp = 0.0f;
|
|
for (int j = 0; j < nscore; j++) {
|
|
if (scores[j].id < 0) continue;
|
|
if (test_token_bytes_equal(engine, scores[j].id,
|
|
step->top[t].bytes,
|
|
step->top[t].len)) {
|
|
found = true;
|
|
local_lp = scores[j].logprob;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
fprintf(stderr, "ds4-test: vector %s step %d official top token missing locally\n",
|
|
vc->id, i);
|
|
TEST_ASSERT(false);
|
|
} else if (fabsf(local_lp - step->top[t].logprob) > 4.0f) {
|
|
fprintf(stderr,
|
|
"ds4-test: vector %s step %d logprob delta too high: local=%g official=%g\n",
|
|
vc->id, i, local_lp, step->top[t].logprob);
|
|
TEST_ASSERT(false);
|
|
}
|
|
}
|
|
|
|
if (i + 1 < vc->nsteps) {
|
|
TEST_ASSERT(ds4_session_eval(session, token, err, sizeof(err)) == 0);
|
|
}
|
|
}
|
|
|
|
ds4_session_free(session);
|
|
ds4_tokens_free(&prompt);
|
|
}
|
|
|
|
static void test_official_logprob_vectors(void) {
|
|
const char *path = getenv("DS4_TEST_VECTOR_FILE");
|
|
if (!path || !path[0]) path = "tests/test-vectors/official.vec";
|
|
FILE *fp = fopen(path, "rb");
|
|
TEST_ASSERT(fp != NULL);
|
|
if (!fp) return;
|
|
|
|
ds4_engine *engine = test_get_engine(false);
|
|
if (!engine) {
|
|
fclose(fp);
|
|
return;
|
|
}
|
|
|
|
test_vec_case vc;
|
|
while (test_read_vector_case(fp, &vc)) {
|
|
if (!test_fill_vector_case(fp, &vc)) break;
|
|
fprintf(stderr, "ds4-test: vector %s\n", vc.id);
|
|
test_logprob_vector_case(engine, &vc);
|
|
}
|
|
fclose(fp);
|
|
}
|
|
|
|
static const char *test_tool_call_request_json(void) {
|
|
return
|
|
"{"
|
|
"\"model\":\"deepseek-v4-flash\","
|
|
"\"messages\":[{\"role\":\"user\",\"content\":\"List the files in the current directory. Use the provided tool; do not answer in prose.\"}],"
|
|
"\"tools\":[{\"type\":\"function\",\"function\":{"
|
|
"\"name\":\"list_files\","
|
|
"\"description\":\"List files in a directory.\","
|
|
"\"parameters\":{\"type\":\"object\",\"properties\":{"
|
|
"\"path\":{\"type\":\"string\",\"description\":\"Directory path to list.\"}"
|
|
"},\"required\":[\"path\"]}"
|
|
"}}],"
|
|
"\"tool_choice\":\"auto\","
|
|
"\"think\":false,"
|
|
"\"temperature\":0,"
|
|
"\"max_tokens\":256,"
|
|
"\"stream\":false"
|
|
"}";
|
|
}
|
|
|
|
static void test_tool_call_quality_one(bool quality) {
|
|
ds4_engine *engine = test_get_engine(quality);
|
|
if (!engine) return;
|
|
|
|
request r;
|
|
char err[160];
|
|
TEST_ASSERT(parse_chat_request(engine, NULL, test_tool_call_request_json(),
|
|
512, 32768, &r, err, sizeof(err)));
|
|
|
|
ds4_session *session = NULL;
|
|
TEST_ASSERT(ds4_session_create(&session, engine, 32768) == 0);
|
|
if (!session) {
|
|
request_free(&r);
|
|
return;
|
|
}
|
|
TEST_ASSERT(ds4_session_sync(session, &r.prompt, err, sizeof(err)) == 0);
|
|
|
|
buf text = {0};
|
|
uint64_t rng = 123;
|
|
bool decode_ok = true;
|
|
bool saw_tool_start = false;
|
|
bool saw_tool_end = false;
|
|
for (int i = 0; i < r.max_tokens; i++) {
|
|
int token = ds4_session_sample(session, r.temperature, r.top_k,
|
|
r.top_p, r.min_p, &rng);
|
|
size_t piece_len = 0;
|
|
char *piece = ds4_token_text(engine, token, &piece_len);
|
|
buf_append(&text, piece, piece_len);
|
|
free(piece);
|
|
observe_tool_markers(text.ptr ? text.ptr : "", &saw_tool_start, &saw_tool_end, NULL);
|
|
if (saw_tool_end) break;
|
|
if (ds4_session_eval(session, token, err, sizeof(err)) != 0) {
|
|
decode_ok = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
char *content = NULL;
|
|
char *reasoning = NULL;
|
|
tool_calls calls = {0};
|
|
bool parsed = parse_generated_message(text.ptr ? text.ptr : "",
|
|
&content, &reasoning, &calls);
|
|
TEST_ASSERT(decode_ok);
|
|
TEST_ASSERT(parsed);
|
|
TEST_ASSERT(calls.len > 0);
|
|
TEST_ASSERT(calls.len > 0 && !strcmp(calls.v[0].name, "list_files"));
|
|
|
|
free(content);
|
|
free(reasoning);
|
|
tool_calls_free(&calls);
|
|
buf_free(&text);
|
|
ds4_session_free(session);
|
|
request_free(&r);
|
|
}
|
|
|
|
static void test_tool_call_quality(void) {
|
|
fprintf(stderr, "ds4-test: tool-call quality fast path\n");
|
|
test_tool_call_quality_one(false);
|
|
test_close_engine(false);
|
|
fprintf(stderr, "ds4-test: tool-call quality exact path\n");
|
|
test_tool_call_quality_one(true);
|
|
test_close_engine(true);
|
|
}
|
|
|
|
#endif
|
|
|
|
static void test_server_unit_group(void) {
|
|
ds4_server_unit_tests_run();
|
|
}
|
|
|
|
typedef void (*test_fn)(void);
|
|
|
|
typedef struct {
|
|
const char *flag;
|
|
const char *name;
|
|
const char *desc;
|
|
test_fn fn;
|
|
} ds4_test_entry;
|
|
|
|
static const ds4_test_entry test_entries[] = {
|
|
#ifndef DS4_NO_GPU
|
|
{"--long-context", "long-context", "long-context story fact-recall regression", test_long_story_fact_recall},
|
|
{"--tool-call-quality", "tool-call-quality", "model emits valid DSML tool calls", test_tool_call_quality},
|
|
{"--logprob-vectors", "logprob-vectors", "official API top-logprob vector comparison", test_official_logprob_vectors},
|
|
{"--metal-kernels", "metal-kernels", "isolated Metal kernel numeric regressions", test_metal_f16_matvec_fast_nr0_4},
|
|
#endif
|
|
{"--server", "server", "server parser/rendering/cache unit tests", test_server_unit_group},
|
|
};
|
|
|
|
static void test_print_help(const char *prog) {
|
|
printf("Usage: %s [--all | TEST...]\n\n", prog);
|
|
puts("Tests:");
|
|
puts(" --all");
|
|
puts(" Run every test. This is the default, ordered from slower to faster.");
|
|
for (size_t i = 0; i < sizeof(test_entries) / sizeof(test_entries[0]); i++) {
|
|
printf(" %-20s %s\n", test_entries[i].flag, test_entries[i].desc);
|
|
}
|
|
puts(" --list");
|
|
puts(" Print test names only.");
|
|
puts(" -h, --help");
|
|
puts(" Show this help.");
|
|
puts("\nEnvironment:");
|
|
puts(" DS4_TEST_MODEL=FILE Model path. Default: ds4flash.gguf");
|
|
puts(" DS4_TEST_LONG_PROMPT=FILE Rendered long-context story fact prompt.");
|
|
puts(" DS4_TEST_VECTOR_FILE=FILE Simple official-vector fixture.");
|
|
}
|
|
|
|
static const ds4_test_entry *test_find_entry(const char *arg) {
|
|
for (size_t i = 0; i < sizeof(test_entries) / sizeof(test_entries[0]); i++) {
|
|
if (!strcmp(arg, test_entries[i].flag)) return &test_entries[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void test_run_entry(const ds4_test_entry *entry) {
|
|
int before = test_failures;
|
|
fprintf(stderr, "%s:\n", entry->name);
|
|
entry->fn();
|
|
fprintf(stderr, "%s: ", entry->name);
|
|
ds4_log(stderr,
|
|
test_failures == before ? DS4_LOG_OK : DS4_LOG_ERROR,
|
|
"%s",
|
|
test_failures == before ? "OK" : "ERR");
|
|
fputc('\n', stderr);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
bool run_all = argc == 1;
|
|
bool selected[sizeof(test_entries) / sizeof(test_entries[0])] = {0};
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (!strcmp(argv[i], "--all")) {
|
|
run_all = true;
|
|
} else if (!strcmp(argv[i], "--list")) {
|
|
for (size_t j = 0; j < sizeof(test_entries) / sizeof(test_entries[0]); j++) {
|
|
puts(test_entries[j].flag);
|
|
}
|
|
return 0;
|
|
} else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
|
test_print_help(argv[0]);
|
|
return 0;
|
|
} else {
|
|
const ds4_test_entry *entry = test_find_entry(argv[i]);
|
|
if (!entry) {
|
|
fprintf(stderr, "ds4-test: unknown test switch: %s\n", argv[i]);
|
|
test_print_help(argv[0]);
|
|
return 2;
|
|
}
|
|
selected[(size_t)(entry - test_entries)] = true;
|
|
}
|
|
}
|
|
|
|
if (run_all) {
|
|
for (size_t i = 0; i < sizeof(test_entries) / sizeof(test_entries[0]); i++) {
|
|
test_run_entry(&test_entries[i]);
|
|
}
|
|
} else {
|
|
for (size_t i = 0; i < sizeof(test_entries) / sizeof(test_entries[0]); i++) {
|
|
if (selected[i]) test_run_entry(&test_entries[i]);
|
|
}
|
|
}
|
|
|
|
#ifndef DS4_NO_GPU
|
|
test_close_engines();
|
|
#endif
|
|
|
|
if (test_failures) {
|
|
fprintf(stderr, "ds4 tests: %d failure(s)\n", test_failures);
|
|
return 1;
|
|
}
|
|
puts("ds4 tests: ok");
|
|
return 0;
|
|
}
|