File size: 11,118 Bytes
8d0b310 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | /* Model-backed CUDA oracle for mixed prefill and multi-session decode.
*
* The control advances one prefill suffix and then a decode batch. The mixed
* side advances the same rows through the native shared-MoE path. Every full
* vocabulary logit must remain bit-exact. This is intentionally not part of
* `make test`: it requires the large DeepSeek model and the eight-GPU TP/EP
* configuration.
*
* Run with:
* DS4_TEST_MODEL=/path/to/model.gguf make test-cuda-mixed-batch
*
* Exercise compressed/indexed context with:
* DS4_TEST_CONTEXT=4096 DS4_TEST_MIXED_INITIAL=2048 \
* DS4_TEST_MIXED_ROUNDS=8 make test-cuda-mixed-batch
*/
#include "ds4.h"
#include "ds4_gpu_args.h"
#include "ds4_gpu_mgpu.h"
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define DEFAULT_SESSION_COUNT 8
#define MAX_SESSION_COUNT 16
#define DEFAULT_CONTEXT 1024
#define DEFAULT_INITIAL 128
#define DEFAULT_QUANTUM 128
#define DEFAULT_ROUNDS 3
static const char *decode_prompts[] = {
"Count upward using comma-separated integers.",
"Write a C function and continue it in detail.",
"List prime numbers without explanation.",
"Describe a database transaction step by step.",
"Produce a long JSON array of short strings.",
"Explain TCP congestion control precisely.",
"Write a long deterministic SQL migration.",
"Enumerate powers of two with one per line.",
};
static double now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
}
static void fail(const char *what, const char *err, int row, int round) {
fprintf(stderr, "FAIL: %s row=%d round=%d%s%s\n",
what, row, round, err && err[0] ? ": " : "",
err && err[0] ? err : "");
exit(1);
}
static int env_int(const char *name, int fallback, int min, int max) {
const char *value = getenv(name);
if (!value || !value[0]) return fallback;
char *end = NULL;
long parsed = strtol(value, &end, 10);
if (end == value || *end != '\0' || parsed < min || parsed > max) {
fprintf(stderr, "FAIL: %s must be in [%d, %d]\n", name, min, max);
exit(1);
}
return (int)parsed;
}
static void compare_logits(ds4_session *mixed, ds4_session *control,
float *mixed_logits, float *control_logits,
int vocab, const char *kind, int row, int round) {
if (ds4_session_copy_logits(mixed, mixed_logits, vocab) != vocab ||
ds4_session_copy_logits(control, control_logits, vocab) != vocab) {
fail("copy logits", NULL, row, round);
}
int different = 0;
float worst = 0.0f;
for (int i = 0; i < vocab; i++) {
if (memcmp(&mixed_logits[i], &control_logits[i], sizeof(float)) != 0) {
different++;
}
float delta = fabsf(mixed_logits[i] - control_logits[i]);
if (!isfinite(delta)) delta = INFINITY;
if (delta > worst) worst = delta;
}
const int mixed_argmax = ds4_session_argmax(mixed);
const int control_argmax = ds4_session_argmax(control);
if (different != 0 || mixed_argmax != control_argmax) {
fprintf(stderr,
"FAIL: %s logits row=%d round=%d differing=%d worst=%g "
"argmax=%d/%d\n",
kind, row, round, different, worst,
mixed_argmax, control_argmax);
exit(1);
}
}
int main(void) {
const char *model = getenv("DS4_TEST_MODEL");
if (!model || !model[0]) fail("DS4_TEST_MODEL is not set", NULL, -1, -1);
const int session_count = env_int(
"DS4_TEST_SESSION_COUNT", DEFAULT_SESSION_COUNT, 3,
MAX_SESSION_COUNT);
const int context = env_int(
"DS4_TEST_CONTEXT", DEFAULT_CONTEXT, DEFAULT_CONTEXT, 65536);
const int initial = env_int(
"DS4_TEST_MIXED_INITIAL", DEFAULT_INITIAL, 128, context - 1);
const int quantum = env_int(
"DS4_TEST_MIXED_QUANTUM", DEFAULT_QUANTUM, 1, context - 1);
const int rounds = env_int(
"DS4_TEST_MIXED_ROUNDS", DEFAULT_ROUNDS, 1, 64);
if ((int64_t)initial + (int64_t)quantum * rounds >= context) {
fail("mixed test frontier reaches context limit", NULL, -1, -1);
}
setenv("DS4_CUDA_MIXED_PREFILL_DECODE", "1", 1);
const char *gpu_devices = getenv("DS4_TEST_GPU_DEVICES");
if (!gpu_devices || !gpu_devices[0]) gpu_devices = "0,2,4,6,1,3,5,7";
ds4_gpu_config gpu = {0};
bool skip_cuda = false;
char err[256] = {0};
if (parse_gpu_vram_arg("auto", gpu_devices, &gpu, &skip_cuda,
err, sizeof(err)) != 0 || skip_cuda) {
fail("GPU configuration", err, -1, -1);
}
ds4_engine_options options = {
.model_path = model,
.backend = DS4_BACKEND_CUDA,
.n_threads = 1,
.cuda_tensor_parallel = true,
.share_session_prefill_workspace = true,
.placement_ctx_hint = (uint32_t)context,
};
ds4_engine *engine = NULL;
if (ds4_engine_create_with_gpu_config(&engine, &options, &gpu) != 0) {
fail("engine open", NULL, -1, -1);
}
const char phrase[] = " numbered-token";
const size_t repeat_count = (size_t)context * 2u;
const size_t text_size = repeat_count * (sizeof(phrase) - 1u) + 1u;
char *text = malloc(text_size);
if (!text) fail("prefill text allocation", NULL, -1, -1);
char *dst = text;
for (size_t i = 0; i < repeat_count; i++) {
memcpy(dst, phrase, sizeof(phrase) - 1u);
dst += sizeof(phrase) - 1u;
}
*dst = '\0';
ds4_tokens long_prompt = {0};
ds4_tokenize_text(engine, text, &long_prompt);
free(text);
const int final_frontier = initial + quantum * rounds;
if (long_prompt.len < final_frontier) {
fail("generated prefill prompt is too short", NULL, -1, -1);
}
ds4_session *prefill_mixed = NULL;
ds4_session *prefill_control = NULL;
if (ds4_session_create(&prefill_mixed, engine, (uint32_t)context) != 0 ||
ds4_session_create(&prefill_control, engine, (uint32_t)context) != 0) {
fail("prefill session creation", NULL, -1, -1);
}
ds4_tokens prefix = long_prompt;
prefix.len = initial;
if (ds4_session_sync(prefill_mixed, &prefix, err, sizeof(err)) != 0 ||
ds4_session_sync(prefill_control, &prefix, err, sizeof(err)) != 0) {
fail("initial prefill", err, -1, -1);
}
ds4_session *mixed[MAX_SESSION_COUNT] = {0};
ds4_session *control[MAX_SESSION_COUNT] = {0};
ds4_tokens decode_prompt[MAX_SESSION_COUNT] = {0};
const int prompt_count =
(int)(sizeof(decode_prompts) / sizeof(decode_prompts[0]));
for (int i = 0; i < session_count; i++) {
ds4_encode_chat_prompt(
engine, NULL, decode_prompts[i % prompt_count],
DS4_THINK_NONE, &decode_prompt[i]);
if (ds4_session_create(&mixed[i], engine, (uint32_t)context) != 0 ||
ds4_session_create(&control[i], engine, (uint32_t)context) != 0 ||
ds4_session_sync(mixed[i], &decode_prompt[i], err, sizeof(err)) != 0 ||
ds4_session_sync(control[i], &decode_prompt[i], err, sizeof(err)) != 0) {
fail("decode session setup", err, i, -1);
}
}
const int vocab = ds4_engine_vocab_size(engine);
float *mixed_logits = malloc((size_t)vocab * sizeof(*mixed_logits));
float *control_logits = malloc((size_t)vocab * sizeof(*control_logits));
if (!mixed_logits || !control_logits) {
fail("logit allocation", NULL, -1, -1);
}
double separate_total = 0.0;
double mixed_total = 0.0;
const bool allow_fallback = getenv("DS4_TEST_ALLOW_FALLBACK") != NULL;
bool all_native = true;
for (int round = 0; round < rounds; round++) {
ds4_decode_item mixed_items[MAX_SESSION_COUNT];
ds4_decode_item control_items[MAX_SESSION_COUNT];
for (int row = 0; row < session_count; row++) {
const int i = (round & 1) != 0
? session_count - 1 - row : row;
const int mixed_token = ds4_session_argmax(mixed[i]);
const int control_token = ds4_session_argmax(control[i]);
if (mixed_token != control_token) {
fail("pre-step argmax mismatch", NULL, i, round);
}
mixed_items[row] = (ds4_decode_item){mixed[i], mixed_token};
control_items[row] = (ds4_decode_item){control[i], control_token};
}
ds4_tokens target = long_prompt;
target.len = initial + (round + 1) * quantum;
double started = now_ms();
if (ds4_session_sync(
prefill_control, &target, err, sizeof(err)) != 0 ||
ds4_sessions_eval_batch(
control_items, session_count, err, sizeof(err)) != 0) {
fail("serialized step", err, -1, round);
}
const double separate_ms = now_ms() - started;
separate_total += separate_ms;
const uint64_t native_before = ds4_test_mixed_native_count();
started = now_ms();
if (ds4_sessions_eval_batch_with_prefill(
mixed_items, session_count, prefill_mixed, &target,
err, sizeof(err)) != 0) {
fail("mixed step", err, -1, round);
}
const double mixed_ms = now_ms() - started;
mixed_total += mixed_ms;
const uint64_t native_after = ds4_test_mixed_native_count();
if (native_after != native_before + 1u) {
all_native = false;
if (!allow_fallback || native_after != native_before) {
fail("mixed call used the serialized fallback", NULL, -1, round);
}
}
compare_logits(prefill_mixed, prefill_control,
mixed_logits, control_logits, vocab,
"prefill", -1, round);
for (int i = 0; i < session_count; i++) {
compare_logits(mixed[i], control[i],
mixed_logits, control_logits, vocab,
"decode", i, round);
}
fprintf(stderr,
"PASS: round=%d rows=%d+%d separate=%.3f ms mixed=%.3f ms "
"speedup=%.3fx\n",
round, quantum, session_count, separate_ms, mixed_ms,
separate_ms / mixed_ms);
}
fprintf(stderr,
"test_cuda_mixed_batch PASS context=%d initial=%d rounds=%d "
"rows=%d+%d exact mode=%s speedup=%.3fx\n",
context, initial, rounds, quantum, session_count,
all_native ? "native" : "fallback",
separate_total / mixed_total);
free(control_logits);
free(mixed_logits);
for (int i = 0; i < session_count; i++) {
ds4_tokens_free(&decode_prompt[i]);
ds4_session_free(control[i]);
ds4_session_free(mixed[i]);
}
ds4_session_free(prefill_control);
ds4_session_free(prefill_mixed);
ds4_tokens_free(&long_prompt);
ds4_engine_close(engine);
return 0;
}
|