/* 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 #include #include #include #include #include #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; }