| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #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; |
| } |
|
|