| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| #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 DECODE_STEPS 24 |
| #define TEST_CTX 1024 |
|
|
| static const char *prompts[] = { |
| "Write the integers from 1 to 200, separated by commas. Do not stop early.", |
| "Write a compact C function that validates UTF-8, then explain each branch.", |
| "List the first one hundred prime numbers and show no derivation.", |
| "Describe how an LRU cache works using a detailed worked example.", |
| "Write the first eighty Fibonacci numbers, one per line.", |
| "Explain B-tree insertion with a concrete sequence of twenty keys.", |
| "Generate SQL that creates and queries a small issue tracker schema.", |
| "Compare TCP and UDP using six precise operational examples.", |
| }; |
|
|
| static uint64_t hash_bytes(const void *data, size_t len) { |
| const unsigned char *p = data; |
| uint64_t hash = UINT64_C(1469598103934665603); |
| for (size_t i = 0; i < len; i++) { |
| hash ^= p[i]; |
| hash *= UINT64_C(1099511628211); |
| } |
| return hash; |
| } |
|
|
| 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, int session, int step) { |
| fprintf(stderr, "FAIL: %s session=%d step=%d\n", what, session, step); |
| exit(1); |
| } |
|
|
| static void compare_frontier(ds4_session *control, const float *expected, |
| int expected_argmax, float *actual, int vocab, |
| int session, int step, |
| float *worst_abs, int *nonexact) { |
| if (ds4_session_copy_logits(control, actual, vocab) != vocab) { |
| fail("copy logits", session, step); |
| } |
|
|
| float max_abs = 0.0f; |
| int different = 0; |
| for (int i = 0; i < vocab; i++) { |
| float d = fabsf(actual[i] - expected[i]); |
| if (memcmp(&actual[i], &expected[i], sizeof(float)) != 0) different++; |
| if (!isfinite(d)) d = INFINITY; |
| if (d > max_abs) max_abs = d; |
| } |
| if (max_abs > *worst_abs) *worst_abs = max_abs; |
| *nonexact += different; |
|
|
| int actual_argmax = ds4_session_argmax(control); |
| if (different != 0 || actual_argmax != expected_argmax) { |
| fprintf(stderr, |
| "FAIL: logits mismatch session=%d step=%d control=%d batch=%d " |
| "max_abs=%g differing=%d\n", |
| session, step, actual_argmax, expected_argmax, |
| max_abs, different); |
| exit(1); |
| } |
| } |
|
|
| int main(void) { |
| const char *model = getenv("DS4_TEST_MODEL"); |
| if (!model || !model[0]) { |
| fprintf(stderr, "FAIL: DS4_TEST_MODEL is not set\n"); |
| return 1; |
| } |
|
|
| int session_count = DEFAULT_SESSION_COUNT; |
| const char *session_count_env = getenv("DS4_TEST_SESSION_COUNT"); |
| if (session_count_env && session_count_env[0]) { |
| session_count = atoi(session_count_env); |
| } |
| if (session_count < 2 || session_count > MAX_SESSION_COUNT) { |
| fprintf(stderr, |
| "FAIL: DS4_TEST_SESSION_COUNT must be between 2 and %d\n", |
| MAX_SESSION_COUNT); |
| return 1; |
| } |
|
|
| int test_ctx = TEST_CTX; |
| const char *test_ctx_env = getenv("DS4_TEST_CONTEXT"); |
| if (test_ctx_env && test_ctx_env[0]) test_ctx = atoi(test_ctx_env); |
| if (test_ctx < TEST_CTX || test_ctx > 65536) { |
| fprintf(stderr, |
| "FAIL: DS4_TEST_CONTEXT must be between %d and 65536\n", |
| TEST_CTX); |
| return 1; |
| } |
|
|
| int long_words = 0; |
| const char *long_words_env = getenv("DS4_TEST_LONG_WORDS"); |
| if (long_words_env && long_words_env[0]) long_words = atoi(long_words_env); |
| if (long_words < 0 || long_words > test_ctx - 128) { |
| fprintf(stderr, |
| "FAIL: DS4_TEST_LONG_WORDS must leave at least 128 context tokens\n"); |
| return 1; |
| } |
| char *long_prompt = NULL; |
| if (long_words != 0) { |
| const char word[] = " token"; |
| const size_t word_len = sizeof(word) - 1u; |
| long_prompt = malloc((size_t)long_words * word_len + 1u); |
| if (!long_prompt) fail("long prompt allocation", -1, -1); |
| char *dst = long_prompt; |
| for (int i = 0; i < long_words; i++) { |
| memcpy(dst, word, word_len); |
| dst += word_len; |
| } |
| *dst = '\0'; |
| } |
|
|
| setenv("DS4_CUDA_SESSION_BATCH_MOE", "1", 1); |
|
|
| ds4_gpu_config gpu_cfg = {0}; |
| bool skip_cuda = false; |
| char err[256] = {0}; |
| if (parse_gpu_vram_arg("auto", "0,2,4,6,1,3,5,7", |
| &gpu_cfg, &skip_cuda, err, sizeof(err)) != 0 || |
| skip_cuda) { |
| fprintf(stderr, "FAIL: GPU configuration: %s\n", err); |
| return 1; |
| } |
|
|
| ds4_engine_options opt = { |
| .model_path = model, |
| .backend = DS4_BACKEND_CUDA, |
| .n_threads = 1, |
| .cuda_tensor_parallel = true, |
| .share_session_prefill_workspace = true, |
| .placement_ctx_hint = (uint32_t)test_ctx, |
| }; |
| ds4_engine *engine = NULL; |
| if (ds4_engine_create_with_gpu_config(&engine, &opt, &gpu_cfg) != 0) { |
| fprintf(stderr, "FAIL: engine open\n"); |
| return 1; |
| } |
|
|
| ds4_session *batched[MAX_SESSION_COUNT] = {0}; |
| ds4_tokens prompt[MAX_SESSION_COUNT] = {0}; |
| const int prompt_count = (int)(sizeof(prompts) / sizeof(prompts[0])); |
| for (int i = 0; i < session_count; i++) { |
| const char *prompt_text = long_prompt && (i & 1) == 0 |
| ? long_prompt : prompts[i % prompt_count]; |
| ds4_encode_chat_prompt(engine, NULL, prompt_text, DS4_THINK_NONE, |
| &prompt[i]); |
| if (long_prompt && (i & 1) == 0) { |
| fprintf(stderr, |
| "test_cuda_session_batch long prompt session=%d tokens=%d\n", |
| i, prompt[i].len); |
| if (prompt[i].len + DECODE_STEPS >= test_ctx) { |
| fail("long prompt exceeds decode context", i, -1); |
| } |
| } |
| if (ds4_session_create(&batched[i], engine, (uint32_t)test_ctx) != 0) { |
| fail("session create", i, -1); |
| } |
| if (ds4_session_sync(batched[i], &prompt[i], err, sizeof(err)) != 0) { |
| fprintf(stderr, "FAIL: prefill session=%d: %s\n", i, err); |
| return 1; |
| } |
| } |
|
|
| const int vocab = ds4_engine_vocab_size(engine); |
| const size_t frontiers = (size_t)(DECODE_STEPS + 1) * (size_t)session_count; |
| float *expected = malloc(frontiers * (size_t)vocab * sizeof(*expected)); |
| int *expected_argmax = malloc(frontiers * sizeof(*expected_argmax)); |
| float *actual = malloc((size_t)vocab * sizeof(*actual)); |
| if (!expected || !expected_argmax || !actual) { |
| fail("logit allocation", -1, -1); |
| } |
|
|
| double batch_ms[MAX_SESSION_COUNT + 1] = {0}; |
| int batch_calls[MAX_SESSION_COUNT + 1] = {0}; |
| uint64_t evaluated_rows = 0; |
| double evaluated_ms = 0.0; |
| const bool power_of_two = (session_count & (session_count - 1)) == 0; |
| for (int step = 0; step <= DECODE_STEPS; step++) { |
| int tokens[MAX_SESSION_COUNT]; |
| for (int i = 0; i < session_count; i++) { |
| const size_t frontier = |
| (size_t)step * (size_t)session_count + (size_t)i; |
| if (ds4_session_copy_logits( |
| batched[i], expected + frontier * (size_t)vocab, |
| vocab) != vocab) { |
| fail("archive logits", i, step); |
| } |
| tokens[i] = ds4_session_argmax(batched[i]); |
| expected_argmax[frontier] = tokens[i]; |
| } |
| if (step == DECODE_STEPS) break; |
|
|
| const int group = !power_of_two ? session_count : |
| step % 3 == 0 ? session_count : |
| step % 3 == 1 ? session_count / 2 : |
| session_count / 4 > 1 |
| ? session_count / 4 : 2; |
| for (int base = 0; base < session_count; base += group) { |
| ds4_decode_item items[MAX_SESSION_COUNT]; |
| const int rows = group < session_count - base |
| ? group : session_count - base; |
| for (int row = 0; row < rows; row++) { |
| int i = (step & 1) ? base + rows - 1 - row : base + row; |
| items[row].session = batched[i]; |
| items[row].token = tokens[i]; |
| } |
| const double started = now_ms(); |
| const int eval_rc = ds4_sessions_eval_batch(items, rows, |
| err, sizeof(err)); |
| const double elapsed = now_ms() - started; |
| if (eval_rc != 0) { |
| fprintf(stderr, |
| "FAIL: batch eval size=%d base=%d step=%d: %s\n", |
| rows, base, step, err); |
| return 1; |
| } |
| batch_ms[rows] += elapsed; |
| batch_calls[rows]++; |
| evaluated_rows += (uint64_t)rows; |
| evaluated_ms += elapsed; |
| } |
| } |
| fprintf(stderr, |
| "decode batch timing: rows=%llu total=%.3f ms aggregate=%.1f tok/s\n", |
| (unsigned long long)evaluated_rows, evaluated_ms, |
| evaluated_ms > 0.0 ? (double)evaluated_rows * 1000.0 / evaluated_ms : 0.0); |
| for (int rows = 2; rows <= session_count; rows++) { |
| if (batch_calls[rows] != 0) { |
| fprintf(stderr, |
| " batch=%d calls=%d mean=%.3f ms aggregate=%.1f tok/s\n", |
| rows, batch_calls[rows], |
| batch_ms[rows] / batch_calls[rows], |
| (double)rows * 1000.0 * batch_calls[rows] / batch_ms[rows]); |
| } |
| } |
| for (int i = 0; i < session_count; i++) { |
| ds4_session_free(batched[i]); |
| batched[i] = NULL; |
| } |
|
|
| if (getenv("DS4_TEST_BATCH_ONLY") != NULL) { |
| const size_t logits_bytes = |
| frontiers * (size_t)vocab * sizeof(*expected); |
| fprintf(stderr, |
| "test_cuda_session_batch PASS batch-only sessions=%d steps=%d " |
| "frontier0_hash=%016llx logit_hash=%016llx " |
| "argmax_hash=%016llx\n", |
| session_count, DECODE_STEPS, |
| (unsigned long long)hash_bytes( |
| expected, |
| (size_t)session_count * (size_t)vocab * sizeof(*expected)), |
| (unsigned long long)hash_bytes(expected, logits_bytes), |
| (unsigned long long)hash_bytes( |
| expected_argmax, frontiers * sizeof(*expected_argmax))); |
| free(actual); |
| free(expected_argmax); |
| free(expected); |
| for (int i = 0; i < session_count; i++) { |
| ds4_tokens_free(&prompt[i]); |
| } |
| free(long_prompt); |
| ds4_engine_close(engine); |
| return 0; |
| } |
|
|
| float worst_abs = 0.0f; |
| int nonexact = 0; |
| for (int i = 0; i < session_count; i++) { |
| ds4_session *control = NULL; |
| if (ds4_session_create(&control, engine, (uint32_t)test_ctx) != 0 || |
| ds4_session_sync(control, &prompt[i], err, sizeof(err)) != 0) { |
| fprintf(stderr, "FAIL: control prefill session=%d: %s\n", i, err); |
| return 1; |
| } |
| for (int step = 0; step <= DECODE_STEPS; step++) { |
| const size_t frontier = |
| (size_t)step * (size_t)session_count + (size_t)i; |
| compare_frontier(control, |
| expected + frontier * (size_t)vocab, |
| expected_argmax[frontier], actual, vocab, |
| i, step, &worst_abs, &nonexact); |
| if (step < DECODE_STEPS && |
| ds4_session_eval(control, expected_argmax[frontier], |
| err, sizeof(err)) != 0) { |
| fprintf(stderr, |
| "FAIL: control eval session=%d step=%d: %s\n", |
| i, step, err); |
| return 1; |
| } |
| } |
| ds4_session_free(control); |
| } |
|
|
| fprintf(stderr, |
| "test_cuda_session_batch PASS sessions=%d steps=%d " |
| "worst_logit_abs=%g nonexact_logits=%d\n", |
| session_count, DECODE_STEPS, worst_abs, nonexact); |
|
|
| free(actual); |
| free(expected_argmax); |
| free(expected); |
| for (int i = 0; i < session_count; i++) { |
| ds4_tokens_free(&prompt[i]); |
| } |
| free(long_prompt); |
| ds4_engine_close(engine); |
| return 0; |
| } |
|
|