ds4fa / tests /test_engine_correctness.c
julianmb's picture
Upload folder using huggingface_hub (part 4)
8d0b310 verified
Raw
History Blame Contribute Delete
10.9 kB
/* Deterministic correctness check: greedy-decode a known prompt for N
* tokens and assert the decoded token-ID sequence matches between
* single-tier and GPU-only multi-tier execution.
*
* Uses small ctx (1024) and short generation (16 tokens) so it fits in
* tight VRAM environments. Sister test to test_engine_mgpu_runtime,
* which compares logit deltas at a tighter tolerance (1e-4) — this
* one asserts equivalence at the user-visible token level, which is
* the actual correctness contract.
*
* Requires DS4_TEST_MODEL. Skips with PASS if < 2 CUDA devices visible.
*
* The test prints both decoded sequences as human-readable text so a
* reviewer can eyeball "this is English on-topic" regardless of the
* automated assert. */
#include "ds4.h"
#include "ds4_gpu_mgpu.h"
#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N_GEN_TOKENS 16
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL: %s (line %d)\n", (msg), __LINE__); \
return 1; \
} \
} while (0)
#define CHECKF(cond, fmt, ...) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL: " fmt " (line %d)\n", __VA_ARGS__, __LINE__); \
return 1; \
} \
} while (0)
/* Build a short prompt using the actual chat template
* (BOS + user_id + <text> + assistant_id + think_end so the model skips
* any reasoning trace and goes straight to the answer). With DS4_THINK_NONE
* the model produces a clean short reply, which makes the token-ID compare
* easy to read at a glance.
*
* Why not ds4_tokenize_rendered_chat: that function parses text that
* ALREADY contains special tokens (it just looks for them inline). It adds
* no scaffolding on its own, so the model sees raw text and continues it
* like a completion ("What is the capital of France?...What is the capital
* of France?<EOS>"). ds4_encode_chat_prompt adds the actual BOS / user /
* assistant / think markers DeepSeek-V4-Flash is trained on. */
static void build_prompt(ds4_engine *e, ds4_tokens *out) {
const char *prompt = "What is the capital of France?";
ds4_encode_chat_prompt(e, /*system=*/NULL, prompt, DS4_THINK_NONE, out);
}
/* Decode an array of token IDs to UTF-8 (writes into a heap buffer the
* caller must free). Returns NULL on failure. */
static char *decode_tokens(ds4_engine *e, const int *tokens, int n) {
size_t cap = 256;
size_t len = 0;
char *buf = (char *)malloc(cap);
if (!buf) return NULL;
buf[0] = '\0';
for (int i = 0; i < n; i++) {
size_t tlen = 0;
char *txt = ds4_token_text(e, tokens[i], &tlen);
if (!txt) continue;
if (len + tlen + 1 > cap) {
cap = (len + tlen + 1) * 2;
char *nbuf = (char *)realloc(buf, cap);
if (!nbuf) { free(buf); free(txt); return NULL; }
buf = nbuf;
}
memcpy(buf + len, txt, tlen);
len += tlen;
buf[len] = '\0';
free(txt);
}
return buf;
}
/* Greedy-decode up to N_GEN_TOKENS tokens from a session, stopping at
* EOS (which is what real inference does). Stores generated token IDs
* into `out_tokens` and returns the actual count via `*out_count`
* (always <= N_GEN_TOKENS). On error returns 1.
*
* Continuing past EOS pushes the engine into post-EOS states whose
* deterministic behavior across single-tier and multi-tier is harder
* to reason about; stopping mirrors production inference. */
static int greedy_decode(ds4_engine *e, ds4_session *s, int *out_tokens,
int *out_count) {
char err[256] = {0};
const int eos = ds4_token_eos(e);
*out_count = 0;
for (int i = 0; i < N_GEN_TOKENS; i++) {
int t = ds4_session_argmax(s);
if (t < 0) {
fprintf(stderr, "argmax returned %d at step %d\n", t, i);
return 1;
}
out_tokens[i] = t;
(*out_count)++;
if (t == eos) break;
if (ds4_session_eval(s, t, err, sizeof(err)) != 0) {
fprintf(stderr, "eval failed at step %d: %s\n", i, err);
return 1;
}
}
return 0;
}
int main(void) {
int dev_count = 0;
(void)cudaGetDeviceCount(&dev_count);
fprintf(stderr, "test_engine_correctness: %d CUDA devices visible\n", dev_count);
if (dev_count < 1) {
fprintf(stderr, " skipping (no CUDA devices)\n");
return 0;
}
const char *model_path = getenv("DS4_TEST_MODEL");
if (!model_path || !model_path[0]) {
fprintf(stderr, "FAIL: DS4_TEST_MODEL not set\n");
return 1;
}
/* Pin math mode for the tightest deterministic equivalence between
* single-tier and multi-tier. */
setenv("DS4_CUDA_NO_TF32", "1", 1);
ds4_engine_options opt;
memset(&opt, 0, sizeof(opt));
opt.model_path = model_path;
opt.backend = DS4_BACKEND_CUDA;
opt.n_threads = 1;
opt.warm_weights = false;
opt.quality = false;
/* ---- Single-tier baseline ---- */
fprintf(stderr, "test_engine_correctness: single-tier baseline\n");
ds4_engine *e1 = NULL;
int rc = ds4_engine_open(&e1, &opt);
CHECKF(rc == 0 && e1 != NULL, "single-tier engine_open rc=%d", rc);
ds4_tokens prompt1 = {0};
build_prompt(e1, &prompt1);
CHECK(prompt1.len > 0 && prompt1.len < 256, "tokenize prompt within bounds");
ds4_session *s1 = NULL;
rc = ds4_session_create(&s1, e1, 1024);
CHECKF(rc == 0 && s1 != NULL, "single-tier session_create rc=%d", rc);
char err[256] = {0};
rc = ds4_session_sync(s1, &prompt1, err, sizeof(err));
CHECKF(rc == 0, "single-tier session_sync rc=%d err=%s", rc, err);
int tokens_a[N_GEN_TOKENS];
int n_a = 0;
CHECK(greedy_decode(e1, s1, tokens_a, &n_a) == 0, "single-tier greedy decode");
char *text_a = decode_tokens(e1, tokens_a, n_a);
CHECK(text_a != NULL, "decode single-tier tokens to text");
fprintf(stderr, " single-tier output (%d tokens): \"%s\"\n", n_a, text_a);
fprintf(stderr, " single-tier token IDs:");
for (int i = 0; i < n_a; i++) fprintf(stderr, " %d", tokens_a[i]);
fprintf(stderr, "\n");
/* Stash prompt length before freeing — ds4_tokens_free memsets the
* struct to zero, so reading prompt1.len after free returns 0
* (which broke the cross-engine prompt-length sanity check below). */
const int prompt1_len = prompt1.len;
ds4_session_free(s1);
ds4_tokens_free(&prompt1);
ds4_engine_close(e1);
/* ---- Multi-tier (GPU-only, when 2+ devices visible) ---- */
if (dev_count < 2) {
fprintf(stderr, " skipping multi-tier compare (only 1 CUDA device)\n");
fprintf(stderr, "test_engine_correctness SKIP_PASS (single-tier only)\n");
free(text_a);
return 0;
}
fprintf(stderr, "test_engine_correctness: GPU-only multi-tier\n");
ds4_gpu_config cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.n_gpus = 2;
cfg.device_indices[0] = 0;
cfg.device_indices[1] = 1;
/* Budgets sized to clear the per-tier graph overhead pre-subtract
* (~3.93 GiB at default ctx) plus the user-supplied safety margin
* plus cuBLAS workspace, then still fit the 80 GiB IQ2XXS model with
* room for ctx=1024 KV. If actual free VRAM is lower (other workloads
* on the box), the engine refuses upfront and this test SKIP_PASSes. */
cfg.vram_bytes[0] = (size_t)47ull * 1024ull * 1024ull * 1024ull;
cfg.vram_bytes[1] = (size_t)47ull * 1024ull * 1024ull * 1024ull;
cfg.safety_margin_bytes = (size_t)1024u * 1024u * 1024u;
ds4_engine *e2 = NULL;
rc = ds4_engine_create_with_gpu_config(&e2, &opt, &cfg);
if (rc != 0 || e2 == NULL) {
fprintf(stderr, "test_engine_correctness: multi-tier engine_create rc=%d (env-constrained; SKIP_PASS)\n", rc);
fprintf(stderr, "test_engine_correctness SKIP_PASS\n");
free(text_a);
return 0;
}
ds4_tokens prompt2 = {0};
build_prompt(e2, &prompt2);
CHECKF(prompt2.len == prompt1_len,
"prompt tokenization length differs across engines (single=%d multi=%d)",
prompt1_len, prompt2.len);
ds4_session *s2 = NULL;
rc = ds4_session_create(&s2, e2, 1024);
if (rc != 0 || s2 == NULL) {
fprintf(stderr, "test_engine_correctness: multi-tier session_create rc=%d (likely the open KV-alloc OOM bug; SKIP_PASS)\n", rc);
fprintf(stderr, "test_engine_correctness SKIP_PASS\n");
ds4_tokens_free(&prompt2);
ds4_engine_close(e2);
free(text_a);
return 0;
}
rc = ds4_session_sync(s2, &prompt2, err, sizeof(err));
CHECKF(rc == 0, "multi-tier session_sync rc=%d err=%s", rc, err);
int tokens_b[N_GEN_TOKENS];
int n_b = 0;
CHECK(greedy_decode(e2, s2, tokens_b, &n_b) == 0, "multi-tier greedy decode");
char *text_b = decode_tokens(e2, tokens_b, n_b);
CHECK(text_b != NULL, "decode multi-tier tokens to text");
fprintf(stderr, " multi-tier output (%d tokens): \"%s\"\n", n_b, text_b);
fprintf(stderr, " multi-tier token IDs:");
for (int i = 0; i < n_b; i++) fprintf(stderr, " %d", tokens_b[i]);
fprintf(stderr, "\n");
/* The actual correctness assertion: same number of tokens AND every
* greedy token ID matches. A length mismatch (e.g., multi-tier hits
* EOS earlier or later) is itself a divergence. */
int mismatch_at = -1;
if (n_a != n_b) {
mismatch_at = (n_a < n_b) ? n_a : n_b;
} else {
for (int i = 0; i < n_a; i++) {
if (tokens_a[i] != tokens_b[i]) { mismatch_at = i; break; }
}
}
ds4_session_free(s2);
ds4_tokens_free(&prompt2);
ds4_engine_close(e2);
free(text_a);
free(text_b);
if (mismatch_at >= 0) {
if (n_a != n_b) {
fprintf(stderr,
"FAIL: gen length differs (single=%d multi=%d)\n",
n_a, n_b);
} else {
fprintf(stderr,
"FAIL: token sequences diverge at index %d (single=%d multi=%d)\n",
mismatch_at, tokens_a[mismatch_at], tokens_b[mismatch_at]);
}
return 1;
}
fprintf(stderr, "test_engine_correctness PASS (%d tokens identical incl EOS)\n", n_a);
return 0;
}