| … | … | @@ -139,7 +139,6 @@ INCLUDE(OpenCogCython) |
| 139 | 139 | |
| 140 | 140 | ADD_SUBDIRECTORY(cmake) |
| 141 | 141 | ADD_SUBDIRECTORY(opencog) |
| 142 | ADD_SUBDIRECTORY(benchmarks) | |
| 143 | 142 | |
| 144 | 143 | INCLUDE(OpenCogTestOptions) |
| 145 | 144 | OPENCOG_SETUP_TESTING() |
| … | … | @@ -1,33 +0,0 @@ |
| 1 | # | |
| 2 | # GPU AtomSpace benchmarks. | |
| 3 | # | |
| 4 | ||
| 5 | IF (HAVE_CUDA) | |
| 6 | SET(CUDA_BENCHMARKS | |
| 7 | bench-kernels | |
| 8 | bench-pipeline | |
| 9 | bench-corpus | |
| 10 | bench-corpus-ck | |
| 11 | bench-mi-comparison | |
| 12 | ) | |
| 13 | ||
| 14 | FOREACH(BENCH_NAME ${CUDA_BENCHMARKS}) | |
| 15 | ADD_EXECUTABLE(${BENCH_NAME} ${BENCH_NAME}.cu) | |
| 16 | SET_TARGET_PROPERTIES(${BENCH_NAME} PROPERTIES | |
| 17 | CUDA_SEPARABLE_COMPILATION ON | |
| 18 | CUDA_ARCHITECTURES "50;72;75;86;89" | |
| 19 | ) | |
| 20 | TARGET_INCLUDE_DIRECTORIES(${BENCH_NAME} PRIVATE | |
| 21 | ${PROJECT_SOURCE_DIR} | |
| 22 | ${PROJECT_SOURCE_DIR}/opencog/gpu) | |
| 23 | TARGET_LINK_LIBRARIES(${BENCH_NAME} gpu-kernels) | |
| 24 | ENDFOREACH() | |
| 25 | ENDIF (HAVE_CUDA) | |
| 26 | ||
| 27 | IF (HAVE_OPENCL) | |
| 28 | ADD_EXECUTABLE(bench-corpus-feeder bench-corpus-feeder.c) | |
| 29 | TARGET_LINK_LIBRARIES(bench-corpus-feeder OpenCL::OpenCL) | |
| 30 | TARGET_INCLUDE_DIRECTORIES(bench-corpus-feeder PRIVATE | |
| 31 | ${PROJECT_SOURCE_DIR} | |
| 32 | ${PROJECT_SOURCE_DIR}/opencog/gpu) | |
| 33 | ENDIF (HAVE_OPENCL) |
| … | … | @@ -1,527 +0,0 @@ |
| 1 | /* bench-corpus-ck.cu — Real corpus validation for COMPARTMENT kernel | |
| 2 | * | |
| 3 | * Feeds actual Gutenberg text through the CUDA compartment kernel | |
| 4 | * (1 SM = 1 compartment) and validates: | |
| 5 | * - Classes are formed (non-trivial clustering) | |
| 6 | * - MI values are reasonable (positive, finite) | |
| 7 | * - Convergence behavior matches expectations | |
| 8 | * - Shows actual word clusters from real English text | |
| 9 | * | |
| 10 | * Input: Binary corpus file from bench-corpus-feeder | |
| 11 | * | |
| 12 | * Build: | |
| 13 | * gcc -O2 -o bench-corpus-feeder bench-corpus-feeder.c -lm | |
| 14 | * ./bench-corpus-feeder /path/to/corpus/*.txt > corpus.bin | |
| 15 | * nvcc -O2 -arch=sm_75 -rdc=true -o bench-corpus-ck \ | |
| 16 | * bench-corpus-ck.cu gpu-compartment-kernel.cu -lcudadevrt -lm | |
| 17 | * ./bench-corpus-ck corpus.bin | |
| 18 | */ | |
| 19 | ||
| 20 | #include <cstdint> | |
| 21 | #include <cstdio> | |
| 22 | #include <cstdlib> | |
| 23 | #include <cstring> | |
| 24 | #include <cmath> | |
| 25 | #include <ctime> | |
| 26 | #include <unistd.h> | |
| 27 | #include <cuda_runtime.h> | |
| 28 | #include "gpu-learning-types.h" | |
| 29 | ||
| 30 | /* ─── Forward declarations (compartment kernel API) ─── */ | |
| 31 | ||
| 32 | extern "C" { | |
| 33 | LearningState* ck_init(uint32_t num_words); | |
| 34 | CompartmentState* ck_init_compartments(uint32_t num_compartments, | |
| 35 | uint32_t num_words); | |
| 36 | SentenceRing* ck_init_ring(); | |
| 37 | void ck_feed_sentence(SentenceRing* ring, | |
| 38 | uint32_t* words, uint32_t length); | |
| 39 | int ck_launch(LearningState*, CompartmentState*, | |
| 40 | SentenceRing*, int* done, int* pause, | |
| 41 | uint32_t* iter, uint32_t* pairs, | |
| 42 | uint32_t* classes, double* entropy); | |
| 43 | void ck_wait(); | |
| 44 | void ck_read_classes(LearningState*, uint32_t* out, uint32_t n); | |
| 45 | void ck_shutdown(LearningState*, CompartmentState*, | |
| 46 | SentenceRing*); | |
| 47 | int ck_get_num_sms(); | |
| 48 | void ck_memory_report(int num_compartments); | |
| 49 | } | |
| 50 | ||
| 51 | /* ─── Helpers ─── */ | |
| 52 | ||
| 53 | static double time_ms() { | |
| 54 | struct timespec ts; | |
| 55 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 56 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6; | |
| 57 | } | |
| 58 | ||
| 59 | /* ─── Corpus loader ─── */ | |
| 60 | ||
| 61 | typedef struct { | |
| 62 | uint32_t* words; | |
| 63 | uint32_t length; | |
| 64 | } Sentence; | |
| 65 | ||
| 66 | typedef struct { | |
| 67 | Sentence* sentences; | |
| 68 | uint32_t num_sentences; | |
| 69 | uint32_t num_words; | |
| 70 | } Corpus; | |
| 71 | ||
| 72 | static Corpus load_corpus(const char* filename) { | |
| 73 | Corpus c; | |
| 74 | memset(&c, 0, sizeof(c)); | |
| 75 | ||
| 76 | FILE* f = fopen(filename, "rb"); | |
| 77 | if (!f) { | |
| 78 | printf("ERROR: Cannot open %s\n", filename); | |
| 79 | return c; | |
| 80 | } | |
| 81 | ||
| 82 | fread(&c.num_sentences, sizeof(uint32_t), 1, f); | |
| 83 | fread(&c.num_words, sizeof(uint32_t), 1, f); | |
| 84 | ||
| 85 | c.sentences = (Sentence*)malloc(c.num_sentences * sizeof(Sentence)); | |
| 86 | ||
| 87 | for (uint32_t i = 0; i < c.num_sentences; i++) { | |
| 88 | fread(&c.sentences[i].length, sizeof(uint32_t), 1, f); | |
| 89 | c.sentences[i].words = | |
| 90 | (uint32_t*)malloc(c.sentences[i].length * sizeof(uint32_t)); | |
| 91 | fread(c.sentences[i].words, sizeof(uint32_t), | |
| 92 | c.sentences[i].length, f); | |
| 93 | } | |
| 94 | ||
| 95 | fclose(f); | |
| 96 | return c; | |
| 97 | } | |
| 98 | ||
| 99 | static void free_corpus(Corpus* c) { | |
| 100 | for (uint32_t i = 0; i < c->num_sentences; i++) | |
| 101 | free(c->sentences[i].words); | |
| 102 | free(c->sentences); | |
| 103 | } | |
| 104 | ||
| 105 | /* ─── Vocabulary loader (for readable output) ─── */ | |
| 106 | ||
| 107 | typedef struct { | |
| 108 | char word[64]; | |
| 109 | uint32_t id; | |
| 110 | } VocabEntry; | |
| 111 | ||
| 112 | static VocabEntry* vocab_table = NULL; | |
| 113 | static uint32_t vocab_count = 0; | |
| 114 | ||
| 115 | static void load_vocab(const char* filename) { | |
| 116 | FILE* f = fopen(filename, "r"); | |
| 117 | if (!f) return; | |
| 118 | ||
| 119 | vocab_table = (VocabEntry*)malloc(LL_MAX_WORDS * sizeof(VocabEntry)); | |
| 120 | vocab_count = 0; | |
| 121 | ||
| 122 | while (fscanf(f, "%u\t%63s", &vocab_table[vocab_count].id, | |
| 123 | vocab_table[vocab_count].word) == 2) { | |
| 124 | vocab_count++; | |
| 125 | if (vocab_count >= LL_MAX_WORDS) break; | |
| 126 | } | |
| 127 | fclose(f); | |
| 128 | } | |
| 129 | ||
| 130 | static const char* word_name(uint32_t id) { | |
| 131 | if (!vocab_table) return "?"; | |
| 132 | for (uint32_t i = 0; i < vocab_count; i++) { | |
| 133 | if (vocab_table[i].id == id) return vocab_table[i].word; | |
| 134 | } | |
| 135 | return "?"; | |
| 136 | } | |
| 137 | ||
| 138 | /* ═══════════════════════════════════════════════════════════════ | |
| 139 | * MAIN — REAL CORPUS VALIDATION | |
| 140 | * ═══════════════════════════════════════════════════════════════ */ | |
| 141 | ||
| 142 | int main(int argc, char** argv) { | |
| 143 | const char* corpus_file = "corpus.bin"; | |
| 144 | if (argc > 1) corpus_file = argv[1]; | |
| 145 | ||
| 146 | printf("Compartment Kernel — Real Corpus Validation\n"); | |
| 147 | printf("============================================\n"); | |
| 148 | ||
| 149 | /* GPU info */ | |
| 150 | cudaDeviceProp prop; | |
| 151 | cudaGetDeviceProperties(&prop, 0); | |
| 152 | printf("GPU: %s (SM %d.%d, %d SMs, %d MB)\n", | |
| 153 | prop.name, prop.major, prop.minor, | |
| 154 | prop.multiProcessorCount, | |
| 155 | (int)(prop.totalGlobalMem / (1024*1024))); | |
| 156 | ||
| 157 | int num_sms = ck_get_num_sms(); | |
| 158 | printf("Compartments: %d (1 per SM)\n\n", num_sms); | |
| 159 | ||
| 160 | /* Load corpus */ | |
| 161 | Corpus corpus = load_corpus(corpus_file); | |
| 162 | if (corpus.num_sentences == 0) return 1; | |
| 163 | ||
| 164 | uint32_t num_words = corpus.num_words; | |
| 165 | if (num_words > LL_MAX_WORDS) { | |
| 166 | printf("Vocabulary %u > LL_MAX_WORDS %d, capping\n", | |
| 167 | num_words, LL_MAX_WORDS); | |
| 168 | num_words = LL_MAX_WORDS; | |
| 169 | } | |
| 170 | ||
| 171 | /* Corpus stats */ | |
| 172 | uint32_t total_sents = corpus.num_sentences; | |
| 173 | double avg_len = 0; | |
| 174 | uint32_t min_len = UINT32_MAX, max_len = 0; | |
| 175 | uint32_t usable = 0; | |
| 176 | ||
| 177 | for (uint32_t i = 0; i < total_sents; i++) { | |
| 178 | /* Count usable words in this sentence */ | |
| 179 | uint32_t len = 0; | |
| 180 | for (uint32_t w = 0; w < corpus.sentences[i].length; w++) { | |
| 181 | if (corpus.sentences[i].words[w] < num_words) len++; | |
| 182 | } | |
| 183 | if (len >= 2) { | |
| 184 | usable++; | |
| 185 | avg_len += len; | |
| 186 | if (len < min_len) min_len = len; | |
| 187 | if (len > max_len) max_len = len; | |
| 188 | } | |
| 189 | } | |
| 190 | avg_len /= (usable > 0 ? usable : 1); | |
| 191 | printf("Corpus: %u total sentences, %u usable (len>=2)\n", | |
| 192 | total_sents, usable); | |
| 193 | printf("Vocabulary: %u words\n", num_words); | |
| 194 | printf("Sentence length: avg=%.1f min=%u max=%u\n\n", | |
| 195 | avg_len, min_len, max_len > LL_MAX_SENTENCE_LEN ? | |
| 196 | LL_MAX_SENTENCE_LEN : max_len); | |
| 197 | ||
| 198 | /* Load vocabulary for readable output */ | |
| 199 | load_vocab("corpus-vocab.txt"); | |
| 200 | if (vocab_table) { | |
| 201 | printf("Vocabulary loaded (%u entries)\n\n", vocab_count); | |
| 202 | } | |
| 203 | ||
| 204 | /* ─── Init GPU ─── */ | |
| 205 | ||
| 206 | LearningState* state = ck_init(num_words); | |
| 207 | CompartmentState* comps = ck_init_compartments(num_sms, num_words); | |
| 208 | SentenceRing* ring = ck_init_ring(); | |
| 209 | ||
| 210 | ck_memory_report(num_sms); | |
| 211 | printf("\n"); | |
| 212 | ||
| 213 | int* done_flag; | |
| 214 | int* pause_flag; | |
| 215 | uint32_t* stats_iter; | |
| 216 | uint32_t* stats_pairs; | |
| 217 | uint32_t* stats_classes; | |
| 218 | double* stats_entropy; | |
| 219 | ||
| 220 | cudaMallocManaged(&done_flag, sizeof(int)); | |
| 221 | cudaMallocManaged(&pause_flag, sizeof(int)); | |
| 222 | cudaMallocManaged(&stats_iter, sizeof(uint32_t)); | |
| 223 | cudaMallocManaged(&stats_pairs, sizeof(uint32_t)); | |
| 224 | cudaMallocManaged(&stats_classes, sizeof(uint32_t)); | |
| 225 | cudaMallocManaged(&stats_entropy, sizeof(double)); | |
| 226 | ||
| 227 | *done_flag = 0; | |
| 228 | *pause_flag = 0; | |
| 229 | *stats_iter = 0; | |
| 230 | *stats_pairs = 0; | |
| 231 | *stats_classes = 0; | |
| 232 | *stats_entropy = 0.0; | |
| 233 | ||
| 234 | /* ─── Launch kernel ─── */ | |
| 235 | ||
| 236 | printf("Launching compartment kernel (%d SMs)...\n", num_sms); | |
| 237 | double t_start = time_ms(); | |
| 238 | ||
| 239 | int err = ck_launch(state, comps, ring, done_flag, pause_flag, | |
| 240 | stats_iter, stats_pairs, stats_classes, | |
| 241 | stats_entropy); | |
| 242 | if (err != 0) { | |
| 243 | printf("FAILED to launch kernel\n"); | |
| 244 | ck_shutdown(state, comps, ring); | |
| 245 | free_corpus(&corpus); | |
| 246 | return 1; | |
| 247 | } | |
| 248 | ||
| 249 | /* ─── Feed with backpressure ─── */ | |
| 250 | ||
| 251 | printf("Feeding %u sentences (ring=%d slots)...\n", | |
| 252 | usable, LL_RING_SIZE); | |
| 253 | double t_feed_start = time_ms(); | |
| 254 | ||
| 255 | uint32_t fed = 0; | |
| 256 | uint32_t skipped = 0; | |
| 257 | uint32_t stalls = 0; | |
| 258 | ||
| 259 | for (uint32_t s = 0; s < total_sents && !(*done_flag); s++) { | |
| 260 | /* Filter words beyond vocabulary */ | |
| 261 | uint32_t filtered[LL_MAX_SENTENCE_LEN]; | |
| 262 | uint32_t flen = 0; | |
| 263 | for (uint32_t w = 0; | |
| 264 | w < corpus.sentences[s].length && flen < LL_MAX_SENTENCE_LEN; | |
| 265 | w++) { | |
| 266 | if (corpus.sentences[s].words[w] < num_words) { | |
| 267 | filtered[flen++] = corpus.sentences[s].words[w]; | |
| 268 | } | |
| 269 | } | |
| 270 | if (flen < 2) { skipped++; continue; } | |
| 271 | ||
| 272 | /* Backpressure: wait if ring > 75% full */ | |
| 273 | while (ring->write_idx - ring->read_idx > | |
| 274 | (LL_RING_SIZE * 3 / 4)) { | |
| 275 | usleep(100); /* 0.1ms */ | |
| 276 | stalls++; | |
| 277 | if (*done_flag) break; | |
| 278 | } | |
| 279 | ||
| 280 | ck_feed_sentence(ring, filtered, flen); | |
| 281 | fed++; | |
| 282 | ||
| 283 | /* Progress every 10K sentences */ | |
| 284 | if (fed % 10000 == 0) { | |
| 285 | printf(" fed %u/%u, iter=%u pairs=%u classes=%u " | |
| 286 | "entropy=%.4f stalls=%u\n", | |
| 287 | fed, usable, *stats_iter, *stats_pairs, | |
| 288 | *stats_classes, *stats_entropy, stalls); | |
| 289 | } | |
| 290 | } | |
| 291 | ||
| 292 | double t_feed_end = time_ms(); | |
| 293 | double feed_time = t_feed_end - t_feed_start; | |
| 294 | printf("Feeding done: %u fed, %u skipped, %u stalls, %.1f ms " | |
| 295 | "(%.0f sents/sec)\n", | |
| 296 | fed, skipped, stalls, feed_time, | |
| 297 | fed * 1000.0 / (feed_time > 0 ? feed_time : 1)); | |
| 298 | ||
| 299 | /* ─── Wait for convergence ─── */ | |
| 300 | ||
| 301 | printf("\nWaiting for convergence (timeout 120s)...\n"); | |
| 302 | double timeout = 120000.0; | |
| 303 | uint32_t last_iter = 0; | |
| 304 | double last_report = time_ms(); | |
| 305 | ||
| 306 | while (!(*done_flag)) { | |
| 307 | usleep(100000); /* 100ms poll */ | |
| 308 | ||
| 309 | if (time_ms() - t_start > timeout) { | |
| 310 | printf(" TIMEOUT after %.0fs\n", timeout / 1000.0); | |
| 311 | break; | |
| 312 | } | |
| 313 | ||
| 314 | if (time_ms() - last_report > 5000.0 && | |
| 315 | *stats_iter > last_iter) { | |
| 316 | printf(" iter=%u pairs=%u classes=%u entropy=%.4f\n", | |
| 317 | *stats_iter, *stats_pairs, *stats_classes, | |
| 318 | *stats_entropy); | |
| 319 | last_iter = *stats_iter; | |
| 320 | last_report = time_ms(); | |
| 321 | } | |
| 322 | } | |
| 323 | ||
| 324 | ck_wait(); | |
| 325 | double t_end = time_ms(); | |
| 326 | double total_time = t_end - t_start; | |
| 327 | ||
| 328 | /* ═══════════════════════════════════════════════════════════════ | |
| 329 | * RESULTS | |
| 330 | * ═══════════════════════════════════════════════════════════════ */ | |
| 331 | ||
| 332 | printf("\n=== Results ===\n"); | |
| 333 | printf("Total time: %.2f seconds\n", total_time / 1000.0); | |
| 334 | printf("Sentences fed: %u\n", fed); | |
| 335 | printf("Iterations: %u\n", *stats_iter); | |
| 336 | printf("Pairs discovered: %u\n", *stats_pairs); | |
| 337 | printf("Classes formed: %u\n", *stats_classes); | |
| 338 | printf("Final entropy: %.4f\n", *stats_entropy); | |
| 339 | printf("Pipeline: %.1f sents/sec\n", | |
| 340 | fed * 1000.0 / total_time); | |
| 341 | printf("Converged: %s\n", | |
| 342 | *done_flag ? "YES" : "NO (timeout)"); | |
| 343 | ||
| 344 | /* ─── MI analysis ─── */ | |
| 345 | ||
| 346 | printf("\n=== MI Analysis ===\n"); | |
| 347 | cudaDeviceSynchronize(); | |
| 348 | ||
| 349 | uint32_t actual_pairs = state->pair_count_u32; | |
| 350 | uint32_t mi_pos = 0, mi_neg = 0, mi_nan = 0, mi_inf = 0; | |
| 351 | double mi_sum = 0, mi_max = -1e30; | |
| 352 | uint32_t mi_max_idx = 0; | |
| 353 | ||
| 354 | for (uint32_t i = 0; i < actual_pairs && i < LL_MAX_PAIRS; i++) { | |
| 355 | double mi = state->pair_mi[i]; | |
| 356 | if (isnan(mi)) mi_nan++; | |
| 357 | else if (isinf(mi)) mi_inf++; | |
| 358 | else if (mi > 0.0) { | |
| 359 | mi_pos++; | |
| 360 | mi_sum += mi; | |
| 361 | if (mi > mi_max) { mi_max = mi; mi_max_idx = i; } | |
| 362 | } else mi_neg++; | |
| 363 | } | |
| 364 | ||
| 365 | printf("Total pairs: %u\n", actual_pairs); | |
| 366 | printf("MI positive: %u, negative: %u, nan: %u, inf: %u\n", | |
| 367 | mi_pos, mi_neg, mi_nan, mi_inf); | |
| 368 | if (mi_pos > 0) { | |
| 369 | printf("Mean positive MI: %.4f\n", mi_sum / mi_pos); | |
| 370 | uint32_t wa = state->pair_word_a[mi_max_idx]; | |
| 371 | uint32_t wb = state->pair_word_b[mi_max_idx]; | |
| 372 | printf("Highest MI pair: %s — %s (MI=%.4f)\n", | |
| 373 | word_name(wa), word_name(wb), mi_max); | |
| 374 | } | |
| 375 | ||
| 376 | /* Top 20 MI pairs */ | |
| 377 | printf("\nTop 20 MI pairs:\n"); | |
| 378 | double* mi_copy = (double*)malloc(actual_pairs * sizeof(double)); | |
| 379 | uint32_t* mi_order = (uint32_t*)malloc(actual_pairs * sizeof(uint32_t)); | |
| 380 | for (uint32_t i = 0; i < actual_pairs; i++) { | |
| 381 | mi_copy[i] = state->pair_mi[i]; | |
| 382 | mi_order[i] = i; | |
| 383 | } | |
| 384 | /* Simple selection of top 20 */ | |
| 385 | for (int rank = 0; rank < 20 && rank < (int)actual_pairs; rank++) { | |
| 386 | uint32_t best = rank; | |
| 387 | for (uint32_t j = rank + 1; j < actual_pairs; j++) { | |
| 388 | if (mi_copy[j] > mi_copy[best]) best = j; | |
| 389 | } | |
| 390 | /* Swap */ | |
| 391 | double tmp_d = mi_copy[rank]; mi_copy[rank] = mi_copy[best]; mi_copy[best] = tmp_d; | |
| 392 | uint32_t tmp_u = mi_order[rank]; mi_order[rank] = mi_order[best]; mi_order[best] = tmp_u; | |
| 393 | ||
| 394 | uint32_t idx = mi_order[rank]; | |
| 395 | uint32_t wa = state->pair_word_a[idx]; | |
| 396 | uint32_t wb = state->pair_word_b[idx]; | |
| 397 | printf(" %2d. %-15s — %-15s MI=%.4f count=%.0f\n", | |
| 398 | rank + 1, word_name(wa), word_name(wb), | |
| 399 | state->pair_mi[idx], state->pair_count[idx]); | |
| 400 | } | |
| 401 | free(mi_copy); | |
| 402 | free(mi_order); | |
| 403 | ||
| 404 | /* ─── Class analysis ─── */ | |
| 405 | ||
| 406 | printf("\n=== Class Analysis ===\n"); | |
| 407 | uint32_t* class_ids = (uint32_t*)malloc(num_words * sizeof(uint32_t)); | |
| 408 | ck_read_classes(state, class_ids, num_words); | |
| 409 | ||
| 410 | /* Count unique classes and class sizes */ | |
| 411 | uint32_t max_class = 0; | |
| 412 | for (uint32_t i = 0; i < num_words; i++) { | |
| 413 | if (class_ids[i] > max_class) max_class = class_ids[i]; | |
| 414 | } | |
| 415 | uint32_t num_classes_actual = max_class + 1; | |
| 416 | uint32_t* class_sz = (uint32_t*)calloc(num_classes_actual, sizeof(uint32_t)); | |
| 417 | for (uint32_t i = 0; i < num_words; i++) { | |
| 418 | class_sz[class_ids[i]]++; | |
| 419 | } | |
| 420 | ||
| 421 | /* Count non-singleton classes */ | |
| 422 | uint32_t singletons = 0, multi = 0; | |
| 423 | for (uint32_t c = 0; c < num_classes_actual; c++) { | |
| 424 | if (class_sz[c] == 1) singletons++; | |
| 425 | else if (class_sz[c] > 1) multi++; | |
| 426 | } | |
| 427 | printf("Classes: %u total, %u multi-word, %u singletons\n", | |
| 428 | num_classes_actual, multi, singletons); | |
| 429 | ||
| 430 | /* Show top 15 largest classes with words */ | |
| 431 | printf("\nTop 15 classes by size:\n"); | |
| 432 | for (int rank = 0; rank < 15; rank++) { | |
| 433 | uint32_t best_c = 0, best_sz = 0; | |
| 434 | for (uint32_t c = 0; c < num_classes_actual; c++) { | |
| 435 | if (class_sz[c] > best_sz) { | |
| 436 | best_sz = class_sz[c]; | |
| 437 | best_c = c; | |
| 438 | } | |
| 439 | } | |
| 440 | if (best_sz <= 1) break; | |
| 441 | ||
| 442 | printf(" Class %3u (size %3u): ", best_c, best_sz); | |
| 443 | int printed = 0; | |
| 444 | for (uint32_t w = 0; w < num_words && printed < 12; w++) { | |
| 445 | if (class_ids[w] == best_c) { | |
| 446 | printf("%s ", word_name(w)); | |
| 447 | printed++; | |
| 448 | } | |
| 449 | } | |
| 450 | if (best_sz > 12) printf("(+%u more)", best_sz - 12); | |
| 451 | printf("\n"); | |
| 452 | ||
| 453 | class_sz[best_c] = 0; | |
| 454 | } | |
| 455 | ||
| 456 | /* ─── Entropy history ─── */ | |
| 457 | ||
| 458 | printf("\nEntropy history:\n"); | |
| 459 | for (int i = 0; i < LL_ENTROPY_WINDOW; i++) { | |
| 460 | printf(" [%d] = %.6f\n", i, state->entropy_history[i]); | |
| 461 | } | |
| 462 | ||
| 463 | /* ─── Convergence details ─── */ | |
| 464 | ||
| 465 | printf("\nConvergence details:\n"); | |
| 466 | printf(" total_sentences: %u\n", state->total_sentences); | |
| 467 | printf(" cc_runs: %u\n", state->cc_runs); | |
| 468 | printf(" idle_rounds: %u\n", state->idle_rounds); | |
| 469 | printf(" iteration: %u\n", state->iteration); | |
| 470 | printf(" pair_count: %u\n", state->pair_count_u32); | |
| 471 | printf(" dirty_count: %u\n", state->dirty_count); | |
| 472 | ||
| 473 | /* ─── Validation summary ─── */ | |
| 474 | ||
| 475 | printf("\n=== Validation ===\n"); | |
| 476 | int pass = 1; | |
| 477 | ||
| 478 | if (*stats_pairs < 1000) { | |
| 479 | printf("FAIL: only %u pairs (expected >=1000 for real corpus)\n", | |
| 480 | *stats_pairs); | |
| 481 | pass = 0; | |
| 482 | } else { | |
| 483 | printf("PASS: %u pairs discovered\n", *stats_pairs); | |
| 484 | } | |
| 485 | ||
| 486 | if (mi_nan > 0 || mi_inf > 0) { | |
| 487 | printf("FAIL: %u NaN + %u Inf MI values\n", mi_nan, mi_inf); | |
| 488 | pass = 0; | |
| 489 | } else { | |
| 490 | printf("PASS: no NaN/Inf MI values\n"); | |
| 491 | } | |
| 492 | ||
| 493 | if (*stats_entropy <= 0.0) { | |
| 494 | printf("FAIL: entropy = %.4f\n", *stats_entropy); | |
| 495 | pass = 0; | |
| 496 | } else { | |
| 497 | printf("PASS: entropy = %.4f\n", *stats_entropy); | |
| 498 | } | |
| 499 | ||
| 500 | if (multi < 2) { | |
| 501 | printf("FAIL: only %u multi-word classes\n", multi); | |
| 502 | pass = 0; | |
| 503 | } else { | |
| 504 | printf("PASS: %u multi-word classes\n", multi); | |
| 505 | } | |
| 506 | ||
| 507 | if (!(*done_flag)) { | |
| 508 | printf("FAIL: did not converge (timeout)\n"); | |
| 509 | pass = 0; | |
| 510 | } else { | |
| 511 | printf("PASS: converged\n"); | |
| 512 | } | |
| 513 | ||
| 514 | printf("\n=== %s ===\n", pass ? "ALL CHECKS PASSED" : "SOME CHECKS FAILED"); | |
| 515 | ||
| 516 | /* Cleanup */ | |
| 517 | free(class_ids); | |
| 518 | free(class_sz); | |
| 519 | if (vocab_table) free(vocab_table); | |
| 520 | ck_shutdown(state, comps, ring); | |
| 521 | free_corpus(&corpus); | |
| 522 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 523 | cudaFree(stats_iter); cudaFree(stats_pairs); | |
| 524 | cudaFree(stats_classes); cudaFree(stats_entropy); | |
| 525 | ||
| 526 | return pass ? 0 : 1; | |
| 527 | } |
| … | … | @@ -1,231 +0,0 @@ |
| 1 | /* bench-corpus-feeder.c — Sentence tokenizer for corpus validation | |
| 2 | * | |
| 3 | * Reads Gutenberg text files, tokenizes into word-ID sequences, | |
| 4 | * outputs binary format for bench-corpus to consume. | |
| 5 | * | |
| 6 | * Output format (.corpus.bin): | |
| 7 | * uint32_t num_sentences | |
| 8 | * uint32_t num_words (vocabulary size) | |
| 9 | * For each sentence: | |
| 10 | * uint32_t length | |
| 11 | * uint32_t word_ids[length] | |
| 12 | * | |
| 13 | * Build: | |
| 14 | * gcc -O2 -o bench-corpus-feeder bench-corpus-feeder.c -lm | |
| 15 | * | |
| 16 | * Usage: | |
| 17 | * ./bench-corpus-feeder corpus/*.txt > corpus.bin | |
| 18 | * ./bench-corpus [corpus.bin] | |
| 19 | */ | |
| 20 | ||
| 21 | #include <stdio.h> | |
| 22 | #include <stdlib.h> | |
| 23 | #include <string.h> | |
| 24 | #include <ctype.h> | |
| 25 | #include <stdint.h> | |
| 26 | ||
| 27 | #define MAX_VOCAB 8192 | |
| 28 | #define MAX_WORD_LEN 64 | |
| 29 | #define MAX_SENT_LEN 64 | |
| 30 | #define MAX_SENTENCES 100000 | |
| 31 | #define MAX_LINE_LEN 4096 | |
| 32 | ||
| 33 | /* Simple hash table for word → ID mapping */ | |
| 34 | ||
| 35 | typedef struct { | |
| 36 | char word[MAX_WORD_LEN]; | |
| 37 | uint32_t id; | |
| 38 | } VocabEntry; | |
| 39 | ||
| 40 | static VocabEntry vocab[MAX_VOCAB]; | |
| 41 | static uint32_t vocab_size = 0; | |
| 42 | ||
| 43 | /* djb2 hash */ | |
| 44 | static uint32_t hash_word(const char* s) { | |
| 45 | uint32_t h = 5381; | |
| 46 | while (*s) { | |
| 47 | h = ((h << 5) + h) + (unsigned char)*s; | |
| 48 | s++; | |
| 49 | } | |
| 50 | return h; | |
| 51 | } | |
| 52 | ||
| 53 | static uint32_t get_or_create_word_id(const char* word) { | |
| 54 | uint32_t h = hash_word(word) % MAX_VOCAB; | |
| 55 | int probe = 0; | |
| 56 | ||
| 57 | while (probe < MAX_VOCAB) { | |
| 58 | uint32_t idx = (h + probe) % MAX_VOCAB; | |
| 59 | if (vocab[idx].word[0] == '\0') { | |
| 60 | /* Empty slot — new word */ | |
| 61 | if (vocab_size >= MAX_VOCAB - 100) return UINT32_MAX; /* near-full */ | |
| 62 | strncpy(vocab[idx].word, word, MAX_WORD_LEN - 1); | |
| 63 | vocab[idx].id = vocab_size; | |
| 64 | vocab_size++; | |
| 65 | return vocab[idx].id; | |
| 66 | } | |
| 67 | if (strcmp(vocab[idx].word, word) == 0) { | |
| 68 | return vocab[idx].id; | |
| 69 | } | |
| 70 | probe++; | |
| 71 | } | |
| 72 | return UINT32_MAX; | |
| 73 | } | |
| 74 | ||
| 75 | /* Sentence storage */ | |
| 76 | ||
| 77 | typedef struct { | |
| 78 | uint32_t words[MAX_SENT_LEN]; | |
| 79 | uint32_t length; | |
| 80 | } Sentence; | |
| 81 | ||
| 82 | static Sentence sentences[MAX_SENTENCES]; | |
| 83 | static uint32_t num_sentences = 0; | |
| 84 | ||
| 85 | /* Detect Gutenberg header/footer lines */ | |
| 86 | static int is_gutenberg_meta(const char* line) { | |
| 87 | if (strstr(line, "*** START OF") != NULL) return 1; | |
| 88 | if (strstr(line, "*** END OF") != NULL) return 1; | |
| 89 | if (strstr(line, "Project Gutenberg") != NULL) return 1; | |
| 90 | if (strstr(line, "This eBook") != NULL) return 1; | |
| 91 | return 0; | |
| 92 | } | |
| 93 | ||
| 94 | /* Tokenize a line into words, splitting on spaces and punctuation */ | |
| 95 | static void tokenize_line(const char* line) { | |
| 96 | char word[MAX_WORD_LEN]; | |
| 97 | int wlen = 0; | |
| 98 | ||
| 99 | Sentence* sent = NULL; | |
| 100 | ||
| 101 | for (const char* p = line; ; p++) { | |
| 102 | char c = *p; | |
| 103 | ||
| 104 | if (isalpha(c) || c == '\'') { | |
| 105 | /* Build word */ | |
| 106 | if (wlen < MAX_WORD_LEN - 1) { | |
| 107 | word[wlen++] = tolower(c); | |
| 108 | } | |
| 109 | } else { | |
| 110 | if (wlen > 0) { | |
| 111 | word[wlen] = '\0'; | |
| 112 | wlen = 0; | |
| 113 | ||
| 114 | uint32_t id = get_or_create_word_id(word); | |
| 115 | if (id == UINT32_MAX) continue; | |
| 116 | ||
| 117 | if (sent == NULL) { | |
| 118 | if (num_sentences >= MAX_SENTENCES) return; | |
| 119 | sent = &sentences[num_sentences]; | |
| 120 | sent->length = 0; | |
| 121 | } | |
| 122 | ||
| 123 | if (sent->length < MAX_SENT_LEN) { | |
| 124 | sent->words[sent->length++] = id; | |
| 125 | } | |
| 126 | } | |
| 127 | ||
| 128 | /* Sentence boundary: period, !, ? */ | |
| 129 | if (c == '.' || c == '!' || c == '?') { | |
| 130 | if (sent != NULL && sent->length >= 2) { | |
| 131 | num_sentences++; | |
| 132 | } | |
| 133 | sent = NULL; | |
| 134 | } | |
| 135 | ||
| 136 | if (c == '\0') break; | |
| 137 | } | |
| 138 | } | |
| 139 | ||
| 140 | /* Line end: commit partial sentence if it has enough words */ | |
| 141 | if (sent != NULL && sent->length >= 2) { | |
| 142 | num_sentences++; | |
| 143 | } | |
| 144 | } | |
| 145 | ||
| 146 | /* Process one file */ | |
| 147 | static void process_file(const char* filename) { | |
| 148 | FILE* f = fopen(filename, "r"); | |
| 149 | if (!f) { | |
| 150 | fprintf(stderr, "Cannot open: %s\n", filename); | |
| 151 | return; | |
| 152 | } | |
| 153 | ||
| 154 | char line[MAX_LINE_LEN]; | |
| 155 | int past_header = 0; | |
| 156 | ||
| 157 | while (fgets(line, sizeof(line), f)) { | |
| 158 | /* Skip Gutenberg header */ | |
| 159 | if (!past_header) { | |
| 160 | if (strstr(line, "*** START OF") != NULL) { | |
| 161 | past_header = 1; | |
| 162 | } | |
| 163 | continue; | |
| 164 | } | |
| 165 | ||
| 166 | /* Stop at Gutenberg footer */ | |
| 167 | if (strstr(line, "*** END OF") != NULL) break; | |
| 168 | ||
| 169 | /* Skip empty lines */ | |
| 170 | int has_alpha = 0; | |
| 171 | for (const char* p = line; *p; p++) { | |
| 172 | if (isalpha(*p)) { has_alpha = 1; break; } | |
| 173 | } | |
| 174 | if (!has_alpha) continue; | |
| 175 | ||
| 176 | /* Skip chapter headings (all caps) */ | |
| 177 | int all_upper = 1; | |
| 178 | for (const char* p = line; *p && *p != '\n'; p++) { | |
| 179 | if (isalpha(*p) && islower(*p)) { all_upper = 0; break; } | |
| 180 | } | |
| 181 | if (all_upper && strlen(line) > 5) continue; | |
| 182 | ||
| 183 | tokenize_line(line); | |
| 184 | } | |
| 185 | ||
| 186 | fclose(f); | |
| 187 | fprintf(stderr, "Processed %s: %u sentences, %u words in vocab\n", | |
| 188 | filename, num_sentences, vocab_size); | |
| 189 | } | |
| 190 | ||
| 191 | int main(int argc, char** argv) { | |
| 192 | if (argc < 2) { | |
| 193 | fprintf(stderr, "Usage: %s <file1.txt> [file2.txt ...]\n", argv[0]); | |
| 194 | return 1; | |
| 195 | } | |
| 196 | ||
| 197 | memset(vocab, 0, sizeof(vocab)); | |
| 198 | ||
| 199 | /* Process all input files */ | |
| 200 | for (int i = 1; i < argc; i++) { | |
| 201 | process_file(argv[i]); | |
| 202 | } | |
| 203 | ||
| 204 | fprintf(stderr, "Total: %u sentences, %u vocabulary words\n", | |
| 205 | num_sentences, vocab_size); | |
| 206 | ||
| 207 | /* Write binary output to stdout */ | |
| 208 | fwrite(&num_sentences, sizeof(uint32_t), 1, stdout); | |
| 209 | fwrite(&vocab_size, sizeof(uint32_t), 1, stdout); | |
| 210 | ||
| 211 | for (uint32_t i = 0; i < num_sentences; i++) { | |
| 212 | fwrite(&sentences[i].length, sizeof(uint32_t), 1, stdout); | |
| 213 | fwrite(sentences[i].words, sizeof(uint32_t), sentences[i].length, stdout); | |
| 214 | } | |
| 215 | ||
| 216 | /* Also write vocabulary (word → ID mapping) for debugging */ | |
| 217 | char vocab_fname[256]; | |
| 218 | snprintf(vocab_fname, sizeof(vocab_fname), "corpus-vocab.txt"); | |
| 219 | FILE* vf = fopen(vocab_fname, "w"); | |
| 220 | if (vf) { | |
| 221 | for (uint32_t i = 0; i < MAX_VOCAB; i++) { | |
| 222 | if (vocab[i].word[0] != '\0') { | |
| 223 | fprintf(vf, "%u\t%s\n", vocab[i].id, vocab[i].word); | |
| 224 | } | |
| 225 | } | |
| 226 | fclose(vf); | |
| 227 | fprintf(stderr, "Vocabulary written to %s\n", vocab_fname); | |
| 228 | } | |
| 229 | ||
| 230 | return 0; | |
| 231 | } |
| … | … | @@ -1,432 +0,0 @@ |
| 1 | /* bench-corpus.cu — Real corpus validation (Experiment 4) | |
| 2 | * | |
| 3 | * Feeds actual Gutenberg text through the CUDA persistent kernel | |
| 4 | * and validates correctness: | |
| 5 | * - Classes are formed (non-trivial clustering) | |
| 6 | * - High-frequency words cluster together | |
| 7 | * - MI values are reasonable (positive, finite) | |
| 8 | * - Convergence behavior matches expectations | |
| 9 | * | |
| 10 | * Input: Binary corpus file from bench-corpus-feeder | |
| 11 | * | |
| 12 | * Build: | |
| 13 | * nvcc -O2 -arch=sm_75 -rdc=true -o bench-corpus \ | |
| 14 | * bench-corpus.cu gpu-learning-loop.cu -lcudadevrt -lm | |
| 15 | * | |
| 16 | * Usage: | |
| 17 | * ./bench-corpus-feeder corpus/*.txt > corpus.bin | |
| 18 | * ./bench-corpus corpus.bin | |
| 19 | */ | |
| 20 | ||
| 21 | #include <cstdint> | |
| 22 | #include <cstdio> | |
| 23 | #include <cstdlib> | |
| 24 | #include <cstring> | |
| 25 | #include <cmath> | |
| 26 | #include <ctime> | |
| 27 | #include <unistd.h> | |
| 28 | #include <cuda_runtime.h> | |
| 29 | #include "gpu-learning-types.h" | |
| 30 | ||
| 31 | /* ─── Forward declarations ─── */ | |
| 32 | ||
| 33 | extern "C" { | |
| 34 | LearningState* ll_init(uint32_t num_words); | |
| 35 | SentenceRing* ll_init_ring(); | |
| 36 | void ll_feed_sentence(SentenceRing* ring, uint32_t* words, uint32_t length); | |
| 37 | int ll_launch(LearningState* state, SentenceRing* ring, | |
| 38 | int* done_flag, int* pause_flag, | |
| 39 | uint32_t* stats_iteration, uint32_t* stats_pairs, | |
| 40 | uint32_t* stats_classes, double* stats_entropy); | |
| 41 | void ll_wait(); | |
| 42 | void ll_read_classes(LearningState* state, uint32_t* out, uint32_t n); | |
| 43 | void ll_shutdown(LearningState* state, SentenceRing* ring); | |
| 44 | } | |
| 45 | ||
| 46 | /* ─── Helpers ─── */ | |
| 47 | ||
| 48 | static double time_ms() { | |
| 49 | struct timespec ts; | |
| 50 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 51 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6; | |
| 52 | } | |
| 53 | ||
| 54 | /* ─── Corpus data ─── */ | |
| 55 | ||
| 56 | typedef struct { | |
| 57 | uint32_t* words; | |
| 58 | uint32_t length; | |
| 59 | } Sentence; | |
| 60 | ||
| 61 | typedef struct { | |
| 62 | Sentence* sentences; | |
| 63 | uint32_t num_sentences; | |
| 64 | uint32_t num_words; /* vocabulary size */ | |
| 65 | } Corpus; | |
| 66 | ||
| 67 | static Corpus load_corpus(const char* filename) { | |
| 68 | Corpus c; | |
| 69 | memset(&c, 0, sizeof(c)); | |
| 70 | ||
| 71 | FILE* f = fopen(filename, "rb"); | |
| 72 | if (!f) { | |
| 73 | printf("ERROR: Cannot open %s\n", filename); | |
| 74 | return c; | |
| 75 | } | |
| 76 | ||
| 77 | fread(&c.num_sentences, sizeof(uint32_t), 1, f); | |
| 78 | fread(&c.num_words, sizeof(uint32_t), 1, f); | |
| 79 | ||
| 80 | printf("Loading corpus: %u sentences, %u vocabulary words\n", | |
| 81 | c.num_sentences, c.num_words); | |
| 82 | ||
| 83 | c.sentences = (Sentence*)malloc(c.num_sentences * sizeof(Sentence)); | |
| 84 | ||
| 85 | for (uint32_t i = 0; i < c.num_sentences; i++) { | |
| 86 | fread(&c.sentences[i].length, sizeof(uint32_t), 1, f); | |
| 87 | c.sentences[i].words = (uint32_t*)malloc(c.sentences[i].length * sizeof(uint32_t)); | |
| 88 | fread(c.sentences[i].words, sizeof(uint32_t), c.sentences[i].length, f); | |
| 89 | } | |
| 90 | ||
| 91 | fclose(f); | |
| 92 | return c; | |
| 93 | } | |
| 94 | ||
| 95 | static void free_corpus(Corpus* c) { | |
| 96 | for (uint32_t i = 0; i < c->num_sentences; i++) { | |
| 97 | free(c->sentences[i].words); | |
| 98 | } | |
| 99 | free(c->sentences); | |
| 100 | } | |
| 101 | ||
| 102 | /* ─── Load vocabulary for printing ─── */ | |
| 103 | ||
| 104 | typedef struct { | |
| 105 | char word[64]; | |
| 106 | uint32_t id; | |
| 107 | } VocabEntry; | |
| 108 | ||
| 109 | static VocabEntry* load_vocab(const char* filename, uint32_t* count) { | |
| 110 | FILE* f = fopen(filename, "r"); | |
| 111 | if (!f) { *count = 0; return NULL; } | |
| 112 | ||
| 113 | VocabEntry* entries = (VocabEntry*)malloc(8192 * sizeof(VocabEntry)); | |
| 114 | *count = 0; | |
| 115 | ||
| 116 | while (fscanf(f, "%u\t%63s", &entries[*count].id, entries[*count].word) == 2) { | |
| 117 | (*count)++; | |
| 118 | if (*count >= 8192) break; | |
| 119 | } | |
| 120 | fclose(f); | |
| 121 | return entries; | |
| 122 | } | |
| 123 | ||
| 124 | static const char* lookup_word(VocabEntry* vocab, uint32_t vocab_count, uint32_t id) { | |
| 125 | for (uint32_t i = 0; i < vocab_count; i++) { | |
| 126 | if (vocab[i].id == id) return vocab[i].word; | |
| 127 | } | |
| 128 | return "???"; | |
| 129 | } | |
| 130 | ||
| 131 | /* ═══════════════════════════════════════════════════════════════ | |
| 132 | * CORPUS VALIDATION | |
| 133 | * ═══════════════════════════════════════════════════════════════ */ | |
| 134 | ||
| 135 | int main(int argc, char** argv) { | |
| 136 | const char* corpus_file = "corpus.bin"; | |
| 137 | if (argc > 1) corpus_file = argv[1]; | |
| 138 | ||
| 139 | printf("GPU Learning Pipeline — Real Corpus Validation\n"); | |
| 140 | printf("===============================================\n"); | |
| 141 | ||
| 142 | cudaDeviceProp prop; | |
| 143 | cudaGetDeviceProperties(&prop, 0); | |
| 144 | printf("GPU: %s (SM %d.%d, %d SMs, %d MB)\n", | |
| 145 | prop.name, prop.major, prop.minor, | |
| 146 | prop.multiProcessorCount, | |
| 147 | (int)(prop.totalGlobalMem / (1024*1024))); | |
| 148 | ||
| 149 | int supports_coop = 0; | |
| 150 | cudaDeviceGetAttribute(&supports_coop, cudaDevAttrCooperativeLaunch, 0); | |
| 151 | if (!supports_coop) { | |
| 152 | printf("ERROR: Device does not support cooperative launch\n"); | |
| 153 | return 1; | |
| 154 | } | |
| 155 | ||
| 156 | /* Load corpus */ | |
| 157 | Corpus corpus = load_corpus(corpus_file); | |
| 158 | if (corpus.num_sentences == 0) return 1; | |
| 159 | ||
| 160 | /* Cap vocabulary to LL_MAX_WORDS */ | |
| 161 | uint32_t num_words = corpus.num_words; | |
| 162 | if (num_words > LL_MAX_WORDS) { | |
| 163 | printf("WARNING: Vocabulary (%u) exceeds LL_MAX_WORDS (%d), capping\n", | |
| 164 | num_words, LL_MAX_WORDS); | |
| 165 | num_words = LL_MAX_WORDS; | |
| 166 | } | |
| 167 | ||
| 168 | /* Cap sentences to avoid timeout */ | |
| 169 | uint32_t num_sentences = corpus.num_sentences; | |
| 170 | uint32_t max_sentences = 10000; | |
| 171 | if (num_sentences > max_sentences) { | |
| 172 | printf("Capping to %u sentences (of %u) for benchmark\n", | |
| 173 | max_sentences, num_sentences); | |
| 174 | num_sentences = max_sentences; | |
| 175 | } | |
| 176 | ||
| 177 | /* Corpus stats */ | |
| 178 | double avg_len = 0; | |
| 179 | uint32_t min_len = UINT32_MAX, max_len = 0; | |
| 180 | for (uint32_t i = 0; i < num_sentences; i++) { | |
| 181 | uint32_t len = corpus.sentences[i].length; | |
| 182 | avg_len += len; | |
| 183 | if (len < min_len) min_len = len; | |
| 184 | if (len > max_len) max_len = len; | |
| 185 | } | |
| 186 | avg_len /= num_sentences; | |
| 187 | printf("Corpus stats: %u sentences, avg_len=%.1f, min=%u, max=%u\n", | |
| 188 | num_sentences, avg_len, min_len, max_len); | |
| 189 | ||
| 190 | /* ─── Init GPU pipeline ─── */ | |
| 191 | ||
| 192 | LearningState* state = ll_init(num_words); | |
| 193 | SentenceRing* ring = ll_init_ring(); | |
| 194 | ||
| 195 | int* done_flag; | |
| 196 | int* pause_flag; | |
| 197 | uint32_t* stats_iteration; | |
| 198 | uint32_t* stats_pairs; | |
| 199 | uint32_t* stats_classes; | |
| 200 | double* stats_entropy; | |
| 201 | ||
| 202 | cudaMallocManaged(&done_flag, sizeof(int)); | |
| 203 | cudaMallocManaged(&pause_flag, sizeof(int)); | |
| 204 | cudaMallocManaged(&stats_iteration, sizeof(uint32_t)); | |
| 205 | cudaMallocManaged(&stats_pairs, sizeof(uint32_t)); | |
| 206 | cudaMallocManaged(&stats_classes, sizeof(uint32_t)); | |
| 207 | cudaMallocManaged(&stats_entropy, sizeof(double)); | |
| 208 | ||
| 209 | *done_flag = 0; | |
| 210 | *pause_flag = 0; | |
| 211 | *stats_iteration = 0; | |
| 212 | *stats_pairs = 0; | |
| 213 | *stats_classes = 0; | |
| 214 | *stats_entropy = 0.0; | |
| 215 | ||
| 216 | /* Launch persistent kernel */ | |
| 217 | printf("\nLaunching persistent kernel...\n"); | |
| 218 | double t_start = time_ms(); | |
| 219 | ||
| 220 | int err = ll_launch(state, ring, done_flag, pause_flag, | |
| 221 | stats_iteration, stats_pairs, stats_classes, stats_entropy); | |
| 222 | if (err != 0) { | |
| 223 | printf("FAILED to launch kernel!\n"); | |
| 224 | ll_shutdown(state, ring); | |
| 225 | free_corpus(&corpus); | |
| 226 | return 1; | |
| 227 | } | |
| 228 | ||
| 229 | /* Feed sentences */ | |
| 230 | printf("Feeding %u sentences...\n", num_sentences); | |
| 231 | double t_feed_start = time_ms(); | |
| 232 | ||
| 233 | for (uint32_t s = 0; s < num_sentences; s++) { | |
| 234 | /* Filter out words beyond our vocabulary */ | |
| 235 | uint32_t filtered[LL_MAX_SENTENCE_LEN]; | |
| 236 | uint32_t flen = 0; | |
| 237 | for (uint32_t w = 0; w < corpus.sentences[s].length && flen < LL_MAX_SENTENCE_LEN; w++) { | |
| 238 | if (corpus.sentences[s].words[w] < num_words) { | |
| 239 | filtered[flen++] = corpus.sentences[s].words[w]; | |
| 240 | } | |
| 241 | } | |
| 242 | if (flen >= 2) { | |
| 243 | ll_feed_sentence(ring, filtered, flen); | |
| 244 | } | |
| 245 | } | |
| 246 | ||
| 247 | double t_feed_end = time_ms(); | |
| 248 | printf("Feeding complete: %.1f ms (%.0f sents/sec)\n", | |
| 249 | t_feed_end - t_feed_start, | |
| 250 | num_sentences * 1000.0 / (t_feed_end - t_feed_start)); | |
| 251 | ||
| 252 | /* Wait for convergence (timeout 120s for large corpus) */ | |
| 253 | printf("Waiting for convergence...\n"); | |
| 254 | double timeout = 120000.0; | |
| 255 | uint32_t last_iter = 0; | |
| 256 | double last_report = time_ms(); | |
| 257 | ||
| 258 | while (!(*done_flag)) { | |
| 259 | usleep(50000); /* 50ms poll */ | |
| 260 | ||
| 261 | if (time_ms() - t_start > timeout) { | |
| 262 | printf(" TIMEOUT after %.0f seconds\n", timeout / 1000.0); | |
| 263 | *done_flag = 1; | |
| 264 | break; | |
| 265 | } | |
| 266 | ||
| 267 | /* Progress every 5 seconds */ | |
| 268 | if (time_ms() - last_report > 5000.0 && *stats_iteration > last_iter) { | |
| 269 | printf(" iter=%u pairs=%u classes=%u entropy=%.4f\n", | |
| 270 | *stats_iteration, *stats_pairs, *stats_classes, *stats_entropy); | |
| 271 | last_iter = *stats_iteration; | |
| 272 | last_report = time_ms(); | |
| 273 | } | |
| 274 | } | |
| 275 | ||
| 276 | ll_wait(); | |
| 277 | double t_end = time_ms(); | |
| 278 | double total_time = t_end - t_start; | |
| 279 | ||
| 280 | printf("\n=== Results ===\n"); | |
| 281 | printf("Total time: %.2f seconds\n", total_time / 1000.0); | |
| 282 | printf("Iterations: %u\n", *stats_iteration); | |
| 283 | printf("Pairs discovered: %u\n", *stats_pairs); | |
| 284 | printf("Classes formed: %u\n", *stats_classes); | |
| 285 | printf("Final entropy: %.4f\n", *stats_entropy); | |
| 286 | printf("Throughput: %.1f sentences/sec\n", | |
| 287 | num_sentences * 1000.0 / total_time); | |
| 288 | printf("Converged: %s\n", | |
| 289 | *stats_iteration < LL_MAX_ITERATIONS ? "YES" : "NO (hit max iterations)"); | |
| 290 | ||
| 291 | /* ─── Validation checks ─── */ | |
| 292 | ||
| 293 | printf("\n=== Validation ===\n"); | |
| 294 | int pass = 1; | |
| 295 | ||
| 296 | /* Check 1: Non-trivial clustering */ | |
| 297 | if (*stats_classes < 2) { | |
| 298 | printf("FAIL: Only %u classes (expected >= 2)\n", *stats_classes); | |
| 299 | pass = 0; | |
| 300 | } else if (*stats_classes >= num_words) { | |
| 301 | printf("FAIL: %u classes = %u words (no clustering)\n", *stats_classes, num_words); | |
| 302 | pass = 0; | |
| 303 | } else { | |
| 304 | printf("PASS: %u classes formed (non-trivial clustering)\n", *stats_classes); | |
| 305 | } | |
| 306 | ||
| 307 | /* Check 2: Reasonable number of pairs */ | |
| 308 | if (*stats_pairs < 100) { | |
| 309 | printf("FAIL: Only %u pairs (expected >= 100)\n", *stats_pairs); | |
| 310 | pass = 0; | |
| 311 | } else { | |
| 312 | printf("PASS: %u pairs discovered\n", *stats_pairs); | |
| 313 | } | |
| 314 | ||
| 315 | /* Check 3: Positive entropy */ | |
| 316 | if (*stats_entropy <= 0.0) { | |
| 317 | printf("FAIL: Entropy = %.4f (expected > 0)\n", *stats_entropy); | |
| 318 | pass = 0; | |
| 319 | } else { | |
| 320 | printf("PASS: Entropy = %.4f (positive)\n", *stats_entropy); | |
| 321 | } | |
| 322 | ||
| 323 | /* Check 4: MI values are reasonable */ | |
| 324 | cudaDeviceSynchronize(); | |
| 325 | uint32_t mi_positive = 0, mi_negative = 0, mi_nan = 0, mi_inf = 0; | |
| 326 | double mi_sum = 0.0; | |
| 327 | uint32_t actual_pairs = state->pair_count_u32; | |
| 328 | ||
| 329 | for (uint32_t i = 0; i < actual_pairs && i < LL_MAX_PAIRS; i++) { | |
| 330 | double mi = state->pair_mi[i]; | |
| 331 | if (isnan(mi)) mi_nan++; | |
| 332 | else if (isinf(mi)) mi_inf++; | |
| 333 | else if (mi > 0.0) { mi_positive++; mi_sum += mi; } | |
| 334 | else mi_negative++; | |
| 335 | } | |
| 336 | ||
| 337 | printf("MI distribution: %u positive, %u negative, %u NaN, %u Inf\n", | |
| 338 | mi_positive, mi_negative, mi_nan, mi_inf); | |
| 339 | if (mi_nan > 0 || mi_inf > 0) { | |
| 340 | printf("FAIL: %u NaN + %u Inf MI values\n", mi_nan, mi_inf); | |
| 341 | pass = 0; | |
| 342 | } else { | |
| 343 | printf("PASS: No NaN/Inf MI values\n"); | |
| 344 | } | |
| 345 | if (mi_positive > 0) { | |
| 346 | printf("PASS: Mean positive MI = %.4f\n", mi_sum / mi_positive); | |
| 347 | } | |
| 348 | ||
| 349 | /* Check 5: Read class assignments and analyze */ | |
| 350 | uint32_t* classes = (uint32_t*)malloc(num_words * sizeof(uint32_t)); | |
| 351 | ll_read_classes(state, classes, num_words); | |
| 352 | ||
| 353 | /* Count class sizes */ | |
| 354 | uint32_t* class_sizes = (uint32_t*)calloc(*stats_classes + 1, sizeof(uint32_t)); | |
| 355 | for (uint32_t i = 0; i < num_words; i++) { | |
| 356 | if (classes[i] < *stats_classes + 1) { | |
| 357 | class_sizes[classes[i]]++; | |
| 358 | } | |
| 359 | } | |
| 360 | ||
| 361 | /* Find largest classes */ | |
| 362 | printf("\nTop 10 classes by size:\n"); | |
| 363 | ||
| 364 | /* Load vocabulary for word names */ | |
| 365 | uint32_t vocab_count = 0; | |
| 366 | VocabEntry* vocab = load_vocab("corpus-vocab.txt", &vocab_count); | |
| 367 | ||
| 368 | for (int rank = 0; rank < 10 && rank < (int)*stats_classes; rank++) { | |
| 369 | /* Find largest remaining class */ | |
| 370 | uint32_t best_class = 0, best_size = 0; | |
| 371 | for (uint32_t c = 0; c < *stats_classes; c++) { | |
| 372 | if (class_sizes[c] > best_size) { | |
| 373 | best_size = class_sizes[c]; | |
| 374 | best_class = c; | |
| 375 | } | |
| 376 | } | |
| 377 | if (best_size == 0) break; | |
| 378 | ||
| 379 | printf(" Class %u (size %u): ", best_class, best_size); | |
| 380 | ||
| 381 | /* Print first 10 words in this class */ | |
| 382 | int printed = 0; | |
| 383 | for (uint32_t w = 0; w < num_words && printed < 10; w++) { | |
| 384 | if (classes[w] == best_class) { | |
| 385 | if (vocab) { | |
| 386 | printf("%s ", lookup_word(vocab, vocab_count, w)); | |
| 387 | } else { | |
| 388 | printf("w%u ", w); | |
| 389 | } | |
| 390 | printed++; | |
| 391 | } | |
| 392 | } | |
| 393 | if (best_size > 10) printf("..."); | |
| 394 | printf("\n"); | |
| 395 | ||
| 396 | class_sizes[best_class] = 0; /* mark as printed */ | |
| 397 | } | |
| 398 | ||
| 399 | /* Check 6: Convergence pattern */ | |
| 400 | printf("\nEntropy history (last %d values):\n", LL_ENTROPY_WINDOW); | |
| 401 | for (int i = 0; i < LL_ENTROPY_WINDOW; i++) { | |
| 402 | printf(" [%d] = %.6f\n", i, state->entropy_history[i]); | |
| 403 | } | |
| 404 | ||
| 405 | /* Summary */ | |
| 406 | printf("\n=== SUMMARY ===\n"); | |
| 407 | if (pass) { | |
| 408 | printf("ALL CHECKS PASSED\n"); | |
| 409 | } else { | |
| 410 | printf("SOME CHECKS FAILED\n"); | |
| 411 | } | |
| 412 | ||
| 413 | printf("\nPerformance:\n"); | |
| 414 | printf(" Pipeline throughput: %.1f sentences/sec\n", | |
| 415 | num_sentences * 1000.0 / total_time); | |
| 416 | printf(" Feed throughput: %.0f sentences/sec\n", | |
| 417 | num_sentences * 1000.0 / (t_feed_end - t_feed_start)); | |
| 418 | printf(" Time per iteration: %.1f ms\n", | |
| 419 | *stats_iteration > 0 ? total_time / *stats_iteration : 0.0); | |
| 420 | ||
| 421 | /* Cleanup */ | |
| 422 | free(classes); | |
| 423 | free(class_sizes); | |
| 424 | if (vocab) free(vocab); | |
| 425 | ll_shutdown(state, ring); | |
| 426 | free_corpus(&corpus); | |
| 427 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 428 | cudaFree(stats_iteration); cudaFree(stats_pairs); | |
| 429 | cudaFree(stats_classes); cudaFree(stats_entropy); | |
| 430 | ||
| 431 | return pass ? 0 : 1; | |
| 432 | } |
| … | … | @@ -1,711 +0,0 @@ |
| 1 | /* bench-kernels.cu — Micro-benchmarks for individual CUDA kernels | |
| 2 | * | |
| 3 | * Measures each kernel in isolation to identify bottlenecks. | |
| 4 | * 100 iterations per benchmark, reports min/mean/max/stddev. | |
| 5 | * | |
| 6 | * Benchmarks: | |
| 7 | * B1a: Connected Components (Shiloach-Vishkin) | |
| 8 | * B1b: Grammar cost computation | |
| 9 | * B1c: Class MI aggregation | |
| 10 | * B1d: PMFG parse (word-only) | |
| 11 | * B1e: Surprise computation | |
| 12 | * B1f: MI-neighborhood divergence | |
| 13 | * | |
| 14 | * Build: | |
| 15 | * nvcc -O2 -arch=sm_75 -rdc=true -o bench-kernels \ | |
| 16 | * bench-kernels.cu gpu-connected-components.cu gpu-connector-rewrite.cu \ | |
| 17 | * gpu-spanning-forest.cu gpu-divergence.cu -lcudadevrt -lm | |
| 18 | */ | |
| 19 | ||
| 20 | #include <cstdint> | |
| 21 | #include <cstdio> | |
| 22 | #include <cstdlib> | |
| 23 | #include <cstring> | |
| 24 | #include <cmath> | |
| 25 | #include <cfloat> | |
| 26 | #include <cuda_runtime.h> | |
| 27 | ||
| 28 | /* ─── Forward declarations from other .cu files ─── */ | |
| 29 | ||
| 30 | extern "C" { | |
| 31 | int cc_run( | |
| 32 | const uint32_t* d_cand_word_a, const uint32_t* d_cand_word_b, | |
| 33 | const double* d_cand_cosine, uint32_t num_candidates, | |
| 34 | uint32_t num_words, float threshold, | |
| 35 | uint32_t* d_edge_a, uint32_t* d_edge_b, float* d_edge_weight, | |
| 36 | uint32_t* d_edge_count, uint32_t edge_capacity, | |
| 37 | uint32_t* d_label, int* d_changed, | |
| 38 | uint32_t* d_component_flags, uint32_t* d_component_count, | |
| 39 | uint32_t* d_class_id, uint32_t* d_next_class_id, | |
| 40 | uint32_t* d_class_sizes, | |
| 41 | uint32_t* h_num_components, uint32_t* h_num_edges); | |
| 42 | ||
| 43 | void grammar_pipeline_run( | |
| 44 | const double* d_sec_count, double* d_sec_cost, uint32_t num_sections, | |
| 45 | const uint32_t* d_pair_word_a, const uint32_t* d_pair_word_b, | |
| 46 | const double* d_pair_mi, const double* d_pair_count, | |
| 47 | uint32_t num_pairs, const uint32_t* d_word_class_id, | |
| 48 | uint64_t* d_ht_keys, double* d_ht_mi_sum, uint32_t* d_ht_count, | |
| 49 | uint32_t ht_capacity, double* d_max_count); | |
| 50 | ||
| 51 | void spanning_forest_run( | |
| 52 | const uint32_t* d_sentence_words, const uint32_t* d_sentence_lengths, | |
| 53 | const uint32_t* d_sentence_offsets, uint32_t num_sentences, | |
| 54 | const uint32_t* d_word_class_id, | |
| 55 | const uint64_t* d_class_ht_keys, const double* d_class_ht_mi, | |
| 56 | uint32_t class_ht_capacity, | |
| 57 | const uint64_t* d_pair_ht_keys, const uint32_t* d_pair_ht_values, | |
| 58 | const double* d_pair_mi, uint32_t pair_ht_capacity, | |
| 59 | uint32_t* d_word_parse_a, uint32_t* d_word_parse_b, | |
| 60 | double* d_word_parse_mi, uint32_t* d_word_parse_count, | |
| 61 | uint32_t* d_gram_parse_a, uint32_t* d_gram_parse_b, | |
| 62 | double* d_gram_parse_mi, uint32_t* d_gram_parse_count, | |
| 63 | double* d_surprise); | |
| 64 | ||
| 65 | void divergence_run( | |
| 66 | const uint32_t* d_pair_a_word_a, const uint32_t* d_pair_a_word_b, | |
| 67 | const double* d_pair_a_mi, const double* d_pair_a_count, | |
| 68 | uint32_t num_pairs_a, | |
| 69 | const uint32_t* d_pair_b_word_a, const uint32_t* d_pair_b_word_b, | |
| 70 | const double* d_pair_b_mi, const double* d_pair_b_count, | |
| 71 | uint32_t num_pairs_b, | |
| 72 | uint32_t num_words, double polysemy_threshold, | |
| 73 | uint32_t* d_nbr_a_ids, double* d_nbr_a_mi, uint32_t* d_nbr_a_count, | |
| 74 | uint32_t* d_nbr_b_ids, double* d_nbr_b_mi, uint32_t* d_nbr_b_count, | |
| 75 | double* d_divergence, uint32_t* d_polysemy_flag); | |
| 76 | } | |
| 77 | ||
| 78 | /* ─── Timing helpers ─── */ | |
| 79 | ||
| 80 | #define ITERS 100 | |
| 81 | ||
| 82 | struct BenchStats { | |
| 83 | double min_ms, max_ms, mean_ms, stddev_ms; | |
| 84 | }; | |
| 85 | ||
| 86 | static BenchStats compute_stats(double* times, int n) { | |
| 87 | BenchStats s; | |
| 88 | s.min_ms = 1e30; s.max_ms = -1e30; | |
| 89 | double sum = 0.0, sum_sq = 0.0; | |
| 90 | for (int i = 0; i < n; i++) { | |
| 91 | if (times[i] < s.min_ms) s.min_ms = times[i]; | |
| 92 | if (times[i] > s.max_ms) s.max_ms = times[i]; | |
| 93 | sum += times[i]; | |
| 94 | sum_sq += times[i] * times[i]; | |
| 95 | } | |
| 96 | s.mean_ms = sum / n; | |
| 97 | s.stddev_ms = sqrt(sum_sq / n - s.mean_ms * s.mean_ms); | |
| 98 | return s; | |
| 99 | } | |
| 100 | ||
| 101 | /* ─── Data generation helpers ─── */ | |
| 102 | ||
| 103 | static uint32_t xorshift32(uint32_t* state) { | |
| 104 | uint32_t x = *state; | |
| 105 | x ^= x << 13; | |
| 106 | x ^= x >> 17; | |
| 107 | x ^= x << 5; | |
| 108 | *state = x; | |
| 109 | return x; | |
| 110 | } | |
| 111 | ||
| 112 | /* Generate random cosine candidate pairs */ | |
| 113 | static void gen_cosine_candidates( | |
| 114 | uint32_t* h_word_a, uint32_t* h_word_b, double* h_cosine, | |
| 115 | uint32_t num_cand, uint32_t num_words, uint32_t seed | |
| 116 | ) { | |
| 117 | uint32_t rng = seed; | |
| 118 | for (uint32_t i = 0; i < num_cand; i++) { | |
| 119 | uint32_t wa = xorshift32(&rng) % num_words; | |
| 120 | uint32_t wb = xorshift32(&rng) % num_words; | |
| 121 | if (wb == wa) wb = (wa + 1) % num_words; | |
| 122 | h_word_a[i] = (wa <= wb) ? wa : wb; | |
| 123 | h_word_b[i] = (wa <= wb) ? wb : wa; | |
| 124 | /* Cosine values: mostly low, some high (realistic distribution) */ | |
| 125 | double cos_val = (double)(xorshift32(&rng) % 10000) / 10000.0; | |
| 126 | cos_val = cos_val * cos_val; /* quadratic: bias towards low values */ | |
| 127 | h_cosine[i] = cos_val; | |
| 128 | } | |
| 129 | } | |
| 130 | ||
| 131 | /* Generate word pairs with MI/count data */ | |
| 132 | static void gen_word_pairs( | |
| 133 | uint32_t* h_wa, uint32_t* h_wb, double* h_mi, double* h_count, | |
| 134 | uint32_t num_pairs, uint32_t num_words, uint32_t seed | |
| 135 | ) { | |
| 136 | uint32_t rng = seed; | |
| 137 | for (uint32_t i = 0; i < num_pairs; i++) { | |
| 138 | uint32_t wa = xorshift32(&rng) % num_words; | |
| 139 | uint32_t wb = xorshift32(&rng) % num_words; | |
| 140 | if (wb == wa) wb = (wa + 1) % num_words; | |
| 141 | h_wa[i] = (wa <= wb) ? wa : wb; | |
| 142 | h_wb[i] = (wa <= wb) ? wb : wa; | |
| 143 | h_mi[i] = 0.5 + 3.0 * (double)(xorshift32(&rng) % 10000) / 10000.0; | |
| 144 | h_count[i] = 1.0 + (double)(xorshift32(&rng) % 100); | |
| 145 | } | |
| 146 | } | |
| 147 | ||
| 148 | /* Build a pair hash table on GPU */ | |
| 149 | static void build_pair_ht( | |
| 150 | uint32_t* h_wa, uint32_t* h_wb, uint32_t num_pairs, | |
| 151 | uint64_t* h_ht_keys, uint32_t* h_ht_values, uint32_t ht_cap | |
| 152 | ) { | |
| 153 | memset(h_ht_keys, 0xFF, ht_cap * sizeof(uint64_t)); | |
| 154 | memset(h_ht_values, 0xFF, ht_cap * sizeof(uint32_t)); | |
| 155 | ||
| 156 | for (uint32_t i = 0; i < num_pairs; i++) { | |
| 157 | uint32_t lo = (h_wa[i] <= h_wb[i]) ? h_wa[i] : h_wb[i]; | |
| 158 | uint32_t hi = (h_wa[i] <= h_wb[i]) ? h_wb[i] : h_wa[i]; | |
| 159 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 160 | ||
| 161 | /* splitmix64 */ | |
| 162 | uint64_t h = key; | |
| 163 | h ^= h >> 30; h *= 0xBF58476D1CE4E5B9ULL; | |
| 164 | h ^= h >> 27; h *= 0x94D049BB133111EBULL; | |
| 165 | h ^= h >> 31; | |
| 166 | ||
| 167 | uint64_t slot = h & (uint64_t)(ht_cap - 1); | |
| 168 | for (int probe = 0; probe < 4096; probe++) { | |
| 169 | if (h_ht_keys[slot] == 0xFFFFFFFFFFFFFFFFULL) { | |
| 170 | h_ht_keys[slot] = key; | |
| 171 | h_ht_values[slot] = i; | |
| 172 | break; | |
| 173 | } | |
| 174 | slot = (slot + 1) & (uint64_t)(ht_cap - 1); | |
| 175 | } | |
| 176 | } | |
| 177 | } | |
| 178 | ||
| 179 | /* ═══════════════════════════════════════════════════════════════ | |
| 180 | * B1a: CONNECTED COMPONENTS | |
| 181 | * ═══════════════════════════════════════════════════════════════ */ | |
| 182 | ||
| 183 | static void bench_cc() { | |
| 184 | printf("\n=== Benchmark B1a: Connected Components ===\n"); | |
| 185 | printf("%-8s | %-8s | %-6s | %-10s | %-10s | %-10s | %-10s\n", | |
| 186 | "Words", "Cands", "Comps", "Min(ms)", "Mean(ms)", "Max(ms)", "StdDev"); | |
| 187 | printf("---------|----------|--------|------------|------------|------------|------------\n"); | |
| 188 | ||
| 189 | uint32_t word_scales[] = {100, 500, 1000, 5000, 8000}; | |
| 190 | int num_scales = 5; | |
| 191 | ||
| 192 | for (int s = 0; s < num_scales; s++) { | |
| 193 | uint32_t nw = word_scales[s]; | |
| 194 | uint32_t nc = nw * 2; /* 2 candidates per word */ | |
| 195 | ||
| 196 | /* Host data */ | |
| 197 | uint32_t* h_wa = (uint32_t*)malloc(nc * sizeof(uint32_t)); | |
| 198 | uint32_t* h_wb = (uint32_t*)malloc(nc * sizeof(uint32_t)); | |
| 199 | double* h_cos = (double*)malloc(nc * sizeof(double)); | |
| 200 | gen_cosine_candidates(h_wa, h_wb, h_cos, nc, nw, 42 + s); | |
| 201 | ||
| 202 | /* Device buffers */ | |
| 203 | uint32_t *d_cand_wa, *d_cand_wb; | |
| 204 | double *d_cand_cos; | |
| 205 | cudaMalloc(&d_cand_wa, nc * sizeof(uint32_t)); | |
| 206 | cudaMalloc(&d_cand_wb, nc * sizeof(uint32_t)); | |
| 207 | cudaMalloc(&d_cand_cos, nc * sizeof(double)); | |
| 208 | cudaMemcpy(d_cand_wa, h_wa, nc * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 209 | cudaMemcpy(d_cand_wb, h_wb, nc * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 210 | cudaMemcpy(d_cand_cos, h_cos, nc * sizeof(double), cudaMemcpyHostToDevice); | |
| 211 | ||
| 212 | /* CC work buffers */ | |
| 213 | uint32_t edge_cap = nc; | |
| 214 | uint32_t *d_edge_a, *d_edge_b, *d_edge_count; | |
| 215 | float *d_edge_weight; | |
| 216 | uint32_t *d_label, *d_comp_flags, *d_comp_count; | |
| 217 | uint32_t *d_class_id, *d_next_id, *d_class_sizes; | |
| 218 | int *d_changed; | |
| 219 | ||
| 220 | cudaMalloc(&d_edge_a, edge_cap * sizeof(uint32_t)); | |
| 221 | cudaMalloc(&d_edge_b, edge_cap * sizeof(uint32_t)); | |
| 222 | cudaMalloc(&d_edge_weight, edge_cap * sizeof(float)); | |
| 223 | cudaMalloc(&d_edge_count, sizeof(uint32_t)); | |
| 224 | cudaMalloc(&d_label, nw * sizeof(uint32_t)); | |
| 225 | cudaMalloc(&d_changed, sizeof(int)); | |
| 226 | cudaMalloc(&d_comp_flags, nw * sizeof(uint32_t)); | |
| 227 | cudaMalloc(&d_comp_count, sizeof(uint32_t)); | |
| 228 | cudaMalloc(&d_class_id, nw * sizeof(uint32_t)); | |
| 229 | cudaMalloc(&d_next_id, sizeof(uint32_t)); | |
| 230 | cudaMalloc(&d_class_sizes, nw * sizeof(uint32_t)); | |
| 231 | ||
| 232 | /* Warmup */ | |
| 233 | uint32_t h_nc_out, h_ne_out; | |
| 234 | cc_run(d_cand_wa, d_cand_wb, d_cand_cos, nc, nw, 0.15f, | |
| 235 | d_edge_a, d_edge_b, d_edge_weight, d_edge_count, edge_cap, | |
| 236 | d_label, d_changed, d_comp_flags, d_comp_count, | |
| 237 | d_class_id, d_next_id, d_class_sizes, | |
| 238 | &h_nc_out, &h_ne_out); | |
| 239 | ||
| 240 | /* Benchmark */ | |
| 241 | double times[ITERS]; | |
| 242 | uint32_t last_comps = 0; | |
| 243 | ||
| 244 | for (int i = 0; i < ITERS; i++) { | |
| 245 | cudaEvent_t start, stop; | |
| 246 | cudaEventCreate(&start); | |
| 247 | cudaEventCreate(&stop); | |
| 248 | cudaEventRecord(start); | |
| 249 | ||
| 250 | cc_run(d_cand_wa, d_cand_wb, d_cand_cos, nc, nw, 0.15f, | |
| 251 | d_edge_a, d_edge_b, d_edge_weight, d_edge_count, edge_cap, | |
| 252 | d_label, d_changed, d_comp_flags, d_comp_count, | |
| 253 | d_class_id, d_next_id, d_class_sizes, | |
| 254 | &h_nc_out, &h_ne_out); | |
| 255 | ||
| 256 | cudaEventRecord(stop); | |
| 257 | cudaEventSynchronize(stop); | |
| 258 | float ms; | |
| 259 | cudaEventElapsedTime(&ms, start, stop); | |
| 260 | times[i] = (double)ms; | |
| 261 | last_comps = h_nc_out; | |
| 262 | cudaEventDestroy(start); | |
| 263 | cudaEventDestroy(stop); | |
| 264 | } | |
| 265 | ||
| 266 | BenchStats bs = compute_stats(times, ITERS); | |
| 267 | printf("%-8u | %-8u | %-6u | %10.3f | %10.3f | %10.3f | %10.3f\n", | |
| 268 | nw, nc, last_comps, bs.min_ms, bs.mean_ms, bs.max_ms, bs.stddev_ms); | |
| 269 | ||
| 270 | /* Cleanup */ | |
| 271 | free(h_wa); free(h_wb); free(h_cos); | |
| 272 | cudaFree(d_cand_wa); cudaFree(d_cand_wb); cudaFree(d_cand_cos); | |
| 273 | cudaFree(d_edge_a); cudaFree(d_edge_b); cudaFree(d_edge_weight); | |
| 274 | cudaFree(d_edge_count); cudaFree(d_label); cudaFree(d_changed); | |
| 275 | cudaFree(d_comp_flags); cudaFree(d_comp_count); | |
| 276 | cudaFree(d_class_id); cudaFree(d_next_id); cudaFree(d_class_sizes); | |
| 277 | } | |
| 278 | } | |
| 279 | ||
| 280 | /* ═══════════════════════════════════════════════════════════════ | |
| 281 | * B1b + B1c: GRAMMAR COSTS + CLASS MI | |
| 282 | * ═══════════════════════════════════════════════════════════════ */ | |
| 283 | ||
| 284 | static void bench_grammar() { | |
| 285 | printf("\n=== Benchmark B1b: Grammar Costs ===\n"); | |
| 286 | printf("%-10s | %-10s | %-10s | %-10s | %-10s\n", | |
| 287 | "Sections", "Min(ms)", "Mean(ms)", "Max(ms)", "StdDev"); | |
| 288 | printf("-----------|------------|------------|------------|------------\n"); | |
| 289 | ||
| 290 | uint32_t sec_scales[] = {100, 1000, 10000, 64000}; | |
| 291 | int num_scales = 4; | |
| 292 | ||
| 293 | for (int s = 0; s < num_scales; s++) { | |
| 294 | uint32_t ns = sec_scales[s]; | |
| 295 | ||
| 296 | /* Generate section counts */ | |
| 297 | double* h_sec_count = (double*)malloc(ns * sizeof(double)); | |
| 298 | uint32_t rng = 123 + s; | |
| 299 | for (uint32_t i = 0; i < ns; i++) { | |
| 300 | h_sec_count[i] = 1.0 + (double)(xorshift32(&rng) % 1000); | |
| 301 | } | |
| 302 | ||
| 303 | /* Pair data for class MI */ | |
| 304 | uint32_t np = (ns < 10000) ? ns : 10000; | |
| 305 | uint32_t nw = 500; | |
| 306 | uint32_t* h_wa = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 307 | uint32_t* h_wb = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 308 | double* h_mi = (double*)malloc(np * sizeof(double)); | |
| 309 | double* h_count = (double*)malloc(np * sizeof(double)); | |
| 310 | gen_word_pairs(h_wa, h_wb, h_mi, h_count, np, nw, 456 + s); | |
| 311 | ||
| 312 | /* Class assignments (10 classes) */ | |
| 313 | uint32_t* h_class = (uint32_t*)malloc(nw * sizeof(uint32_t)); | |
| 314 | for (uint32_t i = 0; i < nw; i++) h_class[i] = i % 10; | |
| 315 | ||
| 316 | /* Allocate device memory */ | |
| 317 | double *d_sec_count, *d_sec_cost, *d_max_count; | |
| 318 | uint32_t *d_wa, *d_wb, *d_class_id; | |
| 319 | double *d_mi, *d_pcount; | |
| 320 | uint64_t *d_ht_keys; | |
| 321 | double *d_ht_mi_sum; | |
| 322 | uint32_t *d_ht_count; | |
| 323 | uint32_t ht_cap = 65536; | |
| 324 | ||
| 325 | cudaMalloc(&d_sec_count, ns * sizeof(double)); | |
| 326 | cudaMalloc(&d_sec_cost, ns * sizeof(double)); | |
| 327 | cudaMalloc(&d_max_count, sizeof(double)); | |
| 328 | cudaMalloc(&d_wa, np * sizeof(uint32_t)); | |
| 329 | cudaMalloc(&d_wb, np * sizeof(uint32_t)); | |
| 330 | cudaMalloc(&d_mi, np * sizeof(double)); | |
| 331 | cudaMalloc(&d_pcount, np * sizeof(double)); | |
| 332 | cudaMalloc(&d_class_id, nw * sizeof(uint32_t)); | |
| 333 | cudaMalloc(&d_ht_keys, ht_cap * sizeof(uint64_t)); | |
| 334 | cudaMalloc(&d_ht_mi_sum, ht_cap * sizeof(double)); | |
| 335 | cudaMalloc(&d_ht_count, ht_cap * sizeof(uint32_t)); | |
| 336 | ||
| 337 | cudaMemcpy(d_sec_count, h_sec_count, ns * sizeof(double), cudaMemcpyHostToDevice); | |
| 338 | cudaMemcpy(d_wa, h_wa, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 339 | cudaMemcpy(d_wb, h_wb, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 340 | cudaMemcpy(d_mi, h_mi, np * sizeof(double), cudaMemcpyHostToDevice); | |
| 341 | cudaMemcpy(d_pcount, h_count, np * sizeof(double), cudaMemcpyHostToDevice); | |
| 342 | cudaMemcpy(d_class_id, h_class, nw * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 343 | ||
| 344 | /* Warmup */ | |
| 345 | grammar_pipeline_run(d_sec_count, d_sec_cost, ns, | |
| 346 | d_wa, d_wb, d_mi, d_pcount, np, d_class_id, | |
| 347 | d_ht_keys, d_ht_mi_sum, d_ht_count, ht_cap, d_max_count); | |
| 348 | ||
| 349 | /* Benchmark */ | |
| 350 | double times[ITERS]; | |
| 351 | for (int i = 0; i < ITERS; i++) { | |
| 352 | cudaEvent_t start, stop; | |
| 353 | cudaEventCreate(&start); | |
| 354 | cudaEventCreate(&stop); | |
| 355 | cudaEventRecord(start); | |
| 356 | ||
| 357 | grammar_pipeline_run(d_sec_count, d_sec_cost, ns, | |
| 358 | d_wa, d_wb, d_mi, d_pcount, np, d_class_id, | |
| 359 | d_ht_keys, d_ht_mi_sum, d_ht_count, ht_cap, d_max_count); | |
| 360 | ||
| 361 | cudaEventRecord(stop); | |
| 362 | cudaEventSynchronize(stop); | |
| 363 | float ms; | |
| 364 | cudaEventElapsedTime(&ms, start, stop); | |
| 365 | times[i] = (double)ms; | |
| 366 | cudaEventDestroy(start); | |
| 367 | cudaEventDestroy(stop); | |
| 368 | } | |
| 369 | ||
| 370 | BenchStats bs = compute_stats(times, ITERS); | |
| 371 | printf("%-10u | %10.3f | %10.3f | %10.3f | %10.3f\n", | |
| 372 | ns, bs.min_ms, bs.mean_ms, bs.max_ms, bs.stddev_ms); | |
| 373 | ||
| 374 | /* Cleanup */ | |
| 375 | free(h_sec_count); free(h_wa); free(h_wb); free(h_mi); free(h_count); free(h_class); | |
| 376 | cudaFree(d_sec_count); cudaFree(d_sec_cost); cudaFree(d_max_count); | |
| 377 | cudaFree(d_wa); cudaFree(d_wb); cudaFree(d_mi); cudaFree(d_pcount); | |
| 378 | cudaFree(d_class_id); | |
| 379 | cudaFree(d_ht_keys); cudaFree(d_ht_mi_sum); cudaFree(d_ht_count); | |
| 380 | } | |
| 381 | } | |
| 382 | ||
| 383 | /* ═══════════════════════════════════════════════════════════════ | |
| 384 | * B1d + B1e: PMFG PARSE + SURPRISE | |
| 385 | * ═══════════════════════════════════════════════════════════════ */ | |
| 386 | ||
| 387 | #define MAX_TREE_EDGES 63 | |
| 388 | ||
| 389 | static void bench_parse() { | |
| 390 | printf("\n=== Benchmark B1d: PMFG Parse + Surprise ===\n"); | |
| 391 | printf("%-6s | %-6s | %-10s | %-10s | %-10s | %-10s | %-12s\n", | |
| 392 | "Sents", "AvgLen", "Min(ms)", "Mean(ms)", "Max(ms)", "StdDev", "Sents/sec"); | |
| 393 | printf("-------|--------|------------|------------|------------|------------|-------------\n"); | |
| 394 | ||
| 395 | struct ParseScale { | |
| 396 | uint32_t num_sentences; | |
| 397 | uint32_t avg_len; | |
| 398 | }; | |
| 399 | ParseScale scales[] = { | |
| 400 | {1, 10}, {1, 32}, {1, 64}, | |
| 401 | {10, 10}, {10, 32}, | |
| 402 | {64, 10}, {64, 32} | |
| 403 | }; | |
| 404 | int num_scales = 7; | |
| 405 | ||
| 406 | for (int s = 0; s < num_scales; s++) { | |
| 407 | uint32_t ns = scales[s].num_sentences; | |
| 408 | uint32_t avg_len = scales[s].avg_len; | |
| 409 | uint32_t nw = 500; /* word pool */ | |
| 410 | uint32_t np = 2000; /* pairs */ | |
| 411 | ||
| 412 | /* Generate pair data + hash table */ | |
| 413 | uint32_t* h_wa = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 414 | uint32_t* h_wb = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 415 | double* h_mi = (double*)malloc(np * sizeof(double)); | |
| 416 | double* h_count = (double*)malloc(np * sizeof(double)); | |
| 417 | gen_word_pairs(h_wa, h_wb, h_mi, h_count, np, nw, 789 + s); | |
| 418 | ||
| 419 | uint32_t ht_cap = 8192; | |
| 420 | uint64_t* h_ht_keys = (uint64_t*)malloc(ht_cap * sizeof(uint64_t)); | |
| 421 | uint32_t* h_ht_vals = (uint32_t*)malloc(ht_cap * sizeof(uint32_t)); | |
| 422 | build_pair_ht(h_wa, h_wb, np, h_ht_keys, h_ht_vals, ht_cap); | |
| 423 | ||
| 424 | /* Class data (10 classes) */ | |
| 425 | uint32_t* h_class = (uint32_t*)malloc(nw * sizeof(uint32_t)); | |
| 426 | for (uint32_t i = 0; i < nw; i++) h_class[i] = i % 10; | |
| 427 | ||
| 428 | /* Generate sentences */ | |
| 429 | uint32_t total_words = 0; | |
| 430 | uint32_t* h_sent_lengths = (uint32_t*)malloc(ns * sizeof(uint32_t)); | |
| 431 | uint32_t* h_sent_offsets = (uint32_t*)malloc(ns * sizeof(uint32_t)); | |
| 432 | uint32_t rng = 321 + s; | |
| 433 | for (uint32_t i = 0; i < ns; i++) { | |
| 434 | h_sent_lengths[i] = avg_len; | |
| 435 | h_sent_offsets[i] = total_words; | |
| 436 | total_words += avg_len; | |
| 437 | } | |
| 438 | uint32_t* h_sent_words = (uint32_t*)malloc(total_words * sizeof(uint32_t)); | |
| 439 | for (uint32_t i = 0; i < total_words; i++) { | |
| 440 | h_sent_words[i] = xorshift32(&rng) % nw; | |
| 441 | } | |
| 442 | ||
| 443 | /* Allocate device buffers */ | |
| 444 | uint32_t *d_wa, *d_wb; | |
| 445 | double *d_mi; | |
| 446 | uint64_t *d_ht_keys; | |
| 447 | uint32_t *d_ht_vals, *d_class_id; | |
| 448 | uint32_t *d_sent_words, *d_sent_lengths, *d_sent_offsets; | |
| 449 | uint32_t *d_word_pa, *d_word_pb, *d_word_pc; | |
| 450 | double *d_word_pmi; | |
| 451 | uint32_t *d_gram_pa, *d_gram_pb, *d_gram_pc; | |
| 452 | double *d_gram_pmi; | |
| 453 | double *d_surprise; | |
| 454 | ||
| 455 | /* Class MI hash table (empty — grammar parse will fallback to word-pair) */ | |
| 456 | uint64_t *d_class_ht_keys; | |
| 457 | double *d_class_ht_mi; | |
| 458 | uint32_t class_ht_cap = 1024; | |
| 459 | ||
| 460 | cudaMalloc(&d_wa, np * sizeof(uint32_t)); | |
| 461 | cudaMalloc(&d_wb, np * sizeof(uint32_t)); | |
| 462 | cudaMalloc(&d_mi, np * sizeof(double)); | |
| 463 | cudaMalloc(&d_ht_keys, ht_cap * sizeof(uint64_t)); | |
| 464 | cudaMalloc(&d_ht_vals, ht_cap * sizeof(uint32_t)); | |
| 465 | cudaMalloc(&d_class_id, nw * sizeof(uint32_t)); | |
| 466 | cudaMalloc(&d_class_ht_keys, class_ht_cap * sizeof(uint64_t)); | |
| 467 | cudaMalloc(&d_class_ht_mi, class_ht_cap * sizeof(double)); | |
| 468 | cudaMalloc(&d_sent_words, total_words * sizeof(uint32_t)); | |
| 469 | cudaMalloc(&d_sent_lengths, ns * sizeof(uint32_t)); | |
| 470 | cudaMalloc(&d_sent_offsets, ns * sizeof(uint32_t)); | |
| 471 | cudaMalloc(&d_word_pa, ns * MAX_TREE_EDGES * sizeof(uint32_t)); | |
| 472 | cudaMalloc(&d_word_pb, ns * MAX_TREE_EDGES * sizeof(uint32_t)); | |
| 473 | cudaMalloc(&d_word_pmi, ns * MAX_TREE_EDGES * sizeof(double)); | |
| 474 | cudaMalloc(&d_word_pc, ns * sizeof(uint32_t)); | |
| 475 | cudaMalloc(&d_gram_pa, ns * MAX_TREE_EDGES * sizeof(uint32_t)); | |
| 476 | cudaMalloc(&d_gram_pb, ns * MAX_TREE_EDGES * sizeof(uint32_t)); | |
| 477 | cudaMalloc(&d_gram_pmi, ns * MAX_TREE_EDGES * sizeof(double)); | |
| 478 | cudaMalloc(&d_gram_pc, ns * sizeof(uint32_t)); | |
| 479 | cudaMalloc(&d_surprise, ns * sizeof(double)); | |
| 480 | ||
| 481 | cudaMemcpy(d_wa, h_wa, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 482 | cudaMemcpy(d_wb, h_wb, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 483 | cudaMemcpy(d_mi, h_mi, np * sizeof(double), cudaMemcpyHostToDevice); | |
| 484 | cudaMemcpy(d_ht_keys, h_ht_keys, ht_cap * sizeof(uint64_t), cudaMemcpyHostToDevice); | |
| 485 | cudaMemcpy(d_ht_vals, h_ht_vals, ht_cap * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 486 | cudaMemcpy(d_class_id, h_class, nw * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 487 | cudaMemset(d_class_ht_keys, 0xFF, class_ht_cap * sizeof(uint64_t)); | |
| 488 | cudaMemset(d_class_ht_mi, 0, class_ht_cap * sizeof(double)); | |
| 489 | cudaMemcpy(d_sent_words, h_sent_words, total_words * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 490 | cudaMemcpy(d_sent_lengths, h_sent_lengths, ns * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 491 | cudaMemcpy(d_sent_offsets, h_sent_offsets, ns * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 492 | ||
| 493 | /* Warmup */ | |
| 494 | spanning_forest_run( | |
| 495 | d_sent_words, d_sent_lengths, d_sent_offsets, ns, | |
| 496 | d_class_id, d_class_ht_keys, d_class_ht_mi, class_ht_cap, | |
| 497 | d_ht_keys, d_ht_vals, d_mi, ht_cap, | |
| 498 | d_word_pa, d_word_pb, d_word_pmi, d_word_pc, | |
| 499 | d_gram_pa, d_gram_pb, d_gram_pmi, d_gram_pc, | |
| 500 | d_surprise); | |
| 501 | ||
| 502 | /* Benchmark */ | |
| 503 | double times[ITERS]; | |
| 504 | for (int i = 0; i < ITERS; i++) { | |
| 505 | cudaEvent_t start, stop; | |
| 506 | cudaEventCreate(&start); | |
| 507 | cudaEventCreate(&stop); | |
| 508 | cudaEventRecord(start); | |
| 509 | ||
| 510 | spanning_forest_run( | |
| 511 | d_sent_words, d_sent_lengths, d_sent_offsets, ns, | |
| 512 | d_class_id, d_class_ht_keys, d_class_ht_mi, class_ht_cap, | |
| 513 | d_ht_keys, d_ht_vals, d_mi, ht_cap, | |
| 514 | d_word_pa, d_word_pb, d_word_pmi, d_word_pc, | |
| 515 | d_gram_pa, d_gram_pb, d_gram_pmi, d_gram_pc, | |
| 516 | d_surprise); | |
| 517 | ||
| 518 | cudaEventRecord(stop); | |
| 519 | cudaEventSynchronize(stop); | |
| 520 | float ms; | |
| 521 | cudaEventElapsedTime(&ms, start, stop); | |
| 522 | times[i] = (double)ms; | |
| 523 | cudaEventDestroy(start); | |
| 524 | cudaEventDestroy(stop); | |
| 525 | } | |
| 526 | ||
| 527 | BenchStats bs = compute_stats(times, ITERS); | |
| 528 | double sents_per_sec = (bs.mean_ms > 0.001) ? (ns * 1000.0 / bs.mean_ms) : 0.0; | |
| 529 | printf("%-6u | %-6u | %10.3f | %10.3f | %10.3f | %10.3f | %12.1f\n", | |
| 530 | ns, avg_len, bs.min_ms, bs.mean_ms, bs.max_ms, bs.stddev_ms, sents_per_sec); | |
| 531 | ||
| 532 | /* Cleanup */ | |
| 533 | free(h_wa); free(h_wb); free(h_mi); free(h_count); free(h_class); | |
| 534 | free(h_ht_keys); free(h_ht_vals); | |
| 535 | free(h_sent_words); free(h_sent_lengths); free(h_sent_offsets); | |
| 536 | cudaFree(d_wa); cudaFree(d_wb); cudaFree(d_mi); | |
| 537 | cudaFree(d_ht_keys); cudaFree(d_ht_vals); cudaFree(d_class_id); | |
| 538 | cudaFree(d_class_ht_keys); cudaFree(d_class_ht_mi); | |
| 539 | cudaFree(d_sent_words); cudaFree(d_sent_lengths); cudaFree(d_sent_offsets); | |
| 540 | cudaFree(d_word_pa); cudaFree(d_word_pb); cudaFree(d_word_pmi); cudaFree(d_word_pc); | |
| 541 | cudaFree(d_gram_pa); cudaFree(d_gram_pb); cudaFree(d_gram_pmi); cudaFree(d_gram_pc); | |
| 542 | cudaFree(d_surprise); | |
| 543 | } | |
| 544 | } | |
| 545 | ||
| 546 | /* ═══════════════════════════════════════════════════════════════ | |
| 547 | * B1f: MI-NEIGHBORHOOD DIVERGENCE | |
| 548 | * ═══════════════════════════════════════════════════════════════ */ | |
| 549 | ||
| 550 | static void bench_divergence() { | |
| 551 | printf("\n=== Benchmark B1f: MI-Neighborhood Divergence ===\n"); | |
| 552 | printf("%-8s | %-8s | %-10s | %-10s | %-10s | %-10s | %-8s\n", | |
| 553 | "Words", "Pairs", "Min(ms)", "Mean(ms)", "Max(ms)", "StdDev", "Polysem"); | |
| 554 | printf("---------|----------|------------|------------|------------|------------|--------\n"); | |
| 555 | ||
| 556 | uint32_t word_scales[] = {100, 500, 1000}; | |
| 557 | int num_scales = 3; | |
| 558 | ||
| 559 | for (int s = 0; s < num_scales; s++) { | |
| 560 | uint32_t nw = word_scales[s]; | |
| 561 | uint32_t np = nw * 4; /* pairs per compartment */ | |
| 562 | ||
| 563 | /* Generate pair data for two compartments */ | |
| 564 | uint32_t* h_wa_a = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 565 | uint32_t* h_wb_a = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 566 | double* h_mi_a = (double*)malloc(np * sizeof(double)); | |
| 567 | double* h_cnt_a = (double*)malloc(np * sizeof(double)); | |
| 568 | gen_word_pairs(h_wa_a, h_wb_a, h_mi_a, h_cnt_a, np, nw, 111 + s); | |
| 569 | ||
| 570 | uint32_t* h_wa_b = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 571 | uint32_t* h_wb_b = (uint32_t*)malloc(np * sizeof(uint32_t)); | |
| 572 | double* h_mi_b = (double*)malloc(np * sizeof(double)); | |
| 573 | double* h_cnt_b = (double*)malloc(np * sizeof(double)); | |
| 574 | gen_word_pairs(h_wa_b, h_wb_b, h_mi_b, h_cnt_b, np, nw, 222 + s); | |
| 575 | ||
| 576 | /* Device buffers */ | |
| 577 | uint32_t *d_wa_a, *d_wb_a, *d_wa_b, *d_wb_b; | |
| 578 | double *d_mi_a, *d_cnt_a, *d_mi_b, *d_cnt_b; | |
| 579 | uint32_t *d_nbr_a_ids, *d_nbr_b_ids, *d_nbr_a_cnt, *d_nbr_b_cnt; | |
| 580 | double *d_nbr_a_mi, *d_nbr_b_mi; | |
| 581 | double *d_divergence; | |
| 582 | uint32_t *d_polysemy; | |
| 583 | ||
| 584 | cudaMalloc(&d_wa_a, np * sizeof(uint32_t)); | |
| 585 | cudaMalloc(&d_wb_a, np * sizeof(uint32_t)); | |
| 586 | cudaMalloc(&d_mi_a, np * sizeof(double)); | |
| 587 | cudaMalloc(&d_cnt_a, np * sizeof(double)); | |
| 588 | cudaMalloc(&d_wa_b, np * sizeof(uint32_t)); | |
| 589 | cudaMalloc(&d_wb_b, np * sizeof(uint32_t)); | |
| 590 | cudaMalloc(&d_mi_b, np * sizeof(double)); | |
| 591 | cudaMalloc(&d_cnt_b, np * sizeof(double)); | |
| 592 | cudaMalloc(&d_nbr_a_ids, nw * 32 * sizeof(uint32_t)); | |
| 593 | cudaMalloc(&d_nbr_a_mi, nw * 32 * sizeof(double)); | |
| 594 | cudaMalloc(&d_nbr_a_cnt, nw * sizeof(uint32_t)); | |
| 595 | cudaMalloc(&d_nbr_b_ids, nw * 32 * sizeof(uint32_t)); | |
| 596 | cudaMalloc(&d_nbr_b_mi, nw * 32 * sizeof(double)); | |
| 597 | cudaMalloc(&d_nbr_b_cnt, nw * sizeof(uint32_t)); | |
| 598 | cudaMalloc(&d_divergence, nw * sizeof(double)); | |
| 599 | cudaMalloc(&d_polysemy, nw * sizeof(uint32_t)); | |
| 600 | ||
| 601 | cudaMemcpy(d_wa_a, h_wa_a, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 602 | cudaMemcpy(d_wb_a, h_wb_a, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 603 | cudaMemcpy(d_mi_a, h_mi_a, np * sizeof(double), cudaMemcpyHostToDevice); | |
| 604 | cudaMemcpy(d_cnt_a, h_cnt_a, np * sizeof(double), cudaMemcpyHostToDevice); | |
| 605 | cudaMemcpy(d_wa_b, h_wa_b, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 606 | cudaMemcpy(d_wb_b, h_wb_b, np * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 607 | cudaMemcpy(d_mi_b, h_mi_b, np * sizeof(double), cudaMemcpyHostToDevice); | |
| 608 | cudaMemcpy(d_cnt_b, h_cnt_b, np * sizeof(double), cudaMemcpyHostToDevice); | |
| 609 | ||
| 610 | /* Warmup */ | |
| 611 | divergence_run( | |
| 612 | d_wa_a, d_wb_a, d_mi_a, d_cnt_a, np, | |
| 613 | d_wa_b, d_wb_b, d_mi_b, d_cnt_b, np, | |
| 614 | nw, 0.5, | |
| 615 | d_nbr_a_ids, d_nbr_a_mi, d_nbr_a_cnt, | |
| 616 | d_nbr_b_ids, d_nbr_b_mi, d_nbr_b_cnt, | |
| 617 | d_divergence, d_polysemy); | |
| 618 | ||
| 619 | /* Benchmark */ | |
| 620 | double times[ITERS]; | |
| 621 | for (int i = 0; i < ITERS; i++) { | |
| 622 | cudaEvent_t start, stop; | |
| 623 | cudaEventCreate(&start); | |
| 624 | cudaEventCreate(&stop); | |
| 625 | cudaEventRecord(start); | |
| 626 | ||
| 627 | divergence_run( | |
| 628 | d_wa_a, d_wb_a, d_mi_a, d_cnt_a, np, | |
| 629 | d_wa_b, d_wb_b, d_mi_b, d_cnt_b, np, | |
| 630 | nw, 0.5, | |
| 631 | d_nbr_a_ids, d_nbr_a_mi, d_nbr_a_cnt, | |
| 632 | d_nbr_b_ids, d_nbr_b_mi, d_nbr_b_cnt, | |
| 633 | d_divergence, d_polysemy); | |
| 634 | ||
| 635 | cudaEventRecord(stop); | |
| 636 | cudaEventSynchronize(stop); | |
| 637 | float ms; | |
| 638 | cudaEventElapsedTime(&ms, start, stop); | |
| 639 | times[i] = (double)ms; | |
| 640 | cudaEventDestroy(start); | |
| 641 | cudaEventDestroy(stop); | |
| 642 | } | |
| 643 | ||
| 644 | /* Count polysemy flags */ | |
| 645 | uint32_t* h_poly = (uint32_t*)malloc(nw * sizeof(uint32_t)); | |
| 646 | cudaMemcpy(h_poly, d_polysemy, nw * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 647 | uint32_t poly_count = 0; | |
| 648 | for (uint32_t i = 0; i < nw; i++) if (h_poly[i]) poly_count++; | |
| 649 | ||
| 650 | BenchStats bs = compute_stats(times, ITERS); | |
| 651 | printf("%-8u | %-8u | %10.3f | %10.3f | %10.3f | %10.3f | %u\n", | |
| 652 | nw, np, bs.min_ms, bs.mean_ms, bs.max_ms, bs.stddev_ms, poly_count); | |
| 653 | ||
| 654 | /* Cleanup */ | |
| 655 | free(h_wa_a); free(h_wb_a); free(h_mi_a); free(h_cnt_a); | |
| 656 | free(h_wa_b); free(h_wb_b); free(h_mi_b); free(h_cnt_b); | |
| 657 | free(h_poly); | |
| 658 | cudaFree(d_wa_a); cudaFree(d_wb_a); cudaFree(d_mi_a); cudaFree(d_cnt_a); | |
| 659 | cudaFree(d_wa_b); cudaFree(d_wb_b); cudaFree(d_mi_b); cudaFree(d_cnt_b); | |
| 660 | cudaFree(d_nbr_a_ids); cudaFree(d_nbr_a_mi); cudaFree(d_nbr_a_cnt); | |
| 661 | cudaFree(d_nbr_b_ids); cudaFree(d_nbr_b_mi); cudaFree(d_nbr_b_cnt); | |
| 662 | cudaFree(d_divergence); cudaFree(d_polysemy); | |
| 663 | } | |
| 664 | } | |
| 665 | ||
| 666 | /* ═══════════════════════════════════════════════════════════════ | |
| 667 | * MAIN | |
| 668 | * ═══════════════════════════════════════════════════════════════ */ | |
| 669 | ||
| 670 | int main() { | |
| 671 | printf("GPU Learning Pipeline — Micro-Benchmarks\n"); | |
| 672 | printf("=========================================\n"); | |
| 673 | ||
| 674 | /* Print GPU info */ | |
| 675 | cudaDeviceProp prop; | |
| 676 | cudaGetDeviceProperties(&prop, 0); | |
| 677 | printf("GPU: %s (SM %d.%d, %d SMs, %d MB)\n", | |
| 678 | prop.name, prop.major, prop.minor, | |
| 679 | prop.multiProcessorCount, | |
| 680 | (int)(prop.totalGlobalMem / (1024*1024))); | |
| 681 | printf("Iterations per benchmark: %d\n", ITERS); | |
| 682 | ||
| 683 | bench_cc(); | |
| 684 | bench_grammar(); | |
| 685 | bench_parse(); | |
| 686 | bench_divergence(); | |
| 687 | ||
| 688 | printf("\n=== All micro-benchmarks complete ===\n"); | |
| 689 | ||
| 690 | /* Print LearningState size estimate */ | |
| 691 | printf("\nMemory estimates:\n"); | |
| 692 | size_t ls_size = sizeof(uint32_t) /* word_count */ | |
| 693 | + 8192 * sizeof(double) /* word_marginal */ | |
| 694 | + 8192 * sizeof(uint32_t) /* word_class_id */ | |
| 695 | + sizeof(uint32_t) /* pair_count */ | |
| 696 | + 262144 * sizeof(uint32_t) * 3 /* pair_word_a/b + pair_dirty */ | |
| 697 | + 262144 * sizeof(double) * 2 /* pair_count + pair_mi */ | |
| 698 | + 524288 * sizeof(uint64_t) /* pair_ht_keys */ | |
| 699 | + 524288 * sizeof(uint32_t) /* pair_ht_values */ | |
| 700 | + sizeof(uint32_t) * 4 + sizeof(double) /* stats */ | |
| 701 | + 5 * sizeof(double) + sizeof(uint32_t) /* entropy */ | |
| 702 | + 65536 * sizeof(uint32_t) * 2 /* cc_edge_a/b */ | |
| 703 | + 8192 * sizeof(uint32_t) /* cc_labels */ | |
| 704 | + sizeof(uint32_t) /* cc_edge_count */ | |
| 705 | + sizeof(int) * 5; /* pipeline flags */ | |
| 706 | printf(" LearningState struct: %.2f MB\n", (double)ls_size / (1024.0 * 1024.0)); | |
| 707 | printf(" SentenceRing struct: %.2f KB\n", | |
| 708 | (double)(64 * (64 * sizeof(uint32_t) + 2 * sizeof(uint32_t)) + 2 * sizeof(uint32_t)) / 1024.0); | |
| 709 | ||
| 710 | return 0; | |
| 711 | } |
| … | … | @@ -1,325 +0,0 @@ |
| 1 | /* bench-mi-comparison.cu — CUDA vs OpenCL MI kernel comparison | |
| 2 | * | |
| 3 | * Generates identical test data at multiple scales, runs the CUDA MI | |
| 4 | * computation (same code path as the persistent kernel), and reports | |
| 5 | * throughput. Also writes test data to a file for OpenCL comparison | |
| 6 | * via the existing gpu-mi.scm Scheme wrapper. | |
| 7 | * | |
| 8 | * Scales: 1K, 10K, 100K, 262K pairs | |
| 9 | * | |
| 10 | * Build: | |
| 11 | * nvcc -O2 -arch=sm_75 -rdc=true -o bench-mi-comparison \ | |
| 12 | * bench-mi-comparison.cu gpu-learning-loop.cu -lcudadevrt -lm | |
| 13 | */ | |
| 14 | ||
| 15 | #include <cstdint> | |
| 16 | #include <cstdio> | |
| 17 | #include <cstdlib> | |
| 18 | #include <cstring> | |
| 19 | #include <cmath> | |
| 20 | #include <ctime> | |
| 21 | #include <cuda_runtime.h> | |
| 22 | #include <cooperative_groups.h> | |
| 23 | #include "gpu-learning-types.h" | |
| 24 | ||
| 25 | namespace cg = cooperative_groups; | |
| 26 | ||
| 27 | /* ─── Forward declarations ─── */ | |
| 28 | ||
| 29 | extern "C" { | |
| 30 | LearningState* ll_init(uint32_t num_words); | |
| 31 | SentenceRing* ll_init_ring(); | |
| 32 | void ll_shutdown(LearningState* state, SentenceRing* ring); | |
| 33 | } | |
| 34 | ||
| 35 | /* ─── Helpers ─── */ | |
| 36 | ||
| 37 | static uint32_t xorshift32(uint32_t* state) { | |
| 38 | uint32_t x = *state; | |
| 39 | x ^= x << 13; | |
| 40 | x ^= x >> 17; | |
| 41 | x ^= x << 5; | |
| 42 | *state = x; | |
| 43 | return x; | |
| 44 | } | |
| 45 | ||
| 46 | static double time_ms() { | |
| 47 | struct timespec ts; | |
| 48 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 49 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6; | |
| 50 | } | |
| 51 | ||
| 52 | /* ─── Reference MI computation (CPU, double precision) ─── */ | |
| 53 | ||
| 54 | static double cpu_mi(double count, double left_marginal, double right_marginal, | |
| 55 | double total_obs) { | |
| 56 | if (count < 1.0) return 0.0; | |
| 57 | double log2_factor = 1.4426950408889634; | |
| 58 | double eps = 1e-10; | |
| 59 | double n = fmax(total_obs, eps); | |
| 60 | double l = fmax(left_marginal, eps); | |
| 61 | double r = fmax(right_marginal, eps); | |
| 62 | return (log(count) + log(n) - log(l) - log(r)) * log2_factor; | |
| 63 | } | |
| 64 | ||
| 65 | /* ═══════════════════════════════════════════════════════════════ | |
| 66 | * MI BENCHMARK | |
| 67 | * | |
| 68 | * Uses LearningState directly (same memory layout as persistent kernel). | |
| 69 | * Populates pairs + marginals, then calls the MI kernel via a minimal | |
| 70 | * cooperative kernel that runs ll_compute_mi_dirty once. | |
| 71 | * ═══════════════════════════════════════════════════════════════ */ | |
| 72 | ||
| 73 | /* Minimal kernel that just computes MI (one iteration of the persistent kernel) */ | |
| 74 | __global__ void mi_only_kernel(LearningState* state) { | |
| 75 | auto grid = cg::this_grid(); | |
| 76 | uint32_t tid = grid.thread_rank(); | |
| 77 | uint32_t stride = grid.size(); | |
| 78 | ||
| 79 | double n = state->total_pair_observations; | |
| 80 | double log2_factor = 1.4426950408889634; | |
| 81 | double eps = 1e-10; | |
| 82 | uint32_t num_pairs = state->pair_count_u32; | |
| 83 | ||
| 84 | for (uint32_t p = tid; p < num_pairs; p += stride) { | |
| 85 | if (state->pair_dirty[p] != 1) continue; | |
| 86 | ||
| 87 | double count = state->pair_count[p]; | |
| 88 | if (count < 1.0) { | |
| 89 | state->pair_mi[p] = 0.0; | |
| 90 | state->pair_dirty[p] = 0; | |
| 91 | continue; | |
| 92 | } | |
| 93 | ||
| 94 | uint32_t wa = state->pair_word_a[p]; | |
| 95 | uint32_t wb = state->pair_word_b[p]; | |
| 96 | double left = fmax(state->word_marginal[wa], eps); | |
| 97 | double right = fmax(state->word_marginal[wb], eps); | |
| 98 | double n_safe = fmax(n, eps); | |
| 99 | ||
| 100 | double mi = (log(count) + log(n_safe) - log(left) - log(right)) * log2_factor; | |
| 101 | state->pair_mi[p] = mi; | |
| 102 | state->pair_dirty[p] = 0; | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | struct MIBenchResult { | |
| 107 | uint32_t num_pairs; | |
| 108 | double cuda_time_ms; | |
| 109 | double cuda_throughput; /* pairs/sec */ | |
| 110 | double cpu_time_ms; | |
| 111 | double cpu_throughput; | |
| 112 | double max_abs_error; | |
| 113 | double mean_abs_error; | |
| 114 | }; | |
| 115 | ||
| 116 | static MIBenchResult bench_mi_at_scale(uint32_t num_pairs, uint32_t num_words, uint32_t seed) { | |
| 117 | MIBenchResult r; | |
| 118 | memset(&r, 0, sizeof(r)); | |
| 119 | r.num_pairs = num_pairs; | |
| 120 | ||
| 121 | uint32_t rng = seed; | |
| 122 | ||
| 123 | /* Init LearningState with managed memory */ | |
| 124 | LearningState* state = ll_init(num_words); | |
| 125 | ||
| 126 | /* Populate pairs and marginals */ | |
| 127 | state->pair_count_u32 = num_pairs; | |
| 128 | state->total_pair_observations = 0.0; | |
| 129 | ||
| 130 | for (uint32_t i = 0; i < num_pairs; i++) { | |
| 131 | uint32_t wa = xorshift32(&rng) % num_words; | |
| 132 | uint32_t wb = xorshift32(&rng) % num_words; | |
| 133 | if (wb == wa) wb = (wa + 1) % num_words; | |
| 134 | uint32_t lo = (wa <= wb) ? wa : wb; | |
| 135 | uint32_t hi = (wa <= wb) ? wb : wa; | |
| 136 | ||
| 137 | state->pair_word_a[i] = lo; | |
| 138 | state->pair_word_b[i] = hi; | |
| 139 | double cnt = 1.0 + (double)(xorshift32(&rng) % 100); | |
| 140 | state->pair_count[i] = cnt; | |
| 141 | state->pair_mi[i] = 0.0; | |
| 142 | state->pair_dirty[i] = 1; | |
| 143 | ||
| 144 | state->word_marginal[lo] += cnt; | |
| 145 | state->word_marginal[hi] += cnt; | |
| 146 | state->total_pair_observations += cnt; | |
| 147 | } | |
| 148 | state->dirty_count = num_pairs; | |
| 149 | ||
| 150 | /* ─── CPU reference ─── */ | |
| 151 | double* cpu_mi_vals = (double*)malloc(num_pairs * sizeof(double)); | |
| 152 | double t_cpu_start = time_ms(); | |
| 153 | for (uint32_t i = 0; i < num_pairs; i++) { | |
| 154 | cpu_mi_vals[i] = cpu_mi( | |
| 155 | state->pair_count[i], | |
| 156 | state->word_marginal[state->pair_word_a[i]], | |
| 157 | state->word_marginal[state->pair_word_b[i]], | |
| 158 | state->total_pair_observations); | |
| 159 | } | |
| 160 | double t_cpu_end = time_ms(); | |
| 161 | r.cpu_time_ms = t_cpu_end - t_cpu_start; | |
| 162 | r.cpu_throughput = (r.cpu_time_ms > 0.001) | |
| 163 | ? (num_pairs * 1000.0 / r.cpu_time_ms) : 0.0; | |
| 164 | ||
| 165 | /* ─── CUDA MI kernel (cooperative launch) ─── */ | |
| 166 | /* Warmup */ | |
| 167 | { | |
| 168 | int num_blocks = 0; | |
| 169 | cudaOccupancyMaxActiveBlocksPerMultiprocessor(&num_blocks, mi_only_kernel, 128, 0); | |
| 170 | int num_sms = 0; | |
| 171 | cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0); | |
| 172 | dim3 grid_dim(num_sms); | |
| 173 | dim3 block_dim(128); | |
| 174 | void* args[] = { &state }; | |
| 175 | cudaLaunchCooperativeKernel((void*)mi_only_kernel, grid_dim, block_dim, args); | |
| 176 | cudaDeviceSynchronize(); | |
| 177 | } | |
| 178 | ||
| 179 | /* Reset dirty flags for benchmark */ | |
| 180 | for (uint32_t i = 0; i < num_pairs; i++) { | |
| 181 | state->pair_dirty[i] = 1; | |
| 182 | state->pair_mi[i] = 0.0; | |
| 183 | } | |
| 184 | cudaDeviceSynchronize(); | |
| 185 | ||
| 186 | /* Benchmark: average over 10 runs */ | |
| 187 | int n_runs = 10; | |
| 188 | double total_cuda_ms = 0.0; | |
| 189 | ||
| 190 | for (int run = 0; run < n_runs; run++) { | |
| 191 | /* Reset dirty flags */ | |
| 192 | for (uint32_t i = 0; i < num_pairs; i++) { | |
| 193 | state->pair_dirty[i] = 1; | |
| 194 | } | |
| 195 | cudaDeviceSynchronize(); | |
| 196 | ||
| 197 | cudaEvent_t start, stop; | |
| 198 | cudaEventCreate(&start); | |
| 199 | cudaEventCreate(&stop); | |
| 200 | ||
| 201 | int num_sms = 0; | |
| 202 | cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0); | |
| 203 | dim3 grid_dim(num_sms); | |
| 204 | dim3 block_dim(128); | |
| 205 | void* args[] = { &state }; | |
| 206 | ||
| 207 | cudaEventRecord(start); | |
| 208 | cudaLaunchCooperativeKernel((void*)mi_only_kernel, grid_dim, block_dim, args); | |
| 209 | cudaEventRecord(stop); | |
| 210 | cudaEventSynchronize(stop); | |
| 211 | ||
| 212 | float ms; | |
| 213 | cudaEventElapsedTime(&ms, start, stop); | |
| 214 | total_cuda_ms += ms; | |
| 215 | ||
| 216 | cudaEventDestroy(start); | |
| 217 | cudaEventDestroy(stop); | |
| 218 | } | |
| 219 | ||
| 220 | r.cuda_time_ms = total_cuda_ms / n_runs; | |
| 221 | r.cuda_throughput = (r.cuda_time_ms > 0.001) | |
| 222 | ? (num_pairs * 1000.0 / r.cuda_time_ms) : 0.0; | |
| 223 | ||
| 224 | /* ─── Compare CUDA vs CPU results ─── */ | |
| 225 | cudaDeviceSynchronize(); | |
| 226 | r.max_abs_error = 0.0; | |
| 227 | r.mean_abs_error = 0.0; | |
| 228 | ||
| 229 | for (uint32_t i = 0; i < num_pairs; i++) { | |
| 230 | double err = fabs(state->pair_mi[i] - cpu_mi_vals[i]); | |
| 231 | if (err > r.max_abs_error) r.max_abs_error = err; | |
| 232 | r.mean_abs_error += err; | |
| 233 | } | |
| 234 | r.mean_abs_error /= num_pairs; | |
| 235 | ||
| 236 | /* ─── Write test data for OpenCL comparison ─── */ | |
| 237 | if (num_pairs <= 10000) { | |
| 238 | char fname[256]; | |
| 239 | snprintf(fname, sizeof(fname), "mi-test-data-%uk.bin", num_pairs / 1000); | |
| 240 | FILE* f = fopen(fname, "wb"); | |
| 241 | if (f) { | |
| 242 | /* Header: num_pairs, num_words, total_obs */ | |
| 243 | fwrite(&num_pairs, sizeof(uint32_t), 1, f); | |
| 244 | fwrite(&num_words, sizeof(uint32_t), 1, f); | |
| 245 | fwrite(&state->total_pair_observations, sizeof(double), 1, f); | |
| 246 | /* Pair data */ | |
| 247 | fwrite(state->pair_word_a, sizeof(uint32_t), num_pairs, f); | |
| 248 | fwrite(state->pair_word_b, sizeof(uint32_t), num_pairs, f); | |
| 249 | fwrite(state->pair_count, sizeof(double), num_pairs, f); | |
| 250 | /* Marginals */ | |
| 251 | fwrite(state->word_marginal, sizeof(double), num_words, f); | |
| 252 | /* Reference MI values */ | |
| 253 | fwrite(cpu_mi_vals, sizeof(double), num_pairs, f); | |
| 254 | fclose(f); | |
| 255 | printf(" Wrote %s for OpenCL comparison\n", fname); | |
| 256 | } | |
| 257 | } | |
| 258 | ||
| 259 | free(cpu_mi_vals); | |
| 260 | ||
| 261 | /* Cleanup: need a dummy ring for ll_shutdown */ | |
| 262 | SentenceRing* ring = ll_init_ring(); | |
| 263 | ll_shutdown(state, ring); | |
| 264 | ||
| 265 | return r; | |
| 266 | } | |
| 267 | ||
| 268 | /* ═══════════════════════════════════════════════════════════════ | |
| 269 | * MAIN | |
| 270 | * ═══════════════════════════════════════════════════════════════ */ | |
| 271 | ||
| 272 | int main() { | |
| 273 | printf("GPU Learning Pipeline — MI Kernel Comparison\n"); | |
| 274 | printf("=============================================\n"); | |
| 275 | ||
| 276 | cudaDeviceProp prop; | |
| 277 | cudaGetDeviceProperties(&prop, 0); | |
| 278 | printf("GPU: %s (SM %d.%d, %d SMs, %d MB)\n", | |
| 279 | prop.name, prop.major, prop.minor, | |
| 280 | prop.multiProcessorCount, | |
| 281 | (int)(prop.totalGlobalMem / (1024*1024))); | |
| 282 | ||
| 283 | int supports_coop = 0; | |
| 284 | cudaDeviceGetAttribute(&supports_coop, cudaDevAttrCooperativeLaunch, 0); | |
| 285 | if (!supports_coop) { | |
| 286 | printf("ERROR: Device does not support cooperative launch\n"); | |
| 287 | return 1; | |
| 288 | } | |
| 289 | ||
| 290 | printf("\n=== Experiment 3: CUDA MI Throughput ===\n"); | |
| 291 | printf("%-10s | %-10s | %-12s | %-14s | %-12s | %-14s | %-12s | %-12s\n", | |
| 292 | "Pairs", "Words", "CUDA(ms)", "CUDA(pairs/s)", "CPU(ms)", "CPU(pairs/s)", | |
| 293 | "MaxErr", "MeanErr"); | |
| 294 | printf("-----------|------------|--------------|----------------|--------------|----------------|--------------|------------\n"); | |
| 295 | ||
| 296 | struct { uint32_t pairs; uint32_t words; } scales[] = { | |
| 297 | {1000, 200}, | |
| 298 | {10000, 1000}, | |
| 299 | {100000, 5000}, | |
| 300 | {262144, 8192} | |
| 301 | }; | |
| 302 | int num_scales = 4; | |
| 303 | ||
| 304 | for (int s = 0; s < num_scales; s++) { | |
| 305 | printf("Running %uK pairs, %u words...\n", scales[s].pairs / 1000, scales[s].words); | |
| 306 | MIBenchResult r = bench_mi_at_scale(scales[s].pairs, scales[s].words, 42 + s); | |
| 307 | ||
| 308 | printf("%-10u | %-10u | %12.3f | %14.0f | %12.3f | %14.0f | %12.2e | %12.2e\n", | |
| 309 | r.num_pairs, scales[s].words, | |
| 310 | r.cuda_time_ms, r.cuda_throughput, | |
| 311 | r.cpu_time_ms, r.cpu_throughput, | |
| 312 | r.max_abs_error, r.mean_abs_error); | |
| 313 | } | |
| 314 | ||
| 315 | /* Baseline comparison summary */ | |
| 316 | printf("\n=== Comparison with known OpenCL baseline ===\n"); | |
| 317 | printf("OpenCL MI throughput (prior benchmark): 385,000 pairs/sec\n"); | |
| 318 | printf("CPU MI throughput (corpus-learning.scm): ~7,000 pairs/sec\n"); | |
| 319 | printf("See CUDA throughput above for comparison.\n"); | |
| 320 | printf("\nNote: OpenCL comparison data files written for scales <= 10K.\n"); | |
| 321 | printf("Run gpu-mi.scm with these files for direct accuracy comparison.\n"); | |
| 322 | ||
| 323 | printf("\n=== MI comparison complete ===\n"); | |
| 324 | return 0; | |
| 325 | } |
| … | … | @@ -1,420 +0,0 @@ |
| 1 | /* bench-pipeline.cu — End-to-end persistent kernel benchmark | |
| 2 | * | |
| 3 | * Tests the full learning pipeline at realistic scales: | |
| 4 | * P1: Small (500 words, 200 sentences) | |
| 5 | * P2: Medium (2K words, 1K sentences) | |
| 6 | * P3: Full (5K words, 5K sentences) | |
| 7 | * P4: Max (8K words, 10K sentences) | |
| 8 | * | |
| 9 | * Also measures (Experiments 5 & 6): | |
| 10 | * - Sentence feeding rate (CPU→GPU bottleneck) | |
| 11 | * - GPU memory profile (cudaMemGetInfo) | |
| 12 | * - Ring buffer utilization | |
| 13 | * | |
| 14 | * Build: | |
| 15 | * nvcc -O2 -arch=sm_75 -rdc=true -o bench-pipeline \ | |
| 16 | * bench-pipeline.cu gpu-learning-loop.cu -lcudadevrt -lm | |
| 17 | */ | |
| 18 | ||
| 19 | #include <cstdint> | |
| 20 | #include <cstdio> | |
| 21 | #include <cstdlib> | |
| 22 | #include <cstring> | |
| 23 | #include <cmath> | |
| 24 | #include <ctime> | |
| 25 | #include <unistd.h> | |
| 26 | #include <cuda_runtime.h> | |
| 27 | #include "gpu-learning-types.h" | |
| 28 | ||
| 29 | /* ─── Forward declarations ─── */ | |
| 30 | ||
| 31 | extern "C" { | |
| 32 | LearningState* ll_init(uint32_t num_words); | |
| 33 | SentenceRing* ll_init_ring(); | |
| 34 | void ll_feed_sentence(SentenceRing* ring, uint32_t* words, uint32_t length); | |
| 35 | int ll_launch(LearningState* state, SentenceRing* ring, | |
| 36 | int* done_flag, int* pause_flag, | |
| 37 | uint32_t* stats_iteration, uint32_t* stats_pairs, | |
| 38 | uint32_t* stats_classes, double* stats_entropy); | |
| 39 | void ll_wait(); | |
| 40 | void ll_read_classes(LearningState* state, uint32_t* out, uint32_t n); | |
| 41 | void ll_shutdown(LearningState* state, SentenceRing* ring); | |
| 42 | } | |
| 43 | ||
| 44 | /* ─── Helpers ─── */ | |
| 45 | ||
| 46 | static uint32_t xorshift32(uint32_t* state) { | |
| 47 | uint32_t x = *state; | |
| 48 | x ^= x << 13; | |
| 49 | x ^= x >> 17; | |
| 50 | x ^= x << 5; | |
| 51 | *state = x; | |
| 52 | return x; | |
| 53 | } | |
| 54 | ||
| 55 | static double time_ms() { | |
| 56 | struct timespec ts; | |
| 57 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 58 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6; | |
| 59 | } | |
| 60 | ||
| 61 | /* Generate synthetic sentences with Zipf-like word distribution */ | |
| 62 | static void gen_sentences( | |
| 63 | uint32_t** out_sentences, /* array of sentence word arrays */ | |
| 64 | uint32_t* out_lengths, /* sentence lengths */ | |
| 65 | uint32_t num_sentences, | |
| 66 | uint32_t num_words, | |
| 67 | uint32_t seed | |
| 68 | ) { | |
| 69 | uint32_t rng = seed; | |
| 70 | ||
| 71 | /* Zipf distribution: word i has frequency proportional to 1/(i+1) */ | |
| 72 | for (uint32_t s = 0; s < num_sentences; s++) { | |
| 73 | /* Sentence length: 5 to 40 words (realistic) */ | |
| 74 | uint32_t len = 5 + (xorshift32(&rng) % 36); | |
| 75 | if (len > LL_MAX_SENTENCE_LEN) len = LL_MAX_SENTENCE_LEN; | |
| 76 | ||
| 77 | out_sentences[s] = (uint32_t*)malloc(len * sizeof(uint32_t)); | |
| 78 | out_lengths[s] = len; | |
| 79 | ||
| 80 | for (uint32_t w = 0; w < len; w++) { | |
| 81 | /* Zipf: higher probability for lower-indexed words */ | |
| 82 | double u = (double)(xorshift32(&rng) % 10000) / 10000.0; | |
| 83 | uint32_t idx = (uint32_t)(num_words * pow(u, 2.0)); | |
| 84 | if (idx >= num_words) idx = num_words - 1; | |
| 85 | out_sentences[s][w] = idx; | |
| 86 | } | |
| 87 | } | |
| 88 | } | |
| 89 | ||
| 90 | static void free_sentences(uint32_t** sents, uint32_t n) { | |
| 91 | for (uint32_t i = 0; i < n; i++) free(sents[i]); | |
| 92 | free(sents); | |
| 93 | } | |
| 94 | ||
| 95 | /* ─── GPU memory reporting ─── */ | |
| 96 | ||
| 97 | static void report_gpu_memory(const char* label) { | |
| 98 | size_t free_mem, total_mem; | |
| 99 | cudaMemGetInfo(&free_mem, &total_mem); | |
| 100 | printf(" GPU memory [%s]: %.1f MB used / %.1f MB total (%.1f MB free)\n", | |
| 101 | label, (total_mem - free_mem) / (1024.0 * 1024.0), | |
| 102 | total_mem / (1024.0 * 1024.0), | |
| 103 | free_mem / (1024.0 * 1024.0)); | |
| 104 | } | |
| 105 | ||
| 106 | /* ═══════════════════════════════════════════════════════════════ | |
| 107 | * PIPELINE BENCHMARK | |
| 108 | * ═══════════════════════════════════════════════════════════════ */ | |
| 109 | ||
| 110 | struct PipelineResult { | |
| 111 | uint32_t num_words; | |
| 112 | uint32_t num_sentences; | |
| 113 | double total_time_ms; | |
| 114 | uint32_t iterations; | |
| 115 | uint32_t pairs; | |
| 116 | uint32_t classes; | |
| 117 | double entropy; | |
| 118 | double sents_per_sec; | |
| 119 | int converged; | |
| 120 | double feeding_time_ms; | |
| 121 | double gpu_used_mb; | |
| 122 | }; | |
| 123 | ||
| 124 | static PipelineResult run_pipeline( | |
| 125 | uint32_t num_words, | |
| 126 | uint32_t num_sentences, | |
| 127 | uint32_t seed | |
| 128 | ) { | |
| 129 | PipelineResult result; | |
| 130 | memset(&result, 0, sizeof(result)); | |
| 131 | result.num_words = num_words; | |
| 132 | result.num_sentences = num_sentences; | |
| 133 | ||
| 134 | /* Generate sentences */ | |
| 135 | uint32_t** sentences = (uint32_t**)malloc(num_sentences * sizeof(uint32_t*)); | |
| 136 | uint32_t* lengths = (uint32_t*)malloc(num_sentences * sizeof(uint32_t)); | |
| 137 | gen_sentences(sentences, lengths, num_sentences, num_words, seed); | |
| 138 | ||
| 139 | /* Measure memory before init */ | |
| 140 | size_t mem_before_free, mem_before_total; | |
| 141 | cudaMemGetInfo(&mem_before_free, &mem_before_total); | |
| 142 | ||
| 143 | /* Init */ | |
| 144 | LearningState* state = ll_init(num_words); | |
| 145 | SentenceRing* ring = ll_init_ring(); | |
| 146 | ||
| 147 | int* done_flag; | |
| 148 | int* pause_flag; | |
| 149 | uint32_t* stats_iteration; | |
| 150 | uint32_t* stats_pairs; | |
| 151 | uint32_t* stats_classes; | |
| 152 | double* stats_entropy; | |
| 153 | ||
| 154 | cudaMallocManaged(&done_flag, sizeof(int)); | |
| 155 | cudaMallocManaged(&pause_flag, sizeof(int)); | |
| 156 | cudaMallocManaged(&stats_iteration, sizeof(uint32_t)); | |
| 157 | cudaMallocManaged(&stats_pairs, sizeof(uint32_t)); | |
| 158 | cudaMallocManaged(&stats_classes, sizeof(uint32_t)); | |
| 159 | cudaMallocManaged(&stats_entropy, sizeof(double)); | |
| 160 | ||
| 161 | *done_flag = 0; | |
| 162 | *pause_flag = 0; | |
| 163 | *stats_iteration = 0; | |
| 164 | *stats_pairs = 0; | |
| 165 | *stats_classes = 0; | |
| 166 | *stats_entropy = 0.0; | |
| 167 | ||
| 168 | /* Measure memory after init */ | |
| 169 | size_t mem_after_free, mem_after_total; | |
| 170 | cudaMemGetInfo(&mem_after_free, &mem_after_total); | |
| 171 | result.gpu_used_mb = (double)(mem_before_free - mem_after_free) / (1024.0 * 1024.0); | |
| 172 | ||
| 173 | /* Launch persistent kernel */ | |
| 174 | double t_start = time_ms(); | |
| 175 | ||
| 176 | int err = ll_launch(state, ring, done_flag, pause_flag, | |
| 177 | stats_iteration, stats_pairs, stats_classes, stats_entropy); | |
| 178 | if (err != 0) { | |
| 179 | printf(" FAILED to launch kernel!\n"); | |
| 180 | ll_shutdown(state, ring); | |
| 181 | free_sentences(sentences, num_sentences); | |
| 182 | free(lengths); | |
| 183 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 184 | cudaFree(stats_iteration); cudaFree(stats_pairs); | |
| 185 | cudaFree(stats_classes); cudaFree(stats_entropy); | |
| 186 | return result; | |
| 187 | } | |
| 188 | ||
| 189 | /* Feed sentences */ | |
| 190 | double t_feed_start = time_ms(); | |
| 191 | for (uint32_t s = 0; s < num_sentences; s++) { | |
| 192 | ll_feed_sentence(ring, sentences[s], lengths[s]); | |
| 193 | } | |
| 194 | double t_feed_end = time_ms(); | |
| 195 | result.feeding_time_ms = t_feed_end - t_feed_start; | |
| 196 | ||
| 197 | /* Wait for convergence (or timeout after 60 seconds) */ | |
| 198 | double timeout = 60000.0; /* 60 seconds */ | |
| 199 | uint32_t last_iter = 0; | |
| 200 | double last_check = time_ms(); | |
| 201 | ||
| 202 | while (!(*done_flag)) { | |
| 203 | usleep(10000); /* 10ms poll interval */ | |
| 204 | ||
| 205 | if (time_ms() - t_start > timeout) { | |
| 206 | printf(" TIMEOUT after %.1f seconds\n", timeout / 1000.0); | |
| 207 | *done_flag = 1; | |
| 208 | break; | |
| 209 | } | |
| 210 | ||
| 211 | /* Progress reporting (every 2 seconds) */ | |
| 212 | if (time_ms() - last_check > 2000.0 && *stats_iteration > last_iter) { | |
| 213 | printf(" ... iter=%u pairs=%u classes=%u entropy=%.3f\n", | |
| 214 | *stats_iteration, *stats_pairs, *stats_classes, *stats_entropy); | |
| 215 | last_iter = *stats_iteration; | |
| 216 | last_check = time_ms(); | |
| 217 | } | |
| 218 | } | |
| 219 | ||
| 220 | ll_wait(); | |
| 221 | double t_end = time_ms(); | |
| 222 | ||
| 223 | result.total_time_ms = t_end - t_start; | |
| 224 | result.iterations = *stats_iteration; | |
| 225 | result.pairs = *stats_pairs; | |
| 226 | result.classes = *stats_classes; | |
| 227 | result.entropy = *stats_entropy; | |
| 228 | result.sents_per_sec = (result.total_time_ms > 0.001) | |
| 229 | ? (num_sentences * 1000.0 / result.total_time_ms) : 0.0; | |
| 230 | result.converged = (*stats_iteration < LL_MAX_ITERATIONS); | |
| 231 | ||
| 232 | /* Cleanup */ | |
| 233 | ll_shutdown(state, ring); | |
| 234 | free_sentences(sentences, num_sentences); | |
| 235 | free(lengths); | |
| 236 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 237 | cudaFree(stats_iteration); cudaFree(stats_pairs); | |
| 238 | cudaFree(stats_classes); cudaFree(stats_entropy); | |
| 239 | ||
| 240 | return result; | |
| 241 | } | |
| 242 | ||
| 243 | /* ═══════════════════════════════════════════════════════════════ | |
| 244 | * EXPERIMENT 5: SENTENCE FEEDING RATE | |
| 245 | * ═══════════════════════════════════════════════════════════════ */ | |
| 246 | ||
| 247 | static void bench_feeding_rate() { | |
| 248 | printf("\n=== Experiment 5: Sentence Feeding Rate ===\n"); | |
| 249 | printf("%-10s | %-10s | %-12s | %-14s\n", | |
| 250 | "Sentences", "AvgLen", "Feed(ms)", "Feed(sents/s)"); | |
| 251 | printf("-----------|------------|--------------|----------------\n"); | |
| 252 | ||
| 253 | uint32_t counts[] = {100, 1000, 5000, 10000}; | |
| 254 | int n_counts = 4; | |
| 255 | ||
| 256 | for (int c = 0; c < n_counts; c++) { | |
| 257 | uint32_t ns = counts[c]; | |
| 258 | uint32_t nw = 5000; | |
| 259 | ||
| 260 | /* Generate sentences */ | |
| 261 | uint32_t** sentences = (uint32_t**)malloc(ns * sizeof(uint32_t*)); | |
| 262 | uint32_t* lengths = (uint32_t*)malloc(ns * sizeof(uint32_t)); | |
| 263 | gen_sentences(sentences, lengths, ns, nw, 555 + c); | |
| 264 | ||
| 265 | /* Compute average length */ | |
| 266 | double avg_len = 0; | |
| 267 | for (uint32_t i = 0; i < ns; i++) avg_len += lengths[i]; | |
| 268 | avg_len /= ns; | |
| 269 | ||
| 270 | /* Init ring only (no kernel — measure pure feeding overhead) */ | |
| 271 | SentenceRing* ring = ll_init_ring(); | |
| 272 | ||
| 273 | /* Mark all slots as consumed (ring starts empty) */ | |
| 274 | for (int i = 0; i < LL_RING_SIZE; i++) { | |
| 275 | ring->slots[i].ready = 0; | |
| 276 | } | |
| 277 | ||
| 278 | /* We can't feed without a consumer, so just measure the memcpy cost */ | |
| 279 | double t_start = time_ms(); | |
| 280 | for (uint32_t s = 0; s < ns; s++) { | |
| 281 | /* Simulate feed: copy to slot, mark ready, advance */ | |
| 282 | uint32_t widx = s % LL_RING_SIZE; | |
| 283 | SentenceSlot* slot = &ring->slots[widx]; | |
| 284 | memcpy(slot->words, sentences[s], lengths[s] * sizeof(uint32_t)); | |
| 285 | slot->length = lengths[s]; | |
| 286 | __sync_synchronize(); | |
| 287 | slot->ready = 1; | |
| 288 | /* Immediately mark consumed so next iteration works */ | |
| 289 | slot->ready = 0; | |
| 290 | } | |
| 291 | double t_end = time_ms(); | |
| 292 | ||
| 293 | double feed_ms = t_end - t_start; | |
| 294 | double sps = (feed_ms > 0.001) ? (ns * 1000.0 / feed_ms) : 0.0; | |
| 295 | ||
| 296 | printf("%-10u | %-10.1f | %12.3f | %14.0f\n", | |
| 297 | ns, avg_len, feed_ms, sps); | |
| 298 | ||
| 299 | free_sentences(sentences, ns); | |
| 300 | free(lengths); | |
| 301 | cudaFree(ring); | |
| 302 | } | |
| 303 | } | |
| 304 | ||
| 305 | /* ═══════════════════════════════════════════════════════════════ | |
| 306 | * EXPERIMENT 6: MEMORY PROFILE | |
| 307 | * ═══════════════════════════════════════════════════════════════ */ | |
| 308 | ||
| 309 | static void bench_memory() { | |
| 310 | printf("\n=== Experiment 6: GPU Memory Profile ===\n"); | |
| 311 | ||
| 312 | report_gpu_memory("baseline"); | |
| 313 | ||
| 314 | /* Allocate LearningState */ | |
| 315 | LearningState* state = ll_init(8192); | |
| 316 | report_gpu_memory("after LearningState init"); | |
| 317 | ||
| 318 | SentenceRing* ring = ll_init_ring(); | |
| 319 | report_gpu_memory("after SentenceRing init"); | |
| 320 | ||
| 321 | /* Control flags */ | |
| 322 | int* done_flag; | |
| 323 | int* pause_flag; | |
| 324 | uint32_t* si; uint32_t* sp; uint32_t* sc; | |
| 325 | double* se; | |
| 326 | cudaMallocManaged(&done_flag, sizeof(int)); | |
| 327 | cudaMallocManaged(&pause_flag, sizeof(int)); | |
| 328 | cudaMallocManaged(&si, sizeof(uint32_t)); | |
| 329 | cudaMallocManaged(&sp, sizeof(uint32_t)); | |
| 330 | cudaMallocManaged(&sc, sizeof(uint32_t)); | |
| 331 | cudaMallocManaged(&se, sizeof(double)); | |
| 332 | report_gpu_memory("after control flags"); | |
| 333 | ||
| 334 | /* Computed struct sizes */ | |
| 335 | printf("\n Computed struct sizes:\n"); | |
| 336 | printf(" sizeof(LearningState) = %zu bytes (%.2f MB)\n", | |
| 337 | sizeof(LearningState), sizeof(LearningState) / (1024.0 * 1024.0)); | |
| 338 | printf(" sizeof(SentenceRing) = %zu bytes (%.2f KB)\n", | |
| 339 | sizeof(SentenceRing), sizeof(SentenceRing) / 1024.0); | |
| 340 | printf(" sizeof(SentenceSlot) = %zu bytes\n", sizeof(SentenceSlot)); | |
| 341 | ||
| 342 | /* Max vocabulary extrapolation */ | |
| 343 | size_t free_mem, total_mem; | |
| 344 | cudaMemGetInfo(&free_mem, &total_mem); | |
| 345 | printf("\n Available GPU memory: %.1f MB\n", free_mem / (1024.0 * 1024.0)); | |
| 346 | printf(" LearningState overhead: %.2f MB\n", sizeof(LearningState) / (1024.0 * 1024.0)); | |
| 347 | ||
| 348 | /* Cleanup */ | |
| 349 | ll_shutdown(state, ring); | |
| 350 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 351 | cudaFree(si); cudaFree(sp); cudaFree(sc); cudaFree(se); | |
| 352 | } | |
| 353 | ||
| 354 | /* ═══════════════════════════════════════════════════════════════ | |
| 355 | * MAIN | |
| 356 | * ═══════════════════════════════════════════════════════════════ */ | |
| 357 | ||
| 358 | int main() { | |
| 359 | printf("GPU Learning Pipeline — Integration Benchmarks\n"); | |
| 360 | printf("===============================================\n"); | |
| 361 | ||
| 362 | cudaDeviceProp prop; | |
| 363 | cudaGetDeviceProperties(&prop, 0); | |
| 364 | printf("GPU: %s (SM %d.%d, %d SMs, %d MB)\n", | |
| 365 | prop.name, prop.major, prop.minor, | |
| 366 | prop.multiProcessorCount, | |
| 367 | (int)(prop.totalGlobalMem / (1024*1024))); | |
| 368 | ||
| 369 | int supports_coop = 0; | |
| 370 | cudaDeviceGetAttribute(&supports_coop, cudaDevAttrCooperativeLaunch, 0); | |
| 371 | if (!supports_coop) { | |
| 372 | printf("ERROR: Device does not support cooperative launch (required for persistent kernel)\n"); | |
| 373 | return 1; | |
| 374 | } | |
| 375 | ||
| 376 | /* Run memory profile first (before any pipeline allocation) */ | |
| 377 | bench_memory(); | |
| 378 | ||
| 379 | /* Pipeline benchmarks */ | |
| 380 | struct { | |
| 381 | const char* name; | |
| 382 | uint32_t words; | |
| 383 | uint32_t sentences; | |
| 384 | } scales[] = { | |
| 385 | {"P1: Small", 500, 200}, | |
| 386 | {"P2: Medium", 2000, 1000}, | |
| 387 | {"P3: Full", 5000, 5000}, | |
| 388 | {"P4: Max", 8000, 10000} | |
| 389 | }; | |
| 390 | int num_scales = 4; | |
| 391 | ||
| 392 | printf("\n=== Experiment 2: Pipeline Integration Benchmarks ===\n"); | |
| 393 | printf("%-12s | %-6s | %-8s | %-8s | %-8s | %-6s | %-8s | %-10s | %-8s | %-8s\n", | |
| 394 | "Test", "Words", "Sents", "Time(s)", "Iters", "Pairs", "Classes", "Entropy", | |
| 395 | "S/sec", "GPU(MB)"); | |
| 396 | printf("-------------|--------|----------|----------|----------|--------|----------|------------|----------|--------\n"); | |
| 397 | ||
| 398 | for (int s = 0; s < num_scales; s++) { | |
| 399 | printf("\nRunning %s (%u words, %u sentences)...\n", | |
| 400 | scales[s].name, scales[s].words, scales[s].sentences); | |
| 401 | ||
| 402 | PipelineResult r = run_pipeline(scales[s].words, scales[s].sentences, 1000 + s); | |
| 403 | ||
| 404 | printf("%-12s | %-6u | %-8u | %8.2f | %-8u | %-6u | %-8u | %10.4f | %8.1f | %8.1f\n", | |
| 405 | scales[s].name, r.num_words, r.num_sentences, | |
| 406 | r.total_time_ms / 1000.0, r.iterations, r.pairs, r.classes, | |
| 407 | r.entropy, r.sents_per_sec, r.gpu_used_mb); | |
| 408 | printf(" Feed time: %.1f ms (%.0f sents/sec feeding rate)\n", | |
| 409 | r.feeding_time_ms, | |
| 410 | r.feeding_time_ms > 0.001 ? r.num_sentences * 1000.0 / r.feeding_time_ms : 0.0); | |
| 411 | printf(" Converged: %s, Iterations: %u\n", | |
| 412 | r.converged ? "YES" : "NO (hit max)", r.iterations); | |
| 413 | } | |
| 414 | ||
| 415 | /* Feeding rate benchmark */ | |
| 416 | bench_feeding_rate(); | |
| 417 | ||
| 418 | printf("\n=== All pipeline benchmarks complete ===\n"); | |
| 419 | return 0; | |
| 420 | } |
| … | … | @@ -1,17 +1,2 @@ |
| 1 | 1 | |
| 2 | # This declaration must happen before subdirectory traversal. | |
| 3 | DECLARE_GUILE_CONFIG_TARGET(SCM_CONFIG "opencog gpu-config" "OPENCOG_TEST") | |
| 4 | ||
| 5 | ADD_SUBDIRECTORY (gpu) | |
| 6 | 2 | ADD_SUBDIRECTORY (persist) |
| 7 | ||
| 8 | # ------------------------------------------------------------------ | |
| 9 | ADD_GUILE_MODULE (FILES | |
| 10 | scm/persist-gpu.scm | |
| 11 | MODULE_DESTINATION "${GUILE_SITE_DIR}/opencog" | |
| 12 | ) | |
| 13 | ||
| 14 | WRITE_GUILE_CONFIG(${GUILE_BIN_DIR}/opencog/gpu-config.scm SCM_CONFIG TRUE) | |
| 15 | WRITE_GUILE_CONFIG(${GUILE_BIN_DIR}/opencog/gpu-config-installable.scm SCM_CONFIG FALSE) | |
| 16 | ||
| 17 | INSTALL(FILES ${GUILE_BIN_DIR}/opencog/gpu-config-installable.scm DESTINATION ${GUILE_SITE_DIR}/opencog RENAME gpu-config.scm) |
| … | … | @@ -1,52 +0,0 @@ |
| 1 | # | |
| 2 | # GPU AtomSpace -- kernels and computation. | |
| 3 | # This is the actual AtomSpace implementation on the GPU. | |
| 4 | # | |
| 5 | # OpenCL kernels (.cl) are JIT-compiled at runtime. | |
| 6 | # CUDA kernels (.cu) are compiled at build time. | |
| 7 | # | |
| 8 | ||
| 9 | # -- OpenCL kernels (installed for runtime JIT compilation) -- | |
| 10 | SET(OPENCL_KERNEL_FILES | |
| 11 | gpu-hashtable.cl | |
| 12 | gpu-atomspace.cl | |
| 13 | gpu-mi.cl | |
| 14 | gpu-cosine.cl | |
| 15 | gpu-counting.cl | |
| 16 | gpu-sections.cl | |
| 17 | gpu-substitute.cl | |
| 18 | ) | |
| 19 | ||
| 20 | INSTALL(FILES ${OPENCL_KERNEL_FILES} | |
| 21 | DESTINATION "share/opencog/opencl/gpu" | |
| 22 | ) | |
| 23 | ||
| 24 | # -- CUDA kernels (compiled into shared library) -- | |
| 25 | IF (HAVE_CUDA) | |
| 26 | SET(CUDA_KERNEL_SOURCES | |
| 27 | gpu-learning-loop.cu | |
| 28 | gpu-learning-bridge.cu | |
| 29 | gpu-compartment-kernel.cu | |
| 30 | gpu-connected-components.cu | |
| 31 | gpu-connector-rewrite.cu | |
| 32 | gpu-spanning-forest.cu | |
| 33 | gpu-divergence.cu | |
| 34 | ) | |
| 35 | ||
| 36 | ADD_LIBRARY(gpu-kernels SHARED ${CUDA_KERNEL_SOURCES}) | |
| 37 | ||
| 38 | SET_TARGET_PROPERTIES(gpu-kernels PROPERTIES | |
| 39 | CUDA_SEPARABLE_COMPILATION ON | |
| 40 | CUDA_ARCHITECTURES "50;72;75;86;89" | |
| 41 | ) | |
| 42 | ||
| 43 | INSTALL(TARGETS gpu-kernels | |
| 44 | EXPORT AtomSpaceGPUTargets | |
| 45 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/opencog" | |
| 46 | ) | |
| 47 | ENDIF (HAVE_CUDA) | |
| 48 | ||
| 49 | # -- Header files -- | |
| 50 | INSTALL(FILES gpu-learning-types.h | |
| 51 | DESTINATION "include/opencog/gpu" | |
| 52 | ) |
| … | … | @@ -1,407 +0,0 @@ |
| 1 | /* | |
| 2 | * gpu-atomspace.cl -- GPU-resident AtomSpace pools | |
| 3 | * | |
| 4 | * Three flat SoA (struct-of-arrays) pools for uniform-size atoms: | |
| 5 | * - WordPool: words/concepts (nodes) | |
| 6 | * - PairPool: word pairs (binary links) | |
| 7 | * - SectionPool: word+disjunct (sections for connector vectors) | |
| 8 | * | |
| 9 | * Each pool has: | |
| 10 | * - A bump allocator (atomic counter for next free slot) | |
| 11 | * - A hash table mapping content hash → pool index | |
| 12 | * - SoA arrays for each field (coalesced GPU access) | |
| 13 | * | |
| 14 | * All counting uses atomic operations (CAS-loop for doubles). | |
| 15 | * | |
| 16 | * This file is concatenated after gpu-hashtable.cl at load time, | |
| 17 | * so ht_hash(), atom_cmpxchg, HT_EMPTY_KEY etc. are available. | |
| 18 | */ | |
| 19 | ||
| 20 | /* ─── Double-precision atomic add via CAS loop ─── */ | |
| 21 | ||
| 22 | /* | |
| 23 | * OpenCL has no native atomicAdd for doubles. We implement it | |
| 24 | * via compare-and-swap on the 64-bit integer representation. | |
| 25 | * Uses atom_cmpxchg from cl_khr_int64_base_atomics. | |
| 26 | */ | |
| 27 | void atomic_add_double(__global volatile double* addr, double val) | |
| 28 | { | |
| 29 | union { ulong i; double f; } next, expected, current; | |
| 30 | current.f = *addr; | |
| 31 | do { | |
| 32 | expected = current; | |
| 33 | next.f = expected.f + val; | |
| 34 | current.i = atom_cmpxchg( | |
| 35 | (__global volatile ulong*)addr, | |
| 36 | expected.i, next.i); | |
| 37 | } while (current.i != expected.i); | |
| 38 | } | |
| 39 | ||
| 40 | /* ─── Pool capacity constants (set by host at compile time) ─── */ | |
| 41 | ||
| 42 | /* These are defined as macros by the host via -D flags: | |
| 43 | * WORD_CAPACITY max words (e.g., 131072 = 128K) | |
| 44 | * PAIR_CAPACITY max pairs (e.g., 4194304 = 4M) | |
| 45 | * SECTION_CAPACITY max sections (e.g., 1048576 = 1M) | |
| 46 | * WORD_HT_CAPACITY word hash table size (power of 2) | |
| 47 | * PAIR_HT_CAPACITY pair hash table size (power of 2) | |
| 48 | * SECTION_HT_CAPACITY section hash table size (power of 2) | |
| 49 | */ | |
| 50 | ||
| 51 | /* ═══════════════════════════════════════════════════════════════ | |
| 52 | * WORD POOL — Nodes | |
| 53 | * | |
| 54 | * SoA layout: | |
| 55 | * word_name_hash[N] ulong — content hash (from string) | |
| 56 | * word_count[N] double — marginal count | |
| 57 | * word_mi_marginal[N] double — sum of MI for marginal | |
| 58 | * word_class_id[N] uint — class assignment (0 = none) | |
| 59 | * | |
| 60 | * Hash table: name_hash → word_index | |
| 61 | * ═══════════════════════════════════════════════════════════════ */ | |
| 62 | ||
| 63 | /* | |
| 64 | * Find or create words in the pool. | |
| 65 | * | |
| 66 | * For each input name_hash, looks up in the word hash table. | |
| 67 | * If found, returns the existing index. If not found, atomically | |
| 68 | * allocates a new slot and initializes it. | |
| 69 | * | |
| 70 | * Args: | |
| 71 | * wht_keys, wht_values - word hash table (keys + values) | |
| 72 | * word_name_hash - word pool: name hashes | |
| 73 | * word_count - word pool: counts (init to 0.0) | |
| 74 | * word_class_id - word pool: class IDs (init to 0) | |
| 75 | * word_next_free - atomic bump allocator (single uint) | |
| 76 | * in_name_hashes - input: name hashes to find/create | |
| 77 | * out_indices - output: pool index for each input | |
| 78 | * num_items - number of inputs | |
| 79 | */ | |
| 80 | __kernel void word_find_or_create( | |
| 81 | __global volatile ulong* wht_keys, | |
| 82 | __global volatile uint* wht_values, | |
| 83 | __global ulong* word_name_hash, | |
| 84 | __global double* word_count, | |
| 85 | __global uint* word_class_id, | |
| 86 | __global volatile uint* word_next_free, | |
| 87 | __global const ulong* in_name_hashes, | |
| 88 | __global uint* out_indices, | |
| 89 | const uint num_items) | |
| 90 | { | |
| 91 | uint tid = get_global_id(0); | |
| 92 | if (tid >= num_items) return; | |
| 93 | ||
| 94 | ulong name_hash = in_name_hashes[tid]; | |
| 95 | if (name_hash == HT_EMPTY_KEY) { | |
| 96 | out_indices[tid] = HT_EMPTY_VALUE; | |
| 97 | return; | |
| 98 | } | |
| 99 | ||
| 100 | ulong ht_cap = WORD_HT_CAPACITY; | |
| 101 | ulong mask = ht_cap - 1; | |
| 102 | ulong slot = ht_hash(name_hash) & mask; | |
| 103 | ||
| 104 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 105 | { | |
| 106 | ulong prev = atom_cmpxchg(&wht_keys[slot], HT_EMPTY_KEY, name_hash); | |
| 107 | ||
| 108 | if (prev == HT_EMPTY_KEY) | |
| 109 | { | |
| 110 | /* We won the slot — allocate from pool and publish value */ | |
| 111 | uint idx = atomic_add(word_next_free, 1U); | |
| 112 | if (idx < WORD_CAPACITY) { | |
| 113 | word_name_hash[idx] = name_hash; | |
| 114 | word_count[idx] = 0.0; | |
| 115 | word_class_id[idx] = 0; | |
| 116 | /* Write value LAST so spin-waiters see valid index */ | |
| 117 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 118 | wht_values[slot] = idx; | |
| 119 | out_indices[tid] = idx; | |
| 120 | } else { | |
| 121 | out_indices[tid] = HT_EMPTY_VALUE; | |
| 122 | } | |
| 123 | return; | |
| 124 | } | |
| 125 | if (prev == name_hash) | |
| 126 | { | |
| 127 | /* Already exists — spin until creator publishes value */ | |
| 128 | uint val = wht_values[slot]; | |
| 129 | while (val == HT_EMPTY_VALUE) { | |
| 130 | val = wht_values[slot]; | |
| 131 | } | |
| 132 | out_indices[tid] = val; | |
| 133 | return; | |
| 134 | } | |
| 135 | ||
| 136 | slot = (slot + 1) & mask; | |
| 137 | } | |
| 138 | out_indices[tid] = HT_EMPTY_VALUE; | |
| 139 | } | |
| 140 | ||
| 141 | /* ═══════════════════════════════════════════════════════════════ | |
| 142 | * PAIR POOL — Binary Links (word pairs) | |
| 143 | * | |
| 144 | * SoA layout: | |
| 145 | * pair_word_a[N] uint — index into WordPool | |
| 146 | * pair_word_b[N] uint — index into WordPool | |
| 147 | * pair_count[N] double — joint count | |
| 148 | * pair_mi[N] double — mutual information | |
| 149 | * pair_flags[N] uint — dirty bit, etc. | |
| 150 | * | |
| 151 | * Hash table key: (word_a << 32) | word_b (packed 64-bit) | |
| 152 | * ═══════════════════════════════════════════════════════════════ */ | |
| 153 | ||
| 154 | /* Pack two 32-bit word indices into a single 64-bit hash key */ | |
| 155 | inline ulong pair_key(uint word_a, uint word_b) | |
| 156 | { | |
| 157 | /* Always store with smaller index first for canonical ordering */ | |
| 158 | uint lo = min(word_a, word_b); | |
| 159 | uint hi = max(word_a, word_b); | |
| 160 | return ((ulong)lo << 32) | (ulong)hi; | |
| 161 | } | |
| 162 | ||
| 163 | /* | |
| 164 | * Find or create word pairs in the pool. | |
| 165 | * | |
| 166 | * Args: | |
| 167 | * pht_keys, pht_values - pair hash table | |
| 168 | * pair_word_a, pair_word_b - pair pool: word indices | |
| 169 | * pair_count - pair pool: counts (init to 0.0) | |
| 170 | * pair_mi - pair pool: MI values (init to 0.0) | |
| 171 | * pair_flags - pair pool: flags (init to 0) | |
| 172 | * pair_next_free - atomic bump allocator | |
| 173 | * in_word_a, in_word_b - input: word index pairs | |
| 174 | * out_indices - output: pair pool index for each input | |
| 175 | * num_items - number of inputs | |
| 176 | */ | |
| 177 | __kernel void pair_find_or_create( | |
| 178 | __global volatile ulong* pht_keys, | |
| 179 | __global volatile uint* pht_values, | |
| 180 | __global uint* pair_word_a, | |
| 181 | __global uint* pair_word_b, | |
| 182 | __global double* pair_count, | |
| 183 | __global double* pair_mi, | |
| 184 | __global uint* pair_flags, | |
| 185 | __global volatile uint* pair_next_free, | |
| 186 | __global const uint* in_word_a, | |
| 187 | __global const uint* in_word_b, | |
| 188 | __global uint* out_indices, | |
| 189 | const uint num_items) | |
| 190 | { | |
| 191 | uint tid = get_global_id(0); | |
| 192 | if (tid >= num_items) return; | |
| 193 | ||
| 194 | uint wa = in_word_a[tid]; | |
| 195 | uint wb = in_word_b[tid]; | |
| 196 | ulong key = pair_key(wa, wb); | |
| 197 | ||
| 198 | ulong ht_cap = PAIR_HT_CAPACITY; | |
| 199 | ulong mask = ht_cap - 1; | |
| 200 | ulong slot = ht_hash(key) & mask; | |
| 201 | ||
| 202 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 203 | { | |
| 204 | ulong prev = atom_cmpxchg(&pht_keys[slot], HT_EMPTY_KEY, key); | |
| 205 | ||
| 206 | if (prev == HT_EMPTY_KEY) | |
| 207 | { | |
| 208 | uint idx = atomic_add(pair_next_free, 1U); | |
| 209 | if (idx < PAIR_CAPACITY) { | |
| 210 | pair_word_a[idx] = min(wa, wb); | |
| 211 | pair_word_b[idx] = max(wa, wb); | |
| 212 | pair_count[idx] = 0.0; | |
| 213 | pair_mi[idx] = 0.0; | |
| 214 | pair_flags[idx] = 0; | |
| 215 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 216 | pht_values[slot] = idx; | |
| 217 | out_indices[tid] = idx; | |
| 218 | } else { | |
| 219 | out_indices[tid] = HT_EMPTY_VALUE; | |
| 220 | } | |
| 221 | return; | |
| 222 | } | |
| 223 | if (prev == key) | |
| 224 | { | |
| 225 | uint val = pht_values[slot]; | |
| 226 | while (val == HT_EMPTY_VALUE) { | |
| 227 | val = pht_values[slot]; | |
| 228 | } | |
| 229 | out_indices[tid] = val; | |
| 230 | return; | |
| 231 | } | |
| 232 | ||
| 233 | slot = (slot + 1) & mask; | |
| 234 | } | |
| 235 | out_indices[tid] = HT_EMPTY_VALUE; | |
| 236 | } | |
| 237 | ||
| 238 | /* ═══════════════════════════════════════════════════════════════ | |
| 239 | * SECTION POOL — Word + Disjunct | |
| 240 | * | |
| 241 | * SoA layout: | |
| 242 | * sec_word[N] uint — index into WordPool | |
| 243 | * sec_disjunct_hash[N] ulong — hash of full disjunct string | |
| 244 | * sec_count[N] double — section count | |
| 245 | * | |
| 246 | * Hash table key: hash(word_idx, disjunct_hash) | |
| 247 | * ═══════════════════════════════════════════════════════════════ */ | |
| 248 | ||
| 249 | /* Combine word index and disjunct hash into a single 64-bit key */ | |
| 250 | inline ulong section_key(uint word_idx, ulong disjunct_hash) | |
| 251 | { | |
| 252 | /* Mix the word index into the disjunct hash */ | |
| 253 | ulong key = disjunct_hash ^ ((ulong)word_idx * 0x9E3779B97F4A7C15UL); | |
| 254 | /* Ensure it's never the sentinel */ | |
| 255 | if (key == HT_EMPTY_KEY) key = 0; | |
| 256 | return key; | |
| 257 | } | |
| 258 | ||
| 259 | /* | |
| 260 | * Find or create sections in the pool. | |
| 261 | * | |
| 262 | * Args: | |
| 263 | * sht_keys, sht_values - section hash table | |
| 264 | * sec_word - section pool: word indices | |
| 265 | * sec_disjunct_hash - section pool: disjunct hashes | |
| 266 | * sec_count - section pool: counts (init to 0.0) | |
| 267 | * sec_next_free - atomic bump allocator | |
| 268 | * in_word_indices - input: word indices | |
| 269 | * in_disjunct_hashes - input: disjunct hashes | |
| 270 | * out_indices - output: section pool index | |
| 271 | * num_items - number of inputs | |
| 272 | */ | |
| 273 | __kernel void section_find_or_create( | |
| 274 | __global volatile ulong* sht_keys, | |
| 275 | __global volatile uint* sht_values, | |
| 276 | __global uint* sec_word, | |
| 277 | __global ulong* sec_disjunct_hash, | |
| 278 | __global double* sec_count, | |
| 279 | __global volatile uint* sec_next_free, | |
| 280 | __global const uint* in_word_indices, | |
| 281 | __global const ulong* in_disjunct_hashes, | |
| 282 | __global uint* out_indices, | |
| 283 | const uint num_items) | |
| 284 | { | |
| 285 | uint tid = get_global_id(0); | |
| 286 | if (tid >= num_items) return; | |
| 287 | ||
| 288 | uint widx = in_word_indices[tid]; | |
| 289 | ulong dhash = in_disjunct_hashes[tid]; | |
| 290 | ulong key = section_key(widx, dhash); | |
| 291 | ||
| 292 | ulong ht_cap = SECTION_HT_CAPACITY; | |
| 293 | ulong mask = ht_cap - 1; | |
| 294 | ulong slot = ht_hash(key) & mask; | |
| 295 | ||
| 296 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 297 | { | |
| 298 | ulong prev = atom_cmpxchg(&sht_keys[slot], HT_EMPTY_KEY, key); | |
| 299 | ||
| 300 | if (prev == HT_EMPTY_KEY) | |
| 301 | { | |
| 302 | uint idx = atomic_add(sec_next_free, 1U); | |
| 303 | if (idx < SECTION_CAPACITY) { | |
| 304 | sec_word[idx] = widx; | |
| 305 | sec_disjunct_hash[idx] = dhash; | |
| 306 | sec_count[idx] = 0.0; | |
| 307 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 308 | sht_values[slot] = idx; | |
| 309 | out_indices[tid] = idx; | |
| 310 | } else { | |
| 311 | out_indices[tid] = HT_EMPTY_VALUE; | |
| 312 | } | |
| 313 | return; | |
| 314 | } | |
| 315 | if (prev == key) | |
| 316 | { | |
| 317 | uint val = sht_values[slot]; | |
| 318 | while (val == HT_EMPTY_VALUE) { | |
| 319 | val = sht_values[slot]; | |
| 320 | } | |
| 321 | out_indices[tid] = val; | |
| 322 | return; | |
| 323 | } | |
| 324 | ||
| 325 | slot = (slot + 1) & mask; | |
| 326 | } | |
| 327 | out_indices[tid] = HT_EMPTY_VALUE; | |
| 328 | } | |
| 329 | ||
| 330 | /* ═══════════════════════════════════════════════════════════════ | |
| 331 | * COUNTING OPERATIONS | |
| 332 | * | |
| 333 | * Atomic double-precision increments on pool arrays. | |
| 334 | * ═══════════════════════════════════════════════════════════════ */ | |
| 335 | ||
| 336 | /* | |
| 337 | * Increment pair counts by 1.0 for given pair indices. | |
| 338 | * Also increments word marginal counts for both words. | |
| 339 | */ | |
| 340 | __kernel void count_pairs( | |
| 341 | __global volatile double* pair_count, | |
| 342 | __global const uint* pair_word_a, | |
| 343 | __global const uint* pair_word_b, | |
| 344 | __global volatile double* word_count, | |
| 345 | __global volatile uint* pair_flags, | |
| 346 | __global const uint* pair_indices, | |
| 347 | const uint num_items) | |
| 348 | { | |
| 349 | uint tid = get_global_id(0); | |
| 350 | if (tid >= num_items) return; | |
| 351 | ||
| 352 | uint pidx = pair_indices[tid]; | |
| 353 | if (pidx == HT_EMPTY_VALUE) return; | |
| 354 | ||
| 355 | /* Increment pair count */ | |
| 356 | atomic_add_double(&pair_count[pidx], 1.0); | |
| 357 | ||
| 358 | /* Increment word marginals */ | |
| 359 | uint wa = pair_word_a[pidx]; | |
| 360 | uint wb = pair_word_b[pidx]; | |
| 361 | atomic_add_double(&word_count[wa], 1.0); | |
| 362 | atomic_add_double(&word_count[wb], 1.0); | |
| 363 | ||
| 364 | /* Mark pair as dirty (needs MI recomputation) */ | |
| 365 | pair_flags[pidx] = 1; | |
| 366 | } | |
| 367 | ||
| 368 | /* | |
| 369 | * Increment section counts by 1.0 for given section indices. | |
| 370 | */ | |
| 371 | __kernel void count_sections( | |
| 372 | __global volatile double* sec_count, | |
| 373 | __global const uint* sec_indices, | |
| 374 | const uint num_items) | |
| 375 | { | |
| 376 | uint tid = get_global_id(0); | |
| 377 | if (tid >= num_items) return; | |
| 378 | ||
| 379 | uint sidx = sec_indices[tid]; | |
| 380 | if (sidx == HT_EMPTY_VALUE) return; | |
| 381 | ||
| 382 | atomic_add_double(&sec_count[sidx], 1.0); | |
| 383 | } | |
| 384 | ||
| 385 | /* ═══════════════════════════════════════════════════════════════ | |
| 386 | * POOL STATISTICS | |
| 387 | * | |
| 388 | * Read back pool sizes (bump allocator values). | |
| 389 | * ═══════════════════════════════════════════════════════════════ */ | |
| 390 | ||
| 391 | /* | |
| 392 | * Read pool sizes into output array. | |
| 393 | * out_stats[0] = word count | |
| 394 | * out_stats[1] = pair count | |
| 395 | * out_stats[2] = section count | |
| 396 | */ | |
| 397 | __kernel void pool_stats( | |
| 398 | __global const uint* word_next_free, | |
| 399 | __global const uint* pair_next_free, | |
| 400 | __global const uint* sec_next_free, | |
| 401 | __global uint* out_stats) | |
| 402 | { | |
| 403 | if (get_global_id(0) != 0) return; | |
| 404 | out_stats[0] = *word_next_free; | |
| 405 | out_stats[1] = *pair_next_free; | |
| 406 | out_stats[2] = *sec_next_free; | |
| 407 | } |
| … | … | @@ -1,771 +0,0 @@ |
| 1 | /* gpu-compartment-kernel.cu — Hardware-native compartmentalized learning | |
| 2 | * | |
| 3 | * Maps 1 SM = 1 compartment (AtomSpace). Each SM accumulates pair counts | |
| 4 | * independently with ZERO cross-SM atomics. After accumulation, a grid-wide | |
| 5 | * merge phase combines compartment stats into the shared LearningState, | |
| 6 | * then MI/CC/entropy run on merged data. | |
| 7 | * | |
| 8 | * Architecture: | |
| 9 | * Phase A: Snapshot ring buffer (thread 0) | |
| 10 | * Phase B: Per-SM sentence counting (each block processes its stride) | |
| 11 | * Phase C: Merge compartments → shared LearningState (atomics) | |
| 12 | * Phase D: MI computation on merged data (all threads) | |
| 13 | * Phase E: Connected components clustering (all threads) | |
| 14 | * Phase F: Entropy + convergence check (all threads + thread 0) | |
| 15 | * Phase G: Reset compartments for next round (all threads) | |
| 16 | * | |
| 17 | * 7 grid.sync() per iteration — all unconditional. | |
| 18 | * | |
| 19 | * Requires: CUDA cooperative groups (CC 7.5+, RTX 2070) | |
| 20 | * | |
| 21 | * Build: | |
| 22 | * nvcc -O2 -arch=sm_75 -rdc=true -c gpu-compartment-kernel.cu | |
| 23 | */ | |
| 24 | ||
| 25 | #include <cstdio> | |
| 26 | #include <cstring> | |
| 27 | #include <cfloat> | |
| 28 | #include <cmath> | |
| 29 | #include <cooperative_groups.h> | |
| 30 | #include "gpu-learning-types.h" | |
| 31 | ||
| 32 | namespace cg = cooperative_groups; | |
| 33 | ||
| 34 | /* ─── Hash function (same splitmix64 as gpu-learning-loop.cu) ─── */ | |
| 35 | ||
| 36 | __device__ __forceinline__ | |
| 37 | uint64_t ck_hash(uint64_t key) { | |
| 38 | key ^= key >> 30; | |
| 39 | key *= 0xBF58476D1CE4E5B9ULL; | |
| 40 | key ^= key >> 27; | |
| 41 | key *= 0x94D049BB133111EBULL; | |
| 42 | key ^= key >> 31; | |
| 43 | return key; | |
| 44 | } | |
| 45 | ||
| 46 | /* ─── Atomic double add ─── */ | |
| 47 | ||
| 48 | __device__ __forceinline__ | |
| 49 | void ck_atomic_add_double(double* addr, double val) { | |
| 50 | unsigned long long int* a = (unsigned long long int*)addr; | |
| 51 | unsigned long long int old = *a, assumed; | |
| 52 | do { | |
| 53 | assumed = old; | |
| 54 | old = atomicCAS(a, assumed, | |
| 55 | __double_as_longlong(__longlong_as_double(assumed) + val)); | |
| 56 | } while (assumed != old); | |
| 57 | } | |
| 58 | ||
| 59 | /* ═══════════════════════════════════════════════════════════════ | |
| 60 | * COMPARTMENT DEVICE FUNCTIONS | |
| 61 | * | |
| 62 | * These operate on a SINGLE CompartmentState — no atomics needed | |
| 63 | * because only threads within one block write to their compartment. | |
| 64 | * ═══════════════════════════════════════════════════════════════ */ | |
| 65 | ||
| 66 | /* Find or create pair in compartment's local hash table. | |
| 67 | * SINGLE-WRITER: only thread 0 of each block calls this, | |
| 68 | * so no atomicCAS needed — plain reads/writes. */ | |
| 69 | ||
| 70 | __device__ | |
| 71 | uint32_t comp_find_or_create_pair(CompartmentState* comp, | |
| 72 | uint32_t wa, uint32_t wb) { | |
| 73 | uint32_t lo = (wa <= wb) ? wa : wb; | |
| 74 | uint32_t hi = (wa <= wb) ? wb : wa; | |
| 75 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 76 | uint64_t mask = LL_COMP_HT_CAPACITY - 1; | |
| 77 | uint64_t slot = ck_hash(key) & mask; | |
| 78 | ||
| 79 | for (int probe = 0; probe < 4096; probe++) { | |
| 80 | uint64_t existing = comp->pair_ht_keys[slot]; | |
| 81 | ||
| 82 | if (existing == LL_EMPTY_KEY) { | |
| 83 | /* New slot */ | |
| 84 | uint32_t idx = comp->pair_count; | |
| 85 | if (idx >= LL_COMP_MAX_PAIRS) return LL_EMPTY_VALUE; | |
| 86 | comp->pair_count = idx + 1; | |
| 87 | comp->pair_ht_keys[slot] = key; | |
| 88 | comp->pair_ht_values[slot] = idx; | |
| 89 | comp->pair_word_a[idx] = lo; | |
| 90 | comp->pair_word_b[idx] = hi; | |
| 91 | comp->pair_obs[idx] = 0.0; | |
| 92 | return idx; | |
| 93 | } | |
| 94 | if (existing == key) { | |
| 95 | return comp->pair_ht_values[slot]; | |
| 96 | } | |
| 97 | slot = (slot + 1) & mask; | |
| 98 | } | |
| 99 | return LL_EMPTY_VALUE; | |
| 100 | } | |
| 101 | ||
| 102 | /* Count pairs from one sentence into compartment (single-writer) */ | |
| 103 | ||
| 104 | __device__ | |
| 105 | void comp_count_sentence(CompartmentState* comp, | |
| 106 | uint32_t* words, uint32_t len) { | |
| 107 | for (uint32_t i = 0; i < len; i++) { | |
| 108 | for (uint32_t j = i + 1; j < len && j <= i + 2; j++) { | |
| 109 | uint32_t idx = comp_find_or_create_pair(comp, words[i], words[j]); | |
| 110 | if (idx == LL_EMPTY_VALUE) continue; | |
| 111 | ||
| 112 | comp->pair_obs[idx] += 1.0; | |
| 113 | comp->word_marginal[words[i]] += 1.0; | |
| 114 | comp->word_marginal[words[j]] += 1.0; | |
| 115 | comp->total_observations += 1.0; | |
| 116 | } | |
| 117 | } | |
| 118 | comp->sentences_processed++; | |
| 119 | } | |
| 120 | ||
| 121 | /* Merge one compartment's accumulated stats into shared LearningState. | |
| 122 | * ALL threads in the block participate — each thread handles a stride | |
| 123 | * of the compartment's pairs. Uses atomics on the shared state. */ | |
| 124 | ||
| 125 | __device__ | |
| 126 | void comp_merge_into_shared(CompartmentState* comp, | |
| 127 | LearningState* shared, | |
| 128 | uint32_t local_tid, | |
| 129 | uint32_t block_size) { | |
| 130 | uint32_t num_local_pairs = comp->pair_count; | |
| 131 | ||
| 132 | /* Each thread in the block merges a stride of pairs */ | |
| 133 | for (uint32_t p = local_tid; p < num_local_pairs; p += block_size) { | |
| 134 | uint32_t wa = comp->pair_word_a[p]; | |
| 135 | uint32_t wb = comp->pair_word_b[p]; | |
| 136 | double obs = comp->pair_obs[p]; | |
| 137 | if (obs < 1.0) continue; | |
| 138 | ||
| 139 | /* Find or create in shared hash table (uses atomicCAS) */ | |
| 140 | uint64_t key = ((uint64_t)wa << 32) | (uint64_t)wb; | |
| 141 | uint64_t mask = LL_PAIR_HT_CAPACITY - 1; | |
| 142 | uint64_t slot = ck_hash(key) & mask; | |
| 143 | ||
| 144 | uint32_t shared_idx = LL_EMPTY_VALUE; | |
| 145 | for (int probe = 0; probe < 4096; probe++) { | |
| 146 | uint64_t existing = atomicCAS( | |
| 147 | (unsigned long long int*)&shared->pair_ht_keys[slot], | |
| 148 | LL_EMPTY_KEY, key); | |
| 149 | ||
| 150 | if (existing == LL_EMPTY_KEY) { | |
| 151 | /* New pair in shared state */ | |
| 152 | uint32_t idx = atomicAdd(&shared->pair_count_u32, 1U); | |
| 153 | if (idx < LL_MAX_PAIRS) { | |
| 154 | shared->pair_word_a[idx] = wa; | |
| 155 | shared->pair_word_b[idx] = wb; | |
| 156 | shared->pair_count[idx] = 0.0; | |
| 157 | shared->pair_mi[idx] = 0.0; | |
| 158 | shared->pair_dirty[idx] = 0; | |
| 159 | __threadfence(); | |
| 160 | shared->pair_ht_values[slot] = idx; | |
| 161 | shared_idx = idx; | |
| 162 | } | |
| 163 | break; | |
| 164 | } | |
| 165 | if (existing == key) { | |
| 166 | /* Existing pair — wait for value */ | |
| 167 | uint32_t val = shared->pair_ht_values[slot]; | |
| 168 | int safety = 1000; | |
| 169 | while (val == LL_EMPTY_VALUE && safety-- > 0) { | |
| 170 | val = shared->pair_ht_values[slot]; | |
| 171 | } | |
| 172 | shared_idx = val; | |
| 173 | break; | |
| 174 | } | |
| 175 | slot = (slot + 1) & mask; | |
| 176 | } | |
| 177 | ||
| 178 | if (shared_idx != LL_EMPTY_VALUE && shared_idx < LL_MAX_PAIRS) { | |
| 179 | ck_atomic_add_double(&shared->pair_count[shared_idx], obs); | |
| 180 | ||
| 181 | /* Mark dirty for MI recomputation */ | |
| 182 | if (atomicExch(&shared->pair_dirty[shared_idx], 1U) == 0U) { | |
| 183 | atomicAdd(&shared->dirty_count, 1U); | |
| 184 | } | |
| 185 | } | |
| 186 | } | |
| 187 | ||
| 188 | /* Merge marginals — each thread handles a stride of words */ | |
| 189 | uint32_t num_words = shared->word_count_u32; | |
| 190 | for (uint32_t w = local_tid; w < num_words; w += block_size) { | |
| 191 | double m = comp->word_marginal[w]; | |
| 192 | if (m > 0.0) { | |
| 193 | ck_atomic_add_double(&shared->word_marginal[w], m); | |
| 194 | } | |
| 195 | } | |
| 196 | ||
| 197 | /* Thread 0 of each block merges total observations */ | |
| 198 | if (local_tid == 0) { | |
| 199 | ck_atomic_add_double(&shared->total_pair_observations, | |
| 200 | comp->total_observations); | |
| 201 | atomicAdd(&shared->total_sentences, comp->sentences_processed); | |
| 202 | } | |
| 203 | } | |
| 204 | ||
| 205 | /* Clear a compartment for next round (all threads in block participate) */ | |
| 206 | ||
| 207 | __device__ | |
| 208 | void comp_clear(CompartmentState* comp, | |
| 209 | uint32_t num_words, | |
| 210 | uint32_t local_tid, | |
| 211 | uint32_t block_size) { | |
| 212 | /* Clear HT keys */ | |
| 213 | for (uint32_t i = local_tid; i < LL_COMP_HT_CAPACITY; i += block_size) { | |
| 214 | comp->pair_ht_keys[i] = LL_EMPTY_KEY; | |
| 215 | comp->pair_ht_values[i] = LL_EMPTY_VALUE; | |
| 216 | } | |
| 217 | /* Clear marginals */ | |
| 218 | for (uint32_t w = local_tid; w < num_words; w += block_size) { | |
| 219 | comp->word_marginal[w] = 0.0; | |
| 220 | } | |
| 221 | /* Thread 0 resets scalars */ | |
| 222 | if (local_tid == 0) { | |
| 223 | comp->pair_count = 0; | |
| 224 | comp->total_observations = 0.0; | |
| 225 | comp->sentences_processed = 0; | |
| 226 | comp->surprise_score = 0.0; | |
| 227 | } | |
| 228 | } | |
| 229 | ||
| 230 | /* ═══════════════════════════════════════════════════════════════ | |
| 231 | * SHARED DEVICE FUNCTIONS (reused from gpu-learning-loop.cu pattern) | |
| 232 | * ═══════════════════════════════════════════════════════════════ */ | |
| 233 | ||
| 234 | /* MI computation — identical to ll_compute_mi_dirty */ | |
| 235 | ||
| 236 | __device__ | |
| 237 | void ck_compute_mi(LearningState* state, cg::grid_group grid) { | |
| 238 | double n = state->total_pair_observations; | |
| 239 | double log2_factor = 1.4426950408889634; | |
| 240 | double eps = 1e-10; | |
| 241 | ||
| 242 | uint32_t num_pairs = state->pair_count_u32; | |
| 243 | uint32_t tid = grid.thread_rank(); | |
| 244 | uint32_t stride = grid.size(); | |
| 245 | ||
| 246 | for (uint32_t p = tid; p < num_pairs; p += stride) { | |
| 247 | if (state->pair_dirty[p] != 1) continue; | |
| 248 | ||
| 249 | double count = state->pair_count[p]; | |
| 250 | if (count < 1.0) { | |
| 251 | state->pair_mi[p] = 0.0; | |
| 252 | state->pair_dirty[p] = 0; | |
| 253 | continue; | |
| 254 | } | |
| 255 | ||
| 256 | uint32_t wa = state->pair_word_a[p]; | |
| 257 | uint32_t wb = state->pair_word_b[p]; | |
| 258 | double left = fmax(state->word_marginal[wa], eps); | |
| 259 | double right = fmax(state->word_marginal[wb], eps); | |
| 260 | double n_safe = fmax(n, eps); | |
| 261 | ||
| 262 | double mi = (log(count) + log(n_safe) - log(left) - log(right)) * log2_factor; | |
| 263 | state->pair_mi[p] = mi; | |
| 264 | state->pair_dirty[p] = 0; | |
| 265 | } | |
| 266 | ||
| 267 | if (tid == 0) { | |
| 268 | state->dirty_count = 0; | |
| 269 | state->mi_updated = 1; | |
| 270 | } | |
| 271 | } | |
| 272 | ||
| 273 | /* Connected components — identical to ll_connected_components */ | |
| 274 | ||
| 275 | __device__ | |
| 276 | void ck_connected_components(LearningState* state, cg::grid_group grid) { | |
| 277 | uint32_t num_words = state->word_count_u32; | |
| 278 | uint32_t num_pairs = state->pair_count_u32; | |
| 279 | uint32_t tid = grid.thread_rank(); | |
| 280 | uint32_t stride = grid.size(); | |
| 281 | ||
| 282 | if (tid == 0) state->cc_edge_count = 0; | |
| 283 | grid.sync(); | |
| 284 | ||
| 285 | /* Filter high-MI pairs into edge list */ | |
| 286 | for (uint32_t p = tid; p < num_pairs; p += stride) { | |
| 287 | if (state->pair_mi[p] > 2.0 && state->pair_count[p] > 3.0) { | |
| 288 | uint32_t idx = atomicAdd(&state->cc_edge_count, 1U); | |
| 289 | if (idx < LL_CC_MAX_EDGES) { | |
| 290 | state->cc_edge_a[idx] = state->pair_word_a[p]; | |
| 291 | state->cc_edge_b[idx] = state->pair_word_b[p]; | |
| 292 | } | |
| 293 | } | |
| 294 | } | |
| 295 | grid.sync(); | |
| 296 | ||
| 297 | uint32_t num_edges = min(state->cc_edge_count, (uint32_t)LL_CC_MAX_EDGES); | |
| 298 | ||
| 299 | /* Init labels */ | |
| 300 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 301 | state->cc_labels[w] = w; | |
| 302 | } | |
| 303 | grid.sync(); | |
| 304 | ||
| 305 | /* Propagate + compress (fixed iteration count for grid.sync safety) */ | |
| 306 | for (int iter = 0; iter < 32; iter++) { | |
| 307 | if (tid == 0) state->cc_changed = 0; | |
| 308 | grid.sync(); | |
| 309 | ||
| 310 | for (uint32_t e = tid; e < num_edges; e += stride) { | |
| 311 | uint32_t u = state->cc_edge_a[e]; | |
| 312 | uint32_t v = state->cc_edge_b[e]; | |
| 313 | uint32_t lu = state->cc_labels[u]; | |
| 314 | uint32_t lv = state->cc_labels[v]; | |
| 315 | if (lu != lv) { | |
| 316 | uint32_t hi = (lu > lv) ? lu : lv; | |
| 317 | uint32_t lo = (lu > lv) ? lv : lu; | |
| 318 | uint32_t old = atomicMin(&state->cc_labels[hi], lo); | |
| 319 | if (old != lo) state->cc_changed = 1; | |
| 320 | } | |
| 321 | } | |
| 322 | grid.sync(); | |
| 323 | ||
| 324 | /* Compress */ | |
| 325 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 326 | uint32_t l = state->cc_labels[w]; | |
| 327 | while (state->cc_labels[l] != l) l = state->cc_labels[l]; | |
| 328 | state->cc_labels[w] = l; | |
| 329 | } | |
| 330 | grid.sync(); | |
| 331 | ||
| 332 | if (!state->cc_changed) break; | |
| 333 | } | |
| 334 | ||
| 335 | /* Assign class IDs */ | |
| 336 | if (tid == 0) state->num_classes = 0; | |
| 337 | grid.sync(); | |
| 338 | ||
| 339 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 340 | if (state->cc_labels[w] == w) { | |
| 341 | uint32_t cid = atomicAdd(&state->num_classes, 1U); | |
| 342 | state->word_class_id[w] = cid; | |
| 343 | } else { | |
| 344 | state->word_class_id[w] = LL_EMPTY_VALUE; | |
| 345 | } | |
| 346 | } | |
| 347 | grid.sync(); | |
| 348 | ||
| 349 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 350 | if (state->word_class_id[w] == LL_EMPTY_VALUE) { | |
| 351 | uint32_t root = state->cc_labels[w]; | |
| 352 | state->word_class_id[w] = state->word_class_id[root]; | |
| 353 | } | |
| 354 | } | |
| 355 | grid.sync(); | |
| 356 | ||
| 357 | if (tid == 0) { | |
| 358 | state->classes_updated = 1; | |
| 359 | state->cc_runs++; | |
| 360 | } | |
| 361 | } | |
| 362 | ||
| 363 | /* Entropy computation — serial on thread 0. | |
| 364 | * cc_labels is shared with CC (root labels). Parallel atomicAdd on | |
| 365 | * cc_labels has a stale-write race across iterations. Thread-0 serial | |
| 366 | * is correct and fast enough (O(num_words) ≈ 8K, <0.1 ms). */ | |
| 367 | ||
| 368 | __device__ | |
| 369 | double ck_compute_entropy(LearningState* state, cg::grid_group grid) { | |
| 370 | uint32_t tid = grid.thread_rank(); | |
| 371 | uint32_t num_words = state->word_count_u32; | |
| 372 | uint32_t nc = state->num_classes; | |
| 373 | ||
| 374 | if (nc == 0) return 0.0; | |
| 375 | double entropy = 0.0; | |
| 376 | if (tid == 0) { | |
| 377 | double n = (double)num_words; | |
| 378 | ||
| 379 | /* Clear histogram bins */ | |
| 380 | for (uint32_t i = 0; i < nc; i++) | |
| 381 | state->cc_labels[i] = 0; | |
| 382 | ||
| 383 | /* Count class sizes */ | |
| 384 | for (uint32_t w = 0; w < num_words; w++) { | |
| 385 | uint32_t cid = state->word_class_id[w]; | |
| 386 | if (cid < nc) | |
| 387 | state->cc_labels[cid]++; | |
| 388 | } | |
| 389 | ||
| 390 | /* Compute Shannon entropy */ | |
| 391 | for (uint32_t i = 0; i < nc; i++) { | |
| 392 | uint32_t sz = state->cc_labels[i]; | |
| 393 | if (sz > 0) { | |
| 394 | double p = (double)sz / n; | |
| 395 | entropy -= p * log2(p); | |
| 396 | } | |
| 397 | } | |
| 398 | } | |
| 399 | return entropy; | |
| 400 | } | |
| 401 | ||
| 402 | /* ═══════════════════════════════════════════════════════════════ | |
| 403 | * PERSISTENT COMPARTMENT KERNEL | |
| 404 | * | |
| 405 | * Launched once with cudaLaunchCooperativeKernel. | |
| 406 | * gridDim.x = number of SMs = number of compartments. | |
| 407 | * Each block IS a compartment. | |
| 408 | * | |
| 409 | * 7 grid.sync() per iteration (all unconditional). | |
| 410 | * ═══════════════════════════════════════════════════════════════ */ | |
| 411 | ||
| 412 | __global__ void compartment_kernel( | |
| 413 | LearningState* shared, | |
| 414 | CompartmentState* compartments, /* array[gridDim.x] */ | |
| 415 | SentenceRing* ring, | |
| 416 | volatile int* done_flag, | |
| 417 | volatile int* pause_flag, | |
| 418 | volatile uint32_t* stats_iteration, | |
| 419 | volatile uint32_t* stats_pairs, | |
| 420 | volatile uint32_t* stats_classes, | |
| 421 | volatile double* stats_entropy | |
| 422 | ) { | |
| 423 | auto grid = cg::this_grid(); | |
| 424 | uint32_t tid = grid.thread_rank(); | |
| 425 | uint32_t bid = blockIdx.x; | |
| 426 | uint32_t local_tid = threadIdx.x; | |
| 427 | uint32_t block_size = blockDim.x; | |
| 428 | uint32_t num_blocks = gridDim.x; | |
| 429 | ||
| 430 | CompartmentState* my_comp = &compartments[bid]; | |
| 431 | ||
| 432 | while (!(*done_flag)) { | |
| 433 | ||
| 434 | /* ═══ Phase A: Snapshot ring buffer (thread 0 only) ═══ | |
| 435 | * Thread 0 captures how many sentences are available. | |
| 436 | * Cap batch to num_blocks * 16 per iteration so the pipeline | |
| 437 | * runs multiple rounds and the entropy window fills up. | |
| 438 | * This prevents the "consume-all-then-spin" problem. */ | |
| 439 | if (tid == 0 && !(*pause_flag)) { | |
| 440 | uint32_t w = ring->write_idx; | |
| 441 | uint32_t r = ring->read_idx; | |
| 442 | uint32_t avail = (w >= r) ? (w - r) : 0; | |
| 443 | /* Cap: each SM gets ~16 sentences per round */ | |
| 444 | uint32_t max_batch = num_blocks * 16; | |
| 445 | if (avail > max_batch) avail = max_batch; | |
| 446 | shared->batch_start = r; | |
| 447 | shared->batch_size = avail; | |
| 448 | if (avail > 0) { | |
| 449 | ring->read_idx = r + avail; /* consume only the batch */ | |
| 450 | shared->has_new_sentences = 1; | |
| 451 | } | |
| 452 | } | |
| 453 | grid.sync(); /* ── sync 1 ── */ | |
| 454 | ||
| 455 | /* ═══ Phase B: Per-SM sentence counting ═══ | |
| 456 | * Block `bid` processes sentences [bid, bid+num_blocks, bid+2*num_blocks, ...] | |
| 457 | * from the snapshot. Only thread 0 of each block writes to its compartment | |
| 458 | * (single-writer, no atomics needed). */ | |
| 459 | if (shared->has_new_sentences) { | |
| 460 | uint32_t batch_start = shared->batch_start; | |
| 461 | uint32_t batch_size = shared->batch_size; | |
| 462 | ||
| 463 | /* Thread 0 of each block counts sentences for its compartment */ | |
| 464 | if (local_tid == 0) { | |
| 465 | for (uint32_t s = bid; s < batch_size; s += num_blocks) { | |
| 466 | uint32_t ridx = (batch_start + s) % LL_RING_SIZE; | |
| 467 | SentenceSlot* slot = &ring->slots[ridx]; | |
| 468 | ||
| 469 | if (slot->ready && slot->length >= 2) { | |
| 470 | comp_count_sentence(my_comp, slot->words, slot->length); | |
| 471 | slot->ready = 0; | |
| 472 | } | |
| 473 | } | |
| 474 | } | |
| 475 | } | |
| 476 | grid.sync(); /* ── sync 2 ── */ | |
| 477 | ||
| 478 | /* ═══ Phase C: Merge compartments → shared state ═══ | |
| 479 | * All threads in each block participate in merging their | |
| 480 | * compartment's accumulated stats into the shared LearningState. | |
| 481 | * This phase uses atomics on shared state. */ | |
| 482 | if (shared->has_new_sentences && my_comp->pair_count > 0) { | |
| 483 | comp_merge_into_shared(my_comp, shared, local_tid, block_size); | |
| 484 | } | |
| 485 | grid.sync(); /* ── sync 3 ── */ | |
| 486 | ||
| 487 | /* ═══ Phase D: MI computation on merged data ═══ | |
| 488 | * All threads participate, operating on shared LearningState. | |
| 489 | * Flush remaining dirty pairs even below threshold when idle. */ | |
| 490 | int do_mi = (shared->dirty_count >= LL_MI_THRESHOLD) || | |
| 491 | (shared->dirty_count > 0 && !shared->has_new_sentences); | |
| 492 | if (do_mi) { | |
| 493 | ck_compute_mi(shared, grid); | |
| 494 | } | |
| 495 | grid.sync(); /* ── sync 4 ── */ | |
| 496 | ||
| 497 | /* ═══ Phase E: Connected components clustering ═══ | |
| 498 | * Run CC when MI freshly updated, OR on idle rounds to fill | |
| 499 | * the entropy window (up to ENTROPY_WINDOW+2 extra rounds). | |
| 500 | * CC is idempotent — same data gives same classes. */ | |
| 501 | int had_new_data = shared->has_new_sentences; | |
| 502 | int need_fill = (!had_new_data && | |
| 503 | shared->total_sentences > 0 && | |
| 504 | shared->pair_count_u32 > 10 && | |
| 505 | shared->entropy_idx < (uint32_t)(LL_ENTROPY_WINDOW + 2)); | |
| 506 | int do_cc = (shared->mi_updated && shared->pair_count_u32 > 10) | |
| 507 | || need_fill; | |
| 508 | if (do_cc) { | |
| 509 | ck_connected_components(shared, grid); | |
| 510 | } | |
| 511 | grid.sync(); /* ── sync 5 ── */ | |
| 512 | ||
| 513 | /* ═══ Phase F: Entropy + convergence check ═══ */ | |
| 514 | double entropy = 0.0; | |
| 515 | if (do_cc) { | |
| 516 | entropy = ck_compute_entropy(shared, grid); | |
| 517 | } | |
| 518 | grid.sync(); /* ── sync 6 ── */ | |
| 519 | ||
| 520 | /* Thread 0: update stats, check phase-gated convergence */ | |
| 521 | if (tid == 0) { | |
| 522 | if (do_cc) { | |
| 523 | uint32_t eidx = shared->entropy_idx % LL_ENTROPY_WINDOW; | |
| 524 | shared->entropy_history[eidx] = entropy; | |
| 525 | shared->entropy_idx++; | |
| 526 | ||
| 527 | *stats_pairs = shared->pair_count_u32; | |
| 528 | *stats_classes = shared->num_classes; | |
| 529 | *stats_entropy = entropy; | |
| 530 | ||
| 531 | /* Only count data rounds as iterations */ | |
| 532 | if (had_new_data) { | |
| 533 | shared->iteration++; | |
| 534 | *stats_iteration = shared->iteration; | |
| 535 | } | |
| 536 | shared->mi_updated = 0; | |
| 537 | } | |
| 538 | ||
| 539 | /* Track idle rounds (no new data) */ | |
| 540 | if (!had_new_data) { | |
| 541 | shared->idle_rounds++; | |
| 542 | } else { | |
| 543 | shared->idle_rounds = 0; | |
| 544 | } | |
| 545 | ||
| 546 | /* Convergence check runs EVERY iteration. | |
| 547 | * Two convergence paths: | |
| 548 | * 1. Phase-gated: all gates pass + entropy plateau | |
| 549 | * 2. Data-exhausted: no new data for 50+ rounds + entropy stable | |
| 550 | * Path 2 prevents infinite spinning when data runs out. */ | |
| 551 | if (shared->entropy_idx >= LL_ENTROPY_WINDOW) { | |
| 552 | double min_e = 1e30, max_e = -1e30; | |
| 553 | for (int i = 0; i < LL_ENTROPY_WINDOW; i++) { | |
| 554 | double e = shared->entropy_history[i]; | |
| 555 | if (e < min_e) min_e = e; | |
| 556 | if (e > max_e) max_e = e; | |
| 557 | } | |
| 558 | int entropy_stable = (max_e > 0.0 && | |
| 559 | (max_e - min_e) / max_e < 0.01); | |
| 560 | ||
| 561 | /* Path 1: Full convergence (meaningful clustering) */ | |
| 562 | int gate_sentences = (shared->total_sentences >= LL_MIN_SENTENCES); | |
| 563 | int gate_cc_runs = (shared->cc_runs >= LL_MIN_CC_RUNS); | |
| 564 | int gate_classes = (shared->num_classes >= LL_MIN_CLASSES); | |
| 565 | int gate_nontrivial = (shared->num_classes < | |
| 566 | shared->word_count_u32 * 95 / 100); | |
| 567 | ||
| 568 | if (entropy_stable && gate_sentences && gate_cc_runs && | |
| 569 | gate_classes && gate_nontrivial) { | |
| 570 | *done_flag = 1; | |
| 571 | } | |
| 572 | ||
| 573 | /* Path 2: Data exhausted (no more info to extract) */ | |
| 574 | if (entropy_stable && shared->idle_rounds > 50 && | |
| 575 | shared->total_sentences > 0) { | |
| 576 | *done_flag = 1; | |
| 577 | } | |
| 578 | } | |
| 579 | ||
| 580 | if (shared->iteration >= LL_MAX_ITERATIONS) { | |
| 581 | *done_flag = 1; | |
| 582 | } | |
| 583 | ||
| 584 | shared->has_new_sentences = 0; | |
| 585 | } | |
| 586 | ||
| 587 | /* ═══ Phase G: Reset compartments for next round ═══ */ | |
| 588 | comp_clear(my_comp, shared->word_count_u32, local_tid, block_size); | |
| 589 | grid.sync(); /* ── sync 7 ── */ | |
| 590 | } | |
| 591 | } | |
| 592 | ||
| 593 | /* ═══════════════════════════════════════════════════════════════ | |
| 594 | * HOST API | |
| 595 | * ═══════════════════════════════════════════════════════════════ */ | |
| 596 | ||
| 597 | extern "C" { | |
| 598 | ||
| 599 | /* Initialize compartment-aware learning state */ | |
| 600 | LearningState* ck_init(uint32_t num_words) { | |
| 601 | LearningState* state; | |
| 602 | cudaMallocManaged(&state, sizeof(LearningState)); | |
| 603 | memset(state, 0, sizeof(LearningState)); | |
| 604 | ||
| 605 | state->word_count_u32 = num_words; | |
| 606 | state->pair_count_u32 = 0; | |
| 607 | state->dirty_count = 0; | |
| 608 | state->total_pair_observations = 0.0; | |
| 609 | state->num_classes = 0; | |
| 610 | state->iteration = 0; | |
| 611 | state->entropy_idx = 0; | |
| 612 | state->cc_edge_count = 0; | |
| 613 | state->total_sentences = 0; | |
| 614 | state->cc_runs = 0; | |
| 615 | state->batch_size = 0; | |
| 616 | state->batch_start = 0; | |
| 617 | state->idle_rounds = 0; | |
| 618 | ||
| 619 | /* Init hash table */ | |
| 620 | memset(state->pair_ht_keys, 0xFF, sizeof(state->pair_ht_keys)); | |
| 621 | memset(state->pair_ht_values, 0xFF, sizeof(state->pair_ht_values)); | |
| 622 | ||
| 623 | /* Init word data */ | |
| 624 | for (uint32_t i = 0; i < num_words; i++) { | |
| 625 | state->word_marginal[i] = 0.0; | |
| 626 | state->word_class_id[i] = LL_EMPTY_VALUE; | |
| 627 | } | |
| 628 | ||
| 629 | return state; | |
| 630 | } | |
| 631 | ||
| 632 | /* Initialize compartment array — one per SM */ | |
| 633 | CompartmentState* ck_init_compartments(uint32_t num_compartments, | |
| 634 | uint32_t num_words) { | |
| 635 | CompartmentState* comps; | |
| 636 | cudaMallocManaged(&comps, num_compartments * sizeof(CompartmentState)); | |
| 637 | ||
| 638 | for (uint32_t c = 0; c < num_compartments; c++) { | |
| 639 | comps[c].pair_count = 0; | |
| 640 | comps[c].total_observations = 0.0; | |
| 641 | comps[c].sentences_processed = 0; | |
| 642 | comps[c].surprise_score = 0.0; | |
| 643 | ||
| 644 | memset(comps[c].pair_ht_keys, 0xFF, sizeof(comps[c].pair_ht_keys)); | |
| 645 | memset(comps[c].pair_ht_values, 0xFF, sizeof(comps[c].pair_ht_values)); | |
| 646 | ||
| 647 | for (uint32_t w = 0; w < num_words; w++) { | |
| 648 | comps[c].word_marginal[w] = 0.0; | |
| 649 | } | |
| 650 | } | |
| 651 | ||
| 652 | return comps; | |
| 653 | } | |
| 654 | ||
| 655 | /* Initialize sentence ring buffer (larger for compartment kernel) */ | |
| 656 | SentenceRing* ck_init_ring() { | |
| 657 | SentenceRing* ring; | |
| 658 | cudaMallocManaged(&ring, sizeof(SentenceRing)); | |
| 659 | memset(ring, 0, sizeof(SentenceRing)); | |
| 660 | return ring; | |
| 661 | } | |
| 662 | ||
| 663 | /* Feed a sentence (CPU writes to unified memory) */ | |
| 664 | void ck_feed_sentence(SentenceRing* ring, uint32_t* words, uint32_t length) { | |
| 665 | if (length > LL_MAX_SENTENCE_LEN) length = LL_MAX_SENTENCE_LEN; | |
| 666 | ||
| 667 | uint32_t widx = ring->write_idx % LL_RING_SIZE; | |
| 668 | SentenceSlot* slot = &ring->slots[widx]; | |
| 669 | ||
| 670 | /* Wait for slot to be consumed (with timeout) */ | |
| 671 | int wait = 0; | |
| 672 | while (slot->ready && wait < 1000000) { wait++; } | |
| 673 | ||
| 674 | memcpy(slot->words, words, length * sizeof(uint32_t)); | |
| 675 | slot->length = length; | |
| 676 | __sync_synchronize(); /* memory fence */ | |
| 677 | slot->ready = 1; | |
| 678 | ring->write_idx++; | |
| 679 | } | |
| 680 | ||
| 681 | /* Launch the compartment kernel */ | |
| 682 | int ck_launch( | |
| 683 | LearningState* state, | |
| 684 | CompartmentState* compartments, | |
| 685 | SentenceRing* ring, | |
| 686 | int* done_flag, | |
| 687 | int* pause_flag, | |
| 688 | uint32_t* stats_iteration, | |
| 689 | uint32_t* stats_pairs, | |
| 690 | uint32_t* stats_classes, | |
| 691 | double* stats_entropy | |
| 692 | ) { | |
| 693 | /* Query SM count */ | |
| 694 | int num_sms = 0; | |
| 695 | cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0); | |
| 696 | ||
| 697 | /* 1 block per SM, 128 threads per block */ | |
| 698 | int total_blocks = num_sms; | |
| 699 | int block_size = 128; | |
| 700 | ||
| 701 | state->num_compartments = (uint32_t)total_blocks; | |
| 702 | ||
| 703 | dim3 grid_dim(total_blocks); | |
| 704 | dim3 block_dim(block_size); | |
| 705 | ||
| 706 | void* args[] = { | |
| 707 | &state, &compartments, &ring, &done_flag, &pause_flag, | |
| 708 | &stats_iteration, &stats_pairs, &stats_classes, &stats_entropy | |
| 709 | }; | |
| 710 | ||
| 711 | cudaError_t err = cudaLaunchCooperativeKernel( | |
| 712 | (void*)compartment_kernel, grid_dim, block_dim, args); | |
| 713 | ||
| 714 | if (err != cudaSuccess) { | |
| 715 | printf("Compartment kernel launch failed: %s\n", | |
| 716 | cudaGetErrorString(err)); | |
| 717 | return -1; | |
| 718 | } | |
| 719 | ||
| 720 | return 0; | |
| 721 | } | |
| 722 | ||
| 723 | /* Wait for kernel completion */ | |
| 724 | void ck_wait() { | |
| 725 | cudaDeviceSynchronize(); | |
| 726 | } | |
| 727 | ||
| 728 | /* Read class assignments */ | |
| 729 | void ck_read_classes(LearningState* state, uint32_t* out, uint32_t n) { | |
| 730 | cudaDeviceSynchronize(); | |
| 731 | memcpy(out, state->word_class_id, n * sizeof(uint32_t)); | |
| 732 | } | |
| 733 | ||
| 734 | /* Cleanup */ | |
| 735 | void ck_shutdown(LearningState* state, CompartmentState* comps, | |
| 736 | SentenceRing* ring) { | |
| 737 | cudaFree(state); | |
| 738 | cudaFree(comps); | |
| 739 | cudaFree(ring); | |
| 740 | } | |
| 741 | ||
| 742 | /* Query number of SMs (for allocating compartments) */ | |
| 743 | int ck_get_num_sms() { | |
| 744 | int num_sms = 0; | |
| 745 | cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0); | |
| 746 | return num_sms; | |
| 747 | } | |
| 748 | ||
| 749 | /* Memory report */ | |
| 750 | void ck_memory_report(int num_compartments) { | |
| 751 | size_t state_sz = sizeof(LearningState); | |
| 752 | size_t comp_sz = sizeof(CompartmentState); | |
| 753 | size_t ring_sz = sizeof(SentenceRing); | |
| 754 | size_t total = state_sz + comp_sz * num_compartments + ring_sz; | |
| 755 | ||
| 756 | size_t free_mem, total_mem; | |
| 757 | cudaMemGetInfo(&free_mem, &total_mem); | |
| 758 | ||
| 759 | printf("Memory report:\n"); | |
| 760 | printf(" LearningState: %8.2f MB\n", state_sz / (1024.0 * 1024.0)); | |
| 761 | printf(" CompartmentState: %8.2f MB (x%d = %.2f MB)\n", | |
| 762 | comp_sz / (1024.0 * 1024.0), num_compartments, | |
| 763 | comp_sz * num_compartments / (1024.0 * 1024.0)); | |
| 764 | printf(" SentenceRing: %8.2f MB (%d slots)\n", | |
| 765 | ring_sz / (1024.0 * 1024.0), LL_RING_SIZE); | |
| 766 | printf(" Total allocated: %8.2f MB\n", total / (1024.0 * 1024.0)); | |
| 767 | printf(" GPU free: %8.2f MB / %.2f MB\n", | |
| 768 | free_mem / (1024.0 * 1024.0), total_mem / (1024.0 * 1024.0)); | |
| 769 | } | |
| 770 | ||
| 771 | } /* extern "C" */ |
| … | … | @@ -1,398 +0,0 @@ |
| 1 | /* gpu-connected-components.cu — Shiloach-Vishkin parallel connected components | |
| 2 | * | |
| 3 | * Replaces CPU agglomerative clustering with GPU label propagation on the | |
| 4 | * cosine similarity graph. Threshold sweep with knee detection finds | |
| 5 | * natural word classes. | |
| 6 | * | |
| 7 | * Data layout matches existing OpenCL SoA pools: | |
| 8 | * - Candidates from gpu-cosine.cl filter_candidates output | |
| 9 | * - Word pool indices for labels/classes | |
| 10 | * - Pair key encoding: ((ulong)lo << 32) | (ulong)hi | |
| 11 | * | |
| 12 | * Build: nvcc -O2 -arch=sm_75 -rdc=true -c gpu-connected-components.cu | |
| 13 | */ | |
| 14 | ||
| 15 | #include <cstdint> | |
| 16 | #include <cstdio> | |
| 17 | #include <cfloat> | |
| 18 | ||
| 19 | /* ─── Constants ─── */ | |
| 20 | ||
| 21 | #ifndef CC_MAX_WORDS | |
| 22 | #define CC_MAX_WORDS 131072 /* matches WORD_CAPACITY */ | |
| 23 | #endif | |
| 24 | ||
| 25 | #ifndef CC_MAX_EDGES | |
| 26 | #define CC_MAX_EDGES 4194304 /* max edges after filtering */ | |
| 27 | #endif | |
| 28 | ||
| 29 | #ifndef CC_MAX_ITERATIONS | |
| 30 | #define CC_MAX_ITERATIONS 64 /* O(log n) convergence, 64 is generous */ | |
| 31 | #endif | |
| 32 | ||
| 33 | /* Sentinel matching OpenCL conventions */ | |
| 34 | #define CC_NO_CLASS 0xFFFFFFFFU | |
| 35 | ||
| 36 | /* ─── Device data structures (SoA) ─── */ | |
| 37 | ||
| 38 | struct CCEdgeList { | |
| 39 | uint32_t* edge_a; /* word index (lo) */ | |
| 40 | uint32_t* edge_b; /* word index (hi) */ | |
| 41 | float* edge_weight; /* cosine similarity */ | |
| 42 | uint32_t* edge_count; /* atomic counter: number of edges */ | |
| 43 | uint32_t capacity; /* CC_MAX_EDGES */ | |
| 44 | }; | |
| 45 | ||
| 46 | struct CCLabels { | |
| 47 | uint32_t* label; /* label[word_idx] = component root */ | |
| 48 | uint32_t num_words; /* active word count */ | |
| 49 | }; | |
| 50 | ||
| 51 | struct CCResult { | |
| 52 | uint32_t* class_id; /* class_id[word_idx] = sequential class id */ | |
| 53 | uint32_t* component_count; /* number of components found */ | |
| 54 | uint32_t* class_sizes; /* size of each class (indexed by class_id) */ | |
| 55 | uint32_t max_classes; /* max distinct classes */ | |
| 56 | }; | |
| 57 | ||
| 58 | /* ─── Kernel 1: Build edge list from cosine candidates ─── */ | |
| 59 | ||
| 60 | __global__ void cc_build_edge_list( | |
| 61 | /* Cosine filter output (from gpu-cosine.cl step 5) */ | |
| 62 | const uint32_t* __restrict__ cand_word_a, | |
| 63 | const uint32_t* __restrict__ cand_word_b, | |
| 64 | const double* __restrict__ cand_cosine, | |
| 65 | uint32_t num_candidates, | |
| 66 | /* Threshold */ | |
| 67 | float min_cosine, | |
| 68 | /* Output edge list */ | |
| 69 | uint32_t* edge_a, | |
| 70 | uint32_t* edge_b, | |
| 71 | float* edge_weight, | |
| 72 | uint32_t* edge_count, | |
| 73 | uint32_t edge_capacity | |
| 74 | ) { | |
| 75 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 76 | if (tid >= num_candidates) return; | |
| 77 | ||
| 78 | float cos_val = (float)cand_cosine[tid]; | |
| 79 | if (cos_val < min_cosine) return; | |
| 80 | ||
| 81 | uint32_t idx = atomicAdd(edge_count, 1U); | |
| 82 | if (idx >= edge_capacity) return; /* safety: don't overflow */ | |
| 83 | ||
| 84 | edge_a[idx] = cand_word_a[tid]; | |
| 85 | edge_b[idx] = cand_word_b[tid]; | |
| 86 | edge_weight[idx] = cos_val; | |
| 87 | } | |
| 88 | ||
| 89 | /* ─── Kernel 2: Initialize labels ─── */ | |
| 90 | ||
| 91 | __global__ void cc_init_labels( | |
| 92 | uint32_t* label, | |
| 93 | uint32_t num_words | |
| 94 | ) { | |
| 95 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 96 | if (tid >= num_words) return; | |
| 97 | label[tid] = tid; /* each word is its own component */ | |
| 98 | } | |
| 99 | ||
| 100 | /* ─── Kernel 3: Label propagation (hook) ─── */ | |
| 101 | /* For each edge (u,v): if labels differ, point max to min via atomicMin. | |
| 102 | * This is the "hook" step of Shiloach-Vishkin. */ | |
| 103 | ||
| 104 | __global__ void cc_propagate( | |
| 105 | const uint32_t* __restrict__ edge_a, | |
| 106 | const uint32_t* __restrict__ edge_b, | |
| 107 | uint32_t num_edges, | |
| 108 | uint32_t* label, | |
| 109 | int* changed /* set to 1 if any label changed */ | |
| 110 | ) { | |
| 111 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 112 | if (tid >= num_edges) return; | |
| 113 | ||
| 114 | uint32_t u = edge_a[tid]; | |
| 115 | uint32_t v = edge_b[tid]; | |
| 116 | ||
| 117 | uint32_t lu = label[u]; | |
| 118 | uint32_t lv = label[v]; | |
| 119 | ||
| 120 | if (lu == lv) return; | |
| 121 | ||
| 122 | /* Hook: point the root of the larger label to the root of the smaller */ | |
| 123 | uint32_t hi = (lu > lv) ? lu : lv; | |
| 124 | uint32_t lo = (lu > lv) ? lv : lu; | |
| 125 | ||
| 126 | uint32_t old = atomicMin(&label[hi], lo); | |
| 127 | if (old != lo) { | |
| 128 | *changed = 1; | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | /* ─── Kernel 4: Pointer jumping (compress) ─── */ | |
| 133 | /* Follow label chains to root: label[i] = label[label[i]] until fixed point. | |
| 134 | * This is the "compress" step — flattens the tree. */ | |
| 135 | ||
| 136 | __global__ void cc_compress( | |
| 137 | uint32_t* label, | |
| 138 | uint32_t num_words | |
| 139 | ) { | |
| 140 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 141 | if (tid >= num_words) return; | |
| 142 | ||
| 143 | uint32_t l = label[tid]; | |
| 144 | /* Path compression: follow chain to root */ | |
| 145 | while (label[l] != l) { | |
| 146 | l = label[l]; | |
| 147 | } | |
| 148 | label[tid] = l; | |
| 149 | } | |
| 150 | ||
| 151 | /* ─── Kernel 5: Count components ─── */ | |
| 152 | /* Each unique root label = one component. Count via atomicAdd on a flag array. */ | |
| 153 | ||
| 154 | __global__ void cc_count_components( | |
| 155 | const uint32_t* __restrict__ label, | |
| 156 | uint32_t num_words, | |
| 157 | uint32_t* component_flags, /* must be zeroed: size = num_words */ | |
| 158 | uint32_t* component_count /* output: number of distinct components */ | |
| 159 | ) { | |
| 160 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 161 | if (tid >= num_words) return; | |
| 162 | ||
| 163 | uint32_t root = label[tid]; | |
| 164 | /* Atomic flag: only first thread to mark this root counts it */ | |
| 165 | uint32_t old = atomicExch(&component_flags[root], 1U); | |
| 166 | if (old == 0U) { | |
| 167 | atomicAdd(component_count, 1U); | |
| 168 | } | |
| 169 | } | |
| 170 | ||
| 171 | /* ─── Kernel 6: Extract classes ─── */ | |
| 172 | /* Convert root labels to sequential class IDs and compute class sizes. | |
| 173 | * Two-pass: first assign class IDs to roots, then map all words. */ | |
| 174 | ||
| 175 | __global__ void cc_assign_root_class_ids( | |
| 176 | const uint32_t* __restrict__ label, | |
| 177 | uint32_t num_words, | |
| 178 | uint32_t* class_id, /* output: class_id[word] */ | |
| 179 | uint32_t* next_class_id /* atomic counter, starts at 0 */ | |
| 180 | ) { | |
| 181 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 182 | if (tid >= num_words) return; | |
| 183 | ||
| 184 | /* Only roots assign themselves a new class ID */ | |
| 185 | if (label[tid] == tid) { | |
| 186 | uint32_t cid = atomicAdd(next_class_id, 1U); | |
| 187 | class_id[tid] = cid; | |
| 188 | } else { | |
| 189 | class_id[tid] = CC_NO_CLASS; /* will be filled in second pass */ | |
| 190 | } | |
| 191 | } | |
| 192 | ||
| 193 | __global__ void cc_map_to_class_ids( | |
| 194 | const uint32_t* __restrict__ label, | |
| 195 | uint32_t num_words, | |
| 196 | uint32_t* class_id /* in/out: roots have IDs, non-roots get mapped */ | |
| 197 | ) { | |
| 198 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 199 | if (tid >= num_words) return; | |
| 200 | ||
| 201 | if (class_id[tid] == CC_NO_CLASS) { | |
| 202 | /* Follow to root, copy its class ID */ | |
| 203 | uint32_t root = label[tid]; | |
| 204 | class_id[tid] = class_id[root]; | |
| 205 | } | |
| 206 | } | |
| 207 | ||
| 208 | __global__ void cc_compute_class_sizes( | |
| 209 | const uint32_t* __restrict__ class_id, | |
| 210 | uint32_t num_words, | |
| 211 | uint32_t* class_sizes /* output: size of each class, zeroed initially */ | |
| 212 | ) { | |
| 213 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 214 | if (tid >= num_words) return; | |
| 215 | ||
| 216 | uint32_t cid = class_id[tid]; | |
| 217 | if (cid != CC_NO_CLASS) { | |
| 218 | atomicAdd(&class_sizes[cid], 1U); | |
| 219 | } | |
| 220 | } | |
| 221 | ||
| 222 | /* ─── Host API: Run full connected components ─── */ | |
| 223 | ||
| 224 | /* Run CC at a single threshold. Returns number of components found. | |
| 225 | * All buffers must be pre-allocated on device. */ | |
| 226 | extern "C" | |
| 227 | int cc_run( | |
| 228 | /* Cosine candidates (GPU buffers from filter_candidates) */ | |
| 229 | const uint32_t* d_cand_word_a, | |
| 230 | const uint32_t* d_cand_word_b, | |
| 231 | const double* d_cand_cosine, | |
| 232 | uint32_t num_candidates, | |
| 233 | uint32_t num_words, | |
| 234 | float threshold, | |
| 235 | /* Pre-allocated GPU buffers */ | |
| 236 | uint32_t* d_edge_a, | |
| 237 | uint32_t* d_edge_b, | |
| 238 | float* d_edge_weight, | |
| 239 | uint32_t* d_edge_count, /* single uint32, zeroed before call */ | |
| 240 | uint32_t edge_capacity, | |
| 241 | uint32_t* d_label, | |
| 242 | int* d_changed, /* single int */ | |
| 243 | uint32_t* d_component_flags, /* size = num_words, zeroed */ | |
| 244 | uint32_t* d_component_count, /* single uint32, zeroed */ | |
| 245 | uint32_t* d_class_id, | |
| 246 | uint32_t* d_next_class_id, /* single uint32, zeroed */ | |
| 247 | uint32_t* d_class_sizes, /* size = num_words, zeroed */ | |
| 248 | /* Output (host) */ | |
| 249 | uint32_t* h_num_components, | |
| 250 | uint32_t* h_num_edges | |
| 251 | ) { | |
| 252 | int threads = 256; | |
| 253 | ||
| 254 | /* Step 1: Build edge list from candidates above threshold */ | |
| 255 | cudaMemset(d_edge_count, 0, sizeof(uint32_t)); | |
| 256 | int blocks_cand = (num_candidates + threads - 1) / threads; | |
| 257 | if (blocks_cand > 0) { | |
| 258 | cc_build_edge_list<<<blocks_cand, threads>>>( | |
| 259 | d_cand_word_a, d_cand_word_b, d_cand_cosine, | |
| 260 | num_candidates, threshold, | |
| 261 | d_edge_a, d_edge_b, d_edge_weight, | |
| 262 | d_edge_count, edge_capacity); | |
| 263 | } | |
| 264 | cudaDeviceSynchronize(); | |
| 265 | ||
| 266 | uint32_t num_edges; | |
| 267 | cudaMemcpy(&num_edges, d_edge_count, sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 268 | if (h_num_edges) *h_num_edges = num_edges; | |
| 269 | ||
| 270 | if (num_edges == 0) { | |
| 271 | /* No edges above threshold → every word is its own component */ | |
| 272 | if (h_num_components) *h_num_components = num_words; | |
| 273 | return (int)num_words; | |
| 274 | } | |
| 275 | ||
| 276 | /* Step 2: Init labels */ | |
| 277 | int blocks_words = (num_words + threads - 1) / threads; | |
| 278 | cc_init_labels<<<blocks_words, threads>>>(d_label, num_words); | |
| 279 | cudaDeviceSynchronize(); | |
| 280 | ||
| 281 | /* Step 3: Iterate propagate + compress until convergence */ | |
| 282 | int blocks_edges = (num_edges + threads - 1) / threads; | |
| 283 | for (int iter = 0; iter < CC_MAX_ITERATIONS; iter++) { | |
| 284 | int h_changed = 0; | |
| 285 | cudaMemcpy(d_changed, &h_changed, sizeof(int), cudaMemcpyHostToDevice); | |
| 286 | ||
| 287 | cc_propagate<<<blocks_edges, threads>>>( | |
| 288 | d_edge_a, d_edge_b, num_edges, d_label, d_changed); | |
| 289 | cudaDeviceSynchronize(); | |
| 290 | ||
| 291 | cc_compress<<<blocks_words, threads>>>(d_label, num_words); | |
| 292 | cudaDeviceSynchronize(); | |
| 293 | ||
| 294 | cudaMemcpy(&h_changed, d_changed, sizeof(int), cudaMemcpyDeviceToHost); | |
| 295 | if (!h_changed) break; | |
| 296 | } | |
| 297 | ||
| 298 | /* Step 4: Count components */ | |
| 299 | cudaMemset(d_component_flags, 0, num_words * sizeof(uint32_t)); | |
| 300 | cudaMemset(d_component_count, 0, sizeof(uint32_t)); | |
| 301 | cc_count_components<<<blocks_words, threads>>>( | |
| 302 | d_label, num_words, d_component_flags, d_component_count); | |
| 303 | cudaDeviceSynchronize(); | |
| 304 | ||
| 305 | uint32_t num_components; | |
| 306 | cudaMemcpy(&num_components, d_component_count, sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 307 | ||
| 308 | /* Step 5: Extract class IDs */ | |
| 309 | cudaMemset(d_next_class_id, 0, sizeof(uint32_t)); | |
| 310 | cc_assign_root_class_ids<<<blocks_words, threads>>>( | |
| 311 | d_label, num_words, d_class_id, d_next_class_id); | |
| 312 | cudaDeviceSynchronize(); | |
| 313 | ||
| 314 | cc_map_to_class_ids<<<blocks_words, threads>>>( | |
| 315 | d_label, num_words, d_class_id); | |
| 316 | cudaDeviceSynchronize(); | |
| 317 | ||
| 318 | /* Step 6: Class sizes */ | |
| 319 | cudaMemset(d_class_sizes, 0, num_words * sizeof(uint32_t)); | |
| 320 | cc_compute_class_sizes<<<blocks_words, threads>>>( | |
| 321 | d_class_id, num_words, d_class_sizes); | |
| 322 | cudaDeviceSynchronize(); | |
| 323 | ||
| 324 | if (h_num_components) *h_num_components = num_components; | |
| 325 | return (int)num_components; | |
| 326 | } | |
| 327 | ||
| 328 | /* ─── Host API: Threshold sweep with knee detection ─── */ | |
| 329 | ||
| 330 | /* Try CC at decreasing thresholds. Detect knee: sharpest drop in | |
| 331 | * component count relative to threshold step. | |
| 332 | * Returns best threshold (at the knee). */ | |
| 333 | extern "C" | |
| 334 | float cc_threshold_sweep( | |
| 335 | const uint32_t* d_cand_word_a, | |
| 336 | const uint32_t* d_cand_word_b, | |
| 337 | const double* d_cand_cosine, | |
| 338 | uint32_t num_candidates, | |
| 339 | uint32_t num_words, | |
| 340 | /* Sweep parameters */ | |
| 341 | const float* thresholds, /* host array of thresholds to try */ | |
| 342 | int num_thresholds, | |
| 343 | /* Pre-allocated GPU work buffers */ | |
| 344 | uint32_t* d_edge_a, | |
| 345 | uint32_t* d_edge_b, | |
| 346 | float* d_edge_weight, | |
| 347 | uint32_t* d_edge_count, | |
| 348 | uint32_t edge_capacity, | |
| 349 | uint32_t* d_label, | |
| 350 | int* d_changed, | |
| 351 | uint32_t* d_component_flags, | |
| 352 | uint32_t* d_component_count, | |
| 353 | uint32_t* d_class_id, | |
| 354 | uint32_t* d_next_class_id, | |
| 355 | uint32_t* d_class_sizes, | |
| 356 | /* Output: component counts per threshold (host) */ | |
| 357 | uint32_t* h_component_counts, | |
| 358 | /* Output: best threshold */ | |
| 359 | float* h_best_threshold | |
| 360 | ) { | |
| 361 | if (num_thresholds < 2) { | |
| 362 | if (h_best_threshold) *h_best_threshold = thresholds[0]; | |
| 363 | return thresholds[0]; | |
| 364 | } | |
| 365 | ||
| 366 | /* Run CC at each threshold */ | |
| 367 | for (int i = 0; i < num_thresholds; i++) { | |
| 368 | uint32_t nc = 0, ne = 0; | |
| 369 | cc_run( | |
| 370 | d_cand_word_a, d_cand_word_b, d_cand_cosine, | |
| 371 | num_candidates, num_words, thresholds[i], | |
| 372 | d_edge_a, d_edge_b, d_edge_weight, | |
| 373 | d_edge_count, edge_capacity, | |
| 374 | d_label, d_changed, | |
| 375 | d_component_flags, d_component_count, | |
| 376 | d_class_id, d_next_class_id, d_class_sizes, | |
| 377 | &nc, &ne); | |
| 378 | h_component_counts[i] = nc; | |
| 379 | } | |
| 380 | ||
| 381 | /* Find knee: largest drop in component count between consecutive thresholds. | |
| 382 | * Thresholds should be sorted descending (high → low). | |
| 383 | * As threshold decreases, more edges connect, fewer components remain. | |
| 384 | * The knee is where the steepest drop occurs. */ | |
| 385 | float best_threshold = thresholds[0]; | |
| 386 | int max_drop = 0; | |
| 387 | for (int i = 0; i < num_thresholds - 1; i++) { | |
| 388 | int drop = (int)h_component_counts[i] - (int)h_component_counts[i + 1]; | |
| 389 | if (drop > max_drop) { | |
| 390 | max_drop = drop; | |
| 391 | /* Use the higher threshold (just before the big merge) */ | |
| 392 | best_threshold = thresholds[i]; | |
| 393 | } | |
| 394 | } | |
| 395 | ||
| 396 | if (h_best_threshold) *h_best_threshold = best_threshold; | |
| 397 | return best_threshold; | |
| 398 | } |
| … | … | @@ -1,404 +0,0 @@ |
| 1 | /* gpu-connector-rewrite.cu — Grammar cost computation and class MI cache | |
| 2 | * | |
| 3 | * Replaces CPU grammar export computations with parallel GPU kernels: | |
| 4 | * 1. grammar_compute_costs: One thread per section → cost = -0.5 * log2(count/max) + 0.1 | |
| 5 | * 2. grammar_build_class_mi: Aggregate word-pair MI into class-pair MI | |
| 6 | * 3. grammar_class_mi_normalize: Normalize class MI by pair count | |
| 7 | * | |
| 8 | * Data layout matches existing OpenCL SoA pools (gpu-atomspace.cl). | |
| 9 | * | |
| 10 | * Build: nvcc -O2 -arch=sm_75 -rdc=true -c gpu-connector-rewrite.cu | |
| 11 | */ | |
| 12 | ||
| 13 | #include <cstdint> | |
| 14 | #include <cstdio> | |
| 15 | #include <cfloat> | |
| 16 | #include <cmath> | |
| 17 | ||
| 18 | /* ─── Constants ─── */ | |
| 19 | ||
| 20 | #ifndef CLASS_MI_HT_CAPACITY | |
| 21 | #define CLASS_MI_HT_CAPACITY 65536 /* power of 2, hash table for class pairs */ | |
| 22 | #endif | |
| 23 | ||
| 24 | /* Hash table sentinels (matching OpenCL conventions) */ | |
| 25 | #define GR_HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 26 | #define GR_HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 27 | ||
| 28 | /* ─── Hash function (same splitmix64 as gpu-hashtable.cl) ─── */ | |
| 29 | ||
| 30 | __device__ __forceinline__ | |
| 31 | uint64_t gr_hash(uint64_t key) { | |
| 32 | key ^= key >> 30; | |
| 33 | key *= 0xBF58476D1CE4E5B9ULL; | |
| 34 | key ^= key >> 27; | |
| 35 | key *= 0x94D049BB133111EBULL; | |
| 36 | key ^= key >> 31; | |
| 37 | return key; | |
| 38 | } | |
| 39 | ||
| 40 | /* ─── Class pair key encoding ─── */ | |
| 41 | ||
| 42 | __device__ __forceinline__ | |
| 43 | uint64_t class_pair_key(uint32_t class_a, uint32_t class_b) { | |
| 44 | uint32_t lo = (class_a <= class_b) ? class_a : class_b; | |
| 45 | uint32_t hi = (class_a <= class_b) ? class_b : class_a; | |
| 46 | return ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 47 | } | |
| 48 | ||
| 49 | /* ─── Atomic double add (CAS loop, same pattern as OpenCL version) ─── */ | |
| 50 | ||
| 51 | __device__ __forceinline__ | |
| 52 | void atomic_add_double(double* addr, double val) { | |
| 53 | unsigned long long int* addr_as_ull = (unsigned long long int*)addr; | |
| 54 | unsigned long long int old = *addr_as_ull; | |
| 55 | unsigned long long int assumed; | |
| 56 | do { | |
| 57 | assumed = old; | |
| 58 | old = atomicCAS(addr_as_ull, assumed, | |
| 59 | __double_as_longlong(__longlong_as_double(assumed) + val)); | |
| 60 | } while (assumed != old); | |
| 61 | } | |
| 62 | ||
| 63 | /* ═══════════════════════════════════════════════════════════════ | |
| 64 | * KERNEL 1: GRAMMAR COST COMPUTATION | |
| 65 | * | |
| 66 | * One thread per section. Computes grammar dictionary cost from | |
| 67 | * section counts: | |
| 68 | * cost = -0.5 * log2(count / max_count) + 0.1 | |
| 69 | * | |
| 70 | * Higher count → lower cost (more common = preferred). | |
| 71 | * The +0.1 base cost prevents zero-cost entries. | |
| 72 | * Sections with count < 1 get cost = 99.0 (effectively pruned). | |
| 73 | * ═══════════════════════════════════════════════════════════════ */ | |
| 74 | ||
| 75 | __global__ void grammar_compute_costs( | |
| 76 | const double* __restrict__ sec_count, /* section counts */ | |
| 77 | double* sec_cost, /* output: grammar costs */ | |
| 78 | uint32_t num_sections, | |
| 79 | double max_count /* max section count across all sections */ | |
| 80 | ) { | |
| 81 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 82 | if (tid >= num_sections) return; | |
| 83 | ||
| 84 | double count = sec_count[tid]; | |
| 85 | if (count < 1.0) { | |
| 86 | sec_cost[tid] = 99.0; /* pruned */ | |
| 87 | return; | |
| 88 | } | |
| 89 | ||
| 90 | /* cost = -0.5 * log2(count / max_count) + 0.1 */ | |
| 91 | double ratio = count / fmax(max_count, 1.0); | |
| 92 | double cost = -0.5 * log2(fmax(ratio, 1e-20)) + 0.1; | |
| 93 | ||
| 94 | /* Clamp to [0.1, 10.0] — reasonable grammar cost range */ | |
| 95 | cost = fmax(0.1, fmin(cost, 10.0)); | |
| 96 | sec_cost[tid] = cost; | |
| 97 | } | |
| 98 | ||
| 99 | /* ═══════════════════════════════════════════════════════════════ | |
| 100 | * FIND MAX SECTION COUNT (reduction) | |
| 101 | * | |
| 102 | * Parallel reduction to find max section count for cost normalization. | |
| 103 | * Uses warp shuffle for final reduction. | |
| 104 | * ═══════════════════════════════════════════════════════════════ */ | |
| 105 | ||
| 106 | __global__ void grammar_find_max_count( | |
| 107 | const double* __restrict__ sec_count, | |
| 108 | uint32_t num_sections, | |
| 109 | double* max_count_out /* single double, initialized to 0 */ | |
| 110 | ) { | |
| 111 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 112 | ||
| 113 | double my_max = 0.0; | |
| 114 | if (tid < num_sections) { | |
| 115 | my_max = sec_count[tid]; | |
| 116 | } | |
| 117 | ||
| 118 | /* Warp-level max reduction */ | |
| 119 | for (int offset = 16; offset > 0; offset >>= 1) { | |
| 120 | double other = __shfl_down_sync(0xFFFFFFFF, my_max, offset); | |
| 121 | my_max = fmax(my_max, other); | |
| 122 | } | |
| 123 | ||
| 124 | /* Lane 0 of each warp does atomicMax (via CAS since no native double atomicMax) */ | |
| 125 | if ((threadIdx.x & 31) == 0 && my_max > 0.0) { | |
| 126 | unsigned long long int* addr = (unsigned long long int*)max_count_out; | |
| 127 | unsigned long long int old = *addr; | |
| 128 | unsigned long long int assumed; | |
| 129 | do { | |
| 130 | assumed = old; | |
| 131 | double old_val = __longlong_as_double(assumed); | |
| 132 | if (my_max <= old_val) break; | |
| 133 | old = atomicCAS(addr, assumed, __double_as_longlong(my_max)); | |
| 134 | } while (assumed != old); | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | /* ═══════════════════════════════════════════════════════════════ | |
| 139 | * KERNEL 2: BUILD CLASS MI CACHE | |
| 140 | * | |
| 141 | * One thread per word pair. If both words are classified, accumulate | |
| 142 | * MI into a class-pair hash table using atomic CAS. | |
| 143 | * | |
| 144 | * class_mi_sum[class_pair] += mi | |
| 145 | * class_mi_count[class_pair] += 1 | |
| 146 | * | |
| 147 | * Hash table layout (SoA): | |
| 148 | * ht_keys[cap] — class pair key ((lo << 32) | hi) | |
| 149 | * ht_mi_sum[cap] — accumulated MI sum | |
| 150 | * ht_count[cap] — number of word pairs contributing | |
| 151 | * ═══════════════════════════════════════════════════════════════ */ | |
| 152 | ||
| 153 | __global__ void grammar_build_class_mi( | |
| 154 | /* Word pair data */ | |
| 155 | const uint32_t* __restrict__ pair_word_a, | |
| 156 | const uint32_t* __restrict__ pair_word_b, | |
| 157 | const double* __restrict__ pair_mi, | |
| 158 | const double* __restrict__ pair_count, | |
| 159 | uint32_t num_pairs, | |
| 160 | /* Word → class mapping */ | |
| 161 | const uint32_t* __restrict__ word_class_id, | |
| 162 | /* Class MI hash table (output) */ | |
| 163 | volatile uint64_t* ht_keys, | |
| 164 | double* ht_mi_sum, | |
| 165 | uint32_t* ht_count, | |
| 166 | uint32_t ht_capacity | |
| 167 | ) { | |
| 168 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 169 | if (tid >= num_pairs) return; | |
| 170 | ||
| 171 | /* Skip pairs with no count */ | |
| 172 | if (pair_count[tid] < 1.0) return; | |
| 173 | ||
| 174 | uint32_t wa = pair_word_a[tid]; | |
| 175 | uint32_t wb = pair_word_b[tid]; | |
| 176 | ||
| 177 | /* Both words must be classified */ | |
| 178 | uint32_t ca = word_class_id[wa]; | |
| 179 | uint32_t cb = word_class_id[wb]; | |
| 180 | if (ca == 0xFFFFFFFFU || cb == 0xFFFFFFFFU) return; | |
| 181 | ||
| 182 | /* Skip self-class pairs (same class) — not useful for grammar */ | |
| 183 | if (ca == cb) return; | |
| 184 | ||
| 185 | double mi = pair_mi[tid]; | |
| 186 | if (mi <= 0.0) return; /* only positive MI */ | |
| 187 | ||
| 188 | uint64_t key = class_pair_key(ca, cb); | |
| 189 | uint64_t mask = (uint64_t)(ht_capacity - 1); | |
| 190 | uint64_t slot = gr_hash(key) & mask; | |
| 191 | ||
| 192 | /* Linear probe to find or create slot */ | |
| 193 | for (int probe = 0; probe < 4096; probe++) { | |
| 194 | uint64_t existing = atomicCAS( | |
| 195 | (unsigned long long int*)&ht_keys[slot], | |
| 196 | GR_HT_EMPTY_KEY, | |
| 197 | key); | |
| 198 | ||
| 199 | if (existing == GR_HT_EMPTY_KEY || existing == key) { | |
| 200 | /* Slot is ours — accumulate */ | |
| 201 | atomic_add_double(&ht_mi_sum[slot], mi); | |
| 202 | atomicAdd(&ht_count[slot], 1U); | |
| 203 | return; | |
| 204 | } | |
| 205 | ||
| 206 | slot = (slot + 1) & mask; | |
| 207 | } | |
| 208 | /* Hash table full — drop this pair (shouldn't happen with proper sizing) */ | |
| 209 | } | |
| 210 | ||
| 211 | /* ═══════════════════════════════════════════════════════════════ | |
| 212 | * KERNEL 3: NORMALIZE CLASS MI | |
| 213 | * | |
| 214 | * One thread per hash table slot. Divides accumulated MI sum | |
| 215 | * by count to get average MI per class pair. | |
| 216 | * ═══════════════════════════════════════════════════════════════ */ | |
| 217 | ||
| 218 | __global__ void grammar_class_mi_normalize( | |
| 219 | const volatile uint64_t* ht_keys, | |
| 220 | double* ht_mi_sum, /* in/out: sum → average */ | |
| 221 | const uint32_t* ht_count, | |
| 222 | uint32_t ht_capacity | |
| 223 | ) { | |
| 224 | uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 225 | if (tid >= ht_capacity) return; | |
| 226 | ||
| 227 | if (ht_keys[tid] == GR_HT_EMPTY_KEY) return; | |
| 228 | ||
| 229 | uint32_t cnt = ht_count[tid]; | |
| 230 | if (cnt > 0) { | |
| 231 | ht_mi_sum[tid] /= (double)cnt; | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | /* ═══════════════════════════════════════════════════════════════ | |
| 236 | * CLASS MI LOOKUP (device function for other kernels) | |
| 237 | * | |
| 238 | * Look up class-pair MI from the hash table. Returns 0.0 if not found. | |
| 239 | * ═══════════════════════════════════════════════════════════════ */ | |
| 240 | ||
| 241 | __device__ | |
| 242 | double class_mi_lookup( | |
| 243 | uint32_t class_a, | |
| 244 | uint32_t class_b, | |
| 245 | const volatile uint64_t* ht_keys, | |
| 246 | const double* ht_mi_sum, | |
| 247 | uint32_t ht_capacity | |
| 248 | ) { | |
| 249 | if (class_a == 0xFFFFFFFFU || class_b == 0xFFFFFFFFU) return 0.0; | |
| 250 | if (class_a == class_b) return 0.0; | |
| 251 | ||
| 252 | uint64_t key = class_pair_key(class_a, class_b); | |
| 253 | uint64_t mask = (uint64_t)(ht_capacity - 1); | |
| 254 | uint64_t slot = gr_hash(key) & mask; | |
| 255 | ||
| 256 | for (int probe = 0; probe < 4096; probe++) { | |
| 257 | uint64_t existing = ht_keys[slot]; | |
| 258 | if (existing == key) return ht_mi_sum[slot]; | |
| 259 | if (existing == GR_HT_EMPTY_KEY) return 0.0; | |
| 260 | slot = (slot + 1) & mask; | |
| 261 | } | |
| 262 | return 0.0; | |
| 263 | } | |
| 264 | ||
| 265 | /* ═══════════════════════════════════════════════════════════════ | |
| 266 | * HOST API | |
| 267 | * ═══════════════════════════════════════════════════════════════ */ | |
| 268 | ||
| 269 | /* Initialize class MI hash table (zero keys, sums, counts) */ | |
| 270 | extern "C" | |
| 271 | void grammar_init_class_mi_ht( | |
| 272 | uint64_t* d_ht_keys, | |
| 273 | double* d_ht_mi_sum, | |
| 274 | uint32_t* d_ht_count, | |
| 275 | uint32_t ht_capacity | |
| 276 | ) { | |
| 277 | cudaMemset(d_ht_keys, 0xFF, ht_capacity * sizeof(uint64_t)); /* all GR_HT_EMPTY_KEY */ | |
| 278 | cudaMemset(d_ht_mi_sum, 0, ht_capacity * sizeof(double)); | |
| 279 | cudaMemset(d_ht_count, 0, ht_capacity * sizeof(uint32_t)); | |
| 280 | } | |
| 281 | ||
| 282 | /* Run full grammar pipeline: find max → compute costs → build class MI → normalize */ | |
| 283 | extern "C" | |
| 284 | void grammar_pipeline_run( | |
| 285 | /* Section data */ | |
| 286 | const double* d_sec_count, | |
| 287 | double* d_sec_cost, | |
| 288 | uint32_t num_sections, | |
| 289 | /* Word pair data */ | |
| 290 | const uint32_t* d_pair_word_a, | |
| 291 | const uint32_t* d_pair_word_b, | |
| 292 | const double* d_pair_mi, | |
| 293 | const double* d_pair_count, | |
| 294 | uint32_t num_pairs, | |
| 295 | /* Word → class */ | |
| 296 | const uint32_t* d_word_class_id, | |
| 297 | /* Class MI hash table */ | |
| 298 | uint64_t* d_ht_keys, | |
| 299 | double* d_ht_mi_sum, | |
| 300 | uint32_t* d_ht_count, | |
| 301 | uint32_t ht_capacity, | |
| 302 | /* Work buffer */ | |
| 303 | double* d_max_count /* single double */ | |
| 304 | ) { | |
| 305 | int threads = 256; | |
| 306 | ||
| 307 | /* Step 1: Find max section count */ | |
| 308 | cudaMemset(d_max_count, 0, sizeof(double)); | |
| 309 | int blocks_sec = (num_sections + threads - 1) / threads; | |
| 310 | if (blocks_sec > 0) { | |
| 311 | grammar_find_max_count<<<blocks_sec, threads>>>( | |
| 312 | d_sec_count, num_sections, d_max_count); | |
| 313 | } | |
| 314 | cudaDeviceSynchronize(); | |
| 315 | ||
| 316 | double h_max_count; | |
| 317 | cudaMemcpy(&h_max_count, d_max_count, sizeof(double), cudaMemcpyDeviceToHost); | |
| 318 | ||
| 319 | /* Step 2: Compute costs */ | |
| 320 | if (blocks_sec > 0) { | |
| 321 | grammar_compute_costs<<<blocks_sec, threads>>>( | |
| 322 | d_sec_count, d_sec_cost, num_sections, h_max_count); | |
| 323 | } | |
| 324 | cudaDeviceSynchronize(); | |
| 325 | ||
| 326 | /* Step 3: Build class MI cache */ | |
| 327 | grammar_init_class_mi_ht(d_ht_keys, d_ht_mi_sum, d_ht_count, ht_capacity); | |
| 328 | cudaDeviceSynchronize(); | |
| 329 | ||
| 330 | int blocks_pairs = (num_pairs + threads - 1) / threads; | |
| 331 | if (blocks_pairs > 0) { | |
| 332 | grammar_build_class_mi<<<blocks_pairs, threads>>>( | |
| 333 | d_pair_word_a, d_pair_word_b, d_pair_mi, d_pair_count, | |
| 334 | num_pairs, d_word_class_id, | |
| 335 | (volatile uint64_t*)d_ht_keys, d_ht_mi_sum, d_ht_count, | |
| 336 | ht_capacity); | |
| 337 | } | |
| 338 | cudaDeviceSynchronize(); | |
| 339 | ||
| 340 | /* Step 4: Normalize */ | |
| 341 | int blocks_ht = (ht_capacity + threads - 1) / threads; | |
| 342 | grammar_class_mi_normalize<<<blocks_ht, threads>>>( | |
| 343 | (const volatile uint64_t*)d_ht_keys, d_ht_mi_sum, d_ht_count, ht_capacity); | |
| 344 | cudaDeviceSynchronize(); | |
| 345 | } | |
| 346 | ||
| 347 | /* Read class MI for a specific class pair (host convenience) */ | |
| 348 | extern "C" | |
| 349 | double grammar_read_class_mi( | |
| 350 | uint32_t class_a, | |
| 351 | uint32_t class_b, | |
| 352 | uint64_t* d_ht_keys, | |
| 353 | double* d_ht_mi_sum, | |
| 354 | uint32_t ht_capacity | |
| 355 | ) { | |
| 356 | uint64_t key = ((uint64_t)(class_a <= class_b ? class_a : class_b) << 32) | |
| 357 | | (uint64_t)(class_a <= class_b ? class_b : class_a); | |
| 358 | ||
| 359 | uint64_t mask = (uint64_t)(ht_capacity - 1); | |
| 360 | ||
| 361 | /* Read hash table on host — copy relevant slots */ | |
| 362 | uint64_t slot = 0; | |
| 363 | { | |
| 364 | /* Compute hash on host (same splitmix64) */ | |
| 365 | uint64_t h = key; | |
| 366 | h ^= h >> 30; h *= 0xBF58476D1CE4E5B9ULL; | |
| 367 | h ^= h >> 27; h *= 0x94D049BB133111EBULL; | |
| 368 | h ^= h >> 31; | |
| 369 | slot = h & mask; | |
| 370 | } | |
| 371 | ||
| 372 | /* Linear probe on device memory */ | |
| 373 | for (int probe = 0; probe < 4096; probe++) { | |
| 374 | uint64_t existing_key; | |
| 375 | cudaMemcpy(&existing_key, &d_ht_keys[slot], sizeof(uint64_t), cudaMemcpyDeviceToHost); | |
| 376 | ||
| 377 | if (existing_key == key) { | |
| 378 | double mi; | |
| 379 | cudaMemcpy(&mi, &d_ht_mi_sum[slot], sizeof(double), cudaMemcpyDeviceToHost); | |
| 380 | return mi; | |
| 381 | } | |
| 382 | if (existing_key == GR_HT_EMPTY_KEY) return 0.0; | |
| 383 | slot = (slot + 1) & mask; | |
| 384 | } | |
| 385 | return 0.0; | |
| 386 | } | |
| 387 | ||
| 388 | /* Count occupied slots in class MI hash table */ | |
| 389 | extern "C" | |
| 390 | uint32_t grammar_class_mi_count( | |
| 391 | uint64_t* d_ht_keys, | |
| 392 | uint32_t ht_capacity | |
| 393 | ) { | |
| 394 | /* Copy keys to host and count non-empty */ | |
| 395 | uint64_t* h_keys = (uint64_t*)malloc(ht_capacity * sizeof(uint64_t)); | |
| 396 | cudaMemcpy(h_keys, d_ht_keys, ht_capacity * sizeof(uint64_t), cudaMemcpyDeviceToHost); | |
| 397 | ||
| 398 | uint32_t count = 0; | |
| 399 | for (uint32_t i = 0; i < ht_capacity; i++) { | |
| 400 | if (h_keys[i] != GR_HT_EMPTY_KEY) count++; | |
| 401 | } | |
| 402 | free(h_keys); | |
| 403 | return count; | |
| 404 | } |
| … | … | @@ -1,340 +0,0 @@ |
| 1 | /* | |
| 2 | * gpu-cosine.cl -- Cosine similarity and candidate generation on GPU | |
| 3 | * | |
| 4 | * Computes pairwise cosine similarity between words based on their | |
| 5 | * section (disjunct) vectors, entirely on GPU-resident data. | |
| 6 | * | |
| 7 | * Pipeline: | |
| 8 | * 1. compute_word_norms — ||v||² for each word | |
| 9 | * 2. build_disjunct_chains — reverse index: djh → linked list of sections | |
| 10 | * 3. accumulate_dot_products — walk chains, accumulate dot(w1,w2) | |
| 11 | * 4. compute_cosines — cosine = dot / (||v1|| × ||v2||) | |
| 12 | * 5. filter_candidates — compact output above threshold | |
| 13 | * | |
| 14 | * No CPU↔GPU data transfer between stages. | |
| 15 | * | |
| 16 | * Requires capacity macros (set via -D flags): | |
| 17 | * DJH_HT_CAPACITY — disjunct reverse index hash table size (power of 2) | |
| 18 | * CANDIDATE_CAPACITY — max candidate pairs | |
| 19 | * CANDIDATE_HT_CAPACITY — candidate pair hash table size (power of 2) | |
| 20 | * | |
| 21 | * Appended after gpu-hashtable.cl, gpu-atomspace.cl at load time. | |
| 22 | * (gpu-sections.cl optional — only needed for full pipeline tests) | |
| 23 | */ | |
| 24 | ||
| 25 | /* ═══════════════════════════════════════════════════════════════ | |
| 26 | * STEP 1: COMPUTE WORD NORMS | |
| 27 | * | |
| 28 | * One thread per section. Atomically adds count² to the word's | |
| 29 | * norm accumulator. After all sections processed: | |
| 30 | * word_norm_sq[w] = Σ_d count(w,d)² | |
| 31 | * | |
| 32 | * where d ranges over all disjuncts for word w. | |
| 33 | * ═══════════════════════════════════════════════════════════════ */ | |
| 34 | ||
| 35 | __kernel void compute_word_norms( | |
| 36 | __global const uint* sec_word, | |
| 37 | __global const double* sec_count, | |
| 38 | __global volatile double* word_norm_sq, | |
| 39 | const uint num_sections) | |
| 40 | { | |
| 41 | uint tid = get_global_id(0); | |
| 42 | if (tid >= num_sections) return; | |
| 43 | ||
| 44 | double count = sec_count[tid]; | |
| 45 | if (count < 0.5) return; | |
| 46 | ||
| 47 | uint word = sec_word[tid]; | |
| 48 | atomic_add_double(&word_norm_sq[word], count * count); | |
| 49 | } | |
| 50 | ||
| 51 | /* ═══════════════════════════════════════════════════════════════ | |
| 52 | * STEP 2: BUILD DISJUNCT REVERSE INDEX (linked list chains) | |
| 53 | * | |
| 54 | * For each unique disjunct_hash, build a linked list of all | |
| 55 | * sections that share that disjunct. Uses lock-free atomic | |
| 56 | * prepend to a per-disjunct chain. | |
| 57 | * | |
| 58 | * Data structures: | |
| 59 | * djh_ht_keys[DJH_HT_CAPACITY] — disjunct hash values | |
| 60 | * djh_ht_values[DJH_HT_CAPACITY] — chain head (section index) | |
| 61 | * sec_chain_next[SECTION_CAPACITY] — per-section next pointer | |
| 62 | * | |
| 63 | * Chain traversal: start at djh_ht_values[slot], follow | |
| 64 | * sec_chain_next[] until HT_EMPTY_VALUE (end of chain). | |
| 65 | * ═══════════════════════════════════════════════════════════════ */ | |
| 66 | ||
| 67 | __kernel void build_disjunct_chains( | |
| 68 | __global const ulong* sec_disjunct_hash, | |
| 69 | __global const double* sec_count, | |
| 70 | __global volatile ulong* djh_ht_keys, | |
| 71 | __global volatile uint* djh_ht_values, | |
| 72 | __global uint* sec_chain_next, | |
| 73 | const uint num_sections) | |
| 74 | { | |
| 75 | uint tid = get_global_id(0); | |
| 76 | if (tid >= num_sections) return; | |
| 77 | ||
| 78 | /* Skip empty sections */ | |
| 79 | if (sec_count[tid] < 0.5) { | |
| 80 | sec_chain_next[tid] = HT_EMPTY_VALUE; | |
| 81 | return; | |
| 82 | } | |
| 83 | ||
| 84 | ulong djh = sec_disjunct_hash[tid]; | |
| 85 | if (djh == HT_EMPTY_KEY) { | |
| 86 | sec_chain_next[tid] = HT_EMPTY_VALUE; | |
| 87 | return; | |
| 88 | } | |
| 89 | ||
| 90 | ulong cap = DJH_HT_CAPACITY; | |
| 91 | ulong mask = cap - 1; | |
| 92 | ulong slot = ht_hash(djh) & mask; | |
| 93 | ||
| 94 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 95 | { | |
| 96 | ulong prev = atom_cmpxchg(&djh_ht_keys[slot], HT_EMPTY_KEY, djh); | |
| 97 | ||
| 98 | if (prev == HT_EMPTY_KEY || prev == djh) | |
| 99 | { | |
| 100 | /* Atomically prepend this section to the chain. | |
| 101 | * atomic_xchg returns the old head (initially HT_EMPTY_VALUE | |
| 102 | * for a fresh slot, which serves as the end-of-chain sentinel). | |
| 103 | * Lock-free: concurrent prepends produce a valid chain. */ | |
| 104 | uint old_head = atomic_xchg(&djh_ht_values[slot], tid); | |
| 105 | sec_chain_next[tid] = old_head; | |
| 106 | return; | |
| 107 | } | |
| 108 | ||
| 109 | slot = (slot + 1) & mask; | |
| 110 | } | |
| 111 | ||
| 112 | /* Hash table full — orphan this section */ | |
| 113 | sec_chain_next[tid] = HT_EMPTY_VALUE; | |
| 114 | } | |
| 115 | ||
| 116 | /* ═══════════════════════════════════════════════════════════════ | |
| 117 | * STEP 3: ACCUMULATE DOT PRODUCTS | |
| 118 | * | |
| 119 | * One thread per section. For each section (word_i, djh_i, count_i): | |
| 120 | * - Look up djh_i in the disjunct reverse index | |
| 121 | * - Walk the chain of sections sharing this disjunct | |
| 122 | * - For each other section (word_j, djh_i, count_j) where word_j != word_i: | |
| 123 | * - Find-or-create candidate pair (word_i, word_j) | |
| 124 | * - Atomically add count_i × count_j to the candidate's dot product | |
| 125 | * | |
| 126 | * Uses canonical ordering (word_i < word_j) to avoid double-counting: | |
| 127 | * each shared disjunct contributes exactly once to each pair's dot product. | |
| 128 | * | |
| 129 | * Candidate pairs are stored in a hash table + pool (same pattern | |
| 130 | * as atom pools from gpu-atomspace.cl). | |
| 131 | * ═══════════════════════════════════════════════════════════════ */ | |
| 132 | ||
| 133 | __kernel void accumulate_dot_products( | |
| 134 | /* Section data */ | |
| 135 | __global const uint* sec_word, | |
| 136 | __global const ulong* sec_disjunct_hash, | |
| 137 | __global const double* sec_count, | |
| 138 | /* Disjunct reverse index */ | |
| 139 | __global const ulong* djh_ht_keys, | |
| 140 | __global const uint* djh_ht_values, | |
| 141 | __global const uint* sec_chain_next, | |
| 142 | /* Candidate hash table */ | |
| 143 | __global volatile ulong* cand_ht_keys, | |
| 144 | __global volatile uint* cand_ht_values, | |
| 145 | /* Candidate pool SoA */ | |
| 146 | __global uint* cand_word_a, | |
| 147 | __global uint* cand_word_b, | |
| 148 | __global volatile double* cand_dot, | |
| 149 | __global volatile uint* cand_next_free, | |
| 150 | /* Size */ | |
| 151 | const uint num_sections) | |
| 152 | { | |
| 153 | uint tid = get_global_id(0); | |
| 154 | if (tid >= num_sections) return; | |
| 155 | ||
| 156 | uint my_word = sec_word[tid]; | |
| 157 | double my_count = sec_count[tid]; | |
| 158 | if (my_count < 0.5) return; | |
| 159 | ||
| 160 | ulong my_djh = sec_disjunct_hash[tid]; | |
| 161 | if (my_djh == HT_EMPTY_KEY) return; | |
| 162 | ||
| 163 | /* ── Look up disjunct in reverse index ── */ | |
| 164 | ||
| 165 | ulong cap = DJH_HT_CAPACITY; | |
| 166 | ulong mask = cap - 1; | |
| 167 | ulong slot = ht_hash(my_djh) & mask; | |
| 168 | uint chain_head = HT_EMPTY_VALUE; | |
| 169 | ||
| 170 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 171 | { | |
| 172 | ulong k = djh_ht_keys[slot]; | |
| 173 | if (k == my_djh) { | |
| 174 | chain_head = djh_ht_values[slot]; | |
| 175 | break; | |
| 176 | } | |
| 177 | if (k == HT_EMPTY_KEY) break; | |
| 178 | slot = (slot + 1) & mask; | |
| 179 | } | |
| 180 | ||
| 181 | if (chain_head == HT_EMPTY_VALUE) return; | |
| 182 | ||
| 183 | /* ── Walk chain, accumulate dot products ── */ | |
| 184 | ||
| 185 | uint cur = chain_head; | |
| 186 | uint safety = 2048; /* max chain length guard */ | |
| 187 | ||
| 188 | while (cur != HT_EMPTY_VALUE && safety > 0) | |
| 189 | { | |
| 190 | safety--; | |
| 191 | ||
| 192 | uint other_word = sec_word[cur]; | |
| 193 | double other_count = sec_count[cur]; | |
| 194 | ||
| 195 | /* Only accumulate once per pair (canonical: my_word < other_word) */ | |
| 196 | if (other_word != my_word && my_word < other_word && other_count >= 0.5) | |
| 197 | { | |
| 198 | /* ── Find or create candidate pair ── */ | |
| 199 | ||
| 200 | uint lo = my_word; /* already < other_word */ | |
| 201 | uint hi = other_word; | |
| 202 | ulong cand_key = ((ulong)lo << 32) | (ulong)hi; | |
| 203 | ||
| 204 | ulong c_cap = CANDIDATE_HT_CAPACITY; | |
| 205 | ulong c_mask = c_cap - 1; | |
| 206 | ulong c_slot = ht_hash(cand_key) & c_mask; | |
| 207 | uint cand_idx = HT_EMPTY_VALUE; | |
| 208 | ||
| 209 | for (uint p = 0; p < HT_MAX_PROBES; p++) | |
| 210 | { | |
| 211 | ulong prev = atom_cmpxchg( | |
| 212 | &cand_ht_keys[c_slot], HT_EMPTY_KEY, cand_key); | |
| 213 | ||
| 214 | if (prev == HT_EMPTY_KEY) | |
| 215 | { | |
| 216 | /* New candidate — allocate from pool */ | |
| 217 | uint idx = atomic_add(cand_next_free, 1U); | |
| 218 | if (idx < CANDIDATE_CAPACITY) { | |
| 219 | cand_word_a[idx] = lo; | |
| 220 | cand_word_b[idx] = hi; | |
| 221 | cand_dot[idx] = 0.0; | |
| 222 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 223 | cand_ht_values[c_slot] = idx; | |
| 224 | cand_idx = idx; | |
| 225 | } | |
| 226 | break; | |
| 227 | } | |
| 228 | if (prev == cand_key) | |
| 229 | { | |
| 230 | /* Existing candidate — spin for value */ | |
| 231 | uint val = cand_ht_values[c_slot]; | |
| 232 | while (val == HT_EMPTY_VALUE) { | |
| 233 | val = cand_ht_values[c_slot]; | |
| 234 | } | |
| 235 | cand_idx = val; | |
| 236 | break; | |
| 237 | } | |
| 238 | ||
| 239 | c_slot = (c_slot + 1) & c_mask; | |
| 240 | } | |
| 241 | ||
| 242 | if (cand_idx != HT_EMPTY_VALUE) | |
| 243 | { | |
| 244 | atomic_add_double(&cand_dot[cand_idx], | |
| 245 | my_count * other_count); | |
| 246 | } | |
| 247 | } | |
| 248 | ||
| 249 | cur = sec_chain_next[cur]; | |
| 250 | } | |
| 251 | } | |
| 252 | ||
| 253 | /* ═══════════════════════════════════════════════════════════════ | |
| 254 | * STEP 4: COMPUTE COSINES | |
| 255 | * | |
| 256 | * One thread per candidate. Converts accumulated dot products | |
| 257 | * to cosine similarity using pre-computed word norms. | |
| 258 | * | |
| 259 | * cosine(w1, w2) = dot(w1, w2) / (||w1|| × ||w2||) | |
| 260 | * ═══════════════════════════════════════════════════════════════ */ | |
| 261 | ||
| 262 | __kernel void compute_cosines( | |
| 263 | __global const uint* cand_word_a, | |
| 264 | __global const uint* cand_word_b, | |
| 265 | __global const double* cand_dot, | |
| 266 | __global double* cand_cosine, | |
| 267 | __global const double* word_norm_sq, | |
| 268 | const uint num_candidates) | |
| 269 | { | |
| 270 | uint tid = get_global_id(0); | |
| 271 | if (tid >= num_candidates) return; | |
| 272 | ||
| 273 | uint wa = cand_word_a[tid]; | |
| 274 | uint wb = cand_word_b[tid]; | |
| 275 | double dot = cand_dot[tid]; | |
| 276 | ||
| 277 | double denom = sqrt(word_norm_sq[wa]) * sqrt(word_norm_sq[wb]); | |
| 278 | cand_cosine[tid] = (denom > 1e-10) ? (dot / denom) : 0.0; | |
| 279 | } | |
| 280 | ||
| 281 | /* ═══════════════════════════════════════════════════════════════ | |
| 282 | * STEP 5: FILTER CANDIDATES | |
| 283 | * | |
| 284 | * Compact candidates above a cosine threshold into contiguous | |
| 285 | * output arrays. Returns (word_a, word_b, cosine) for each | |
| 286 | * passing pair. | |
| 287 | * | |
| 288 | * out_count must be initialized to 0 before kernel launch. | |
| 289 | * ═══════════════════════════════════════════════════════════════ */ | |
| 290 | ||
| 291 | __kernel void filter_candidates( | |
| 292 | __global const uint* cand_word_a, | |
| 293 | __global const uint* cand_word_b, | |
| 294 | __global const double* cand_cosine, | |
| 295 | const uint num_candidates, | |
| 296 | const double threshold, | |
| 297 | __global uint* out_word_a, | |
| 298 | __global uint* out_word_b, | |
| 299 | __global double* out_cosine, | |
| 300 | __global volatile uint* out_count, | |
| 301 | const uint max_output) | |
| 302 | { | |
| 303 | uint tid = get_global_id(0); | |
| 304 | if (tid >= num_candidates) return; | |
| 305 | ||
| 306 | double cos_val = cand_cosine[tid]; | |
| 307 | if (cos_val > threshold) | |
| 308 | { | |
| 309 | uint idx = atomic_add(out_count, 1U); | |
| 310 | if (idx < max_output) { | |
| 311 | out_word_a[idx] = cand_word_a[tid]; | |
| 312 | out_word_b[idx] = cand_word_b[tid]; | |
| 313 | out_cosine[idx] = cos_val; | |
| 314 | } | |
| 315 | } | |
| 316 | } | |
| 317 | ||
| 318 | /* ═══════════════════════════════════════════════════════════════ | |
| 319 | * READBACK: Dump candidate data for verification | |
| 320 | * ═══════════════════════════════════════════════════════════════ */ | |
| 321 | ||
| 322 | __kernel void read_candidates( | |
| 323 | __global const uint* cand_word_a, | |
| 324 | __global const uint* cand_word_b, | |
| 325 | __global const double* cand_dot, | |
| 326 | __global const double* cand_cosine, | |
| 327 | __global uint* out_word_a, | |
| 328 | __global uint* out_word_b, | |
| 329 | __global double* out_dot, | |
| 330 | __global double* out_cosine, | |
| 331 | const uint num_candidates) | |
| 332 | { | |
| 333 | uint tid = get_global_id(0); | |
| 334 | if (tid >= num_candidates) return; | |
| 335 | ||
| 336 | out_word_a[tid] = cand_word_a[tid]; | |
| 337 | out_word_b[tid] = cand_word_b[tid]; | |
| 338 | out_dot[tid] = cand_dot[tid]; | |
| 339 | out_cosine[tid] = cand_cosine[tid]; | |
| 340 | } |
| … | … | @@ -1,296 +0,0 @@ |
| 1 | /* | |
| 2 | * gpu-counting.cl -- Sentence processing and pair counting on GPU | |
| 3 | * | |
| 4 | * Takes batches of sentences (as word pool indices), generates all | |
| 5 | * word pairs within a sliding window, finds-or-creates each pair | |
| 6 | * in the pair pool, and atomically increments counts. | |
| 7 | * | |
| 8 | * This replaces cl-l0-count-pairs! from corpus-learning.scm. | |
| 9 | * | |
| 10 | * Appended after gpu-hashtable.cl and gpu-atomspace.cl at load time. | |
| 11 | */ | |
| 12 | ||
| 13 | /* ═══════════════════════════════════════════════════════════════ | |
| 14 | * SENTENCE PAIR COUNTING | |
| 15 | * | |
| 16 | * One thread per word position. Each thread generates up to | |
| 17 | * WINDOW_SIZE pairs with subsequent words in the same sentence. | |
| 18 | * For each pair: find-or-create + atomic count increment. | |
| 19 | * ═══════════════════════════════════════════════════════════════ */ | |
| 20 | ||
| 21 | /* | |
| 22 | * Process a batch of sentences: count all word pairs within window. | |
| 23 | * | |
| 24 | * Work assignment: one thread per word position across all sentences. | |
| 25 | * Thread i handles the word at flat_words[i], generating pairs with | |
| 26 | * the next WINDOW_SIZE words in the same sentence. | |
| 27 | * | |
| 28 | * Args: | |
| 29 | * flat_words - all sentences concatenated [total_words] | |
| 30 | * sent_offsets - start index of each sentence [num_sentences] | |
| 31 | * sent_lengths - length of each sentence [num_sentences] | |
| 32 | * num_sentences - number of sentences in batch | |
| 33 | * total_words - total words across all sentences | |
| 34 | * window_size - pair window (typically 6) | |
| 35 | * | |
| 36 | * -- Pair pool + hash table -- | |
| 37 | * pht_keys, pht_values - pair hash table | |
| 38 | * pair_word_a, pair_word_b, pair_count, pair_mi, pair_flags | |
| 39 | * pair_next_free | |
| 40 | * | |
| 41 | * -- Word pool (marginals) -- | |
| 42 | * word_count | |
| 43 | * | |
| 44 | * -- Global counter -- | |
| 45 | * total_pair_count - total pairs counted (atomic) | |
| 46 | */ | |
| 47 | __kernel void count_sentence_pairs( | |
| 48 | __global const uint* flat_words, | |
| 49 | __global const uint* sent_offsets, | |
| 50 | __global const uint* sent_lengths, | |
| 51 | const uint num_sentences, | |
| 52 | const uint total_words, | |
| 53 | const uint window_size, | |
| 54 | /* pair hash table */ | |
| 55 | __global volatile ulong* pht_keys, | |
| 56 | __global volatile uint* pht_values, | |
| 57 | /* pair pool SoA */ | |
| 58 | __global uint* pair_word_a, | |
| 59 | __global uint* pair_word_b, | |
| 60 | __global volatile double* pair_count, | |
| 61 | __global double* pair_mi, | |
| 62 | __global volatile uint* pair_flags, | |
| 63 | __global volatile uint* pair_next_free, | |
| 64 | /* word pool */ | |
| 65 | __global volatile double* word_count, | |
| 66 | /* global counter */ | |
| 67 | __global volatile uint* total_pair_count) | |
| 68 | { | |
| 69 | uint tid = get_global_id(0); | |
| 70 | if (tid >= total_words) return; | |
| 71 | ||
| 72 | uint word_i = flat_words[tid]; | |
| 73 | ||
| 74 | /* Find which sentence this word belongs to (linear scan — sentences | |
| 75 | * are small, typically <100 per batch, so this is fast) */ | |
| 76 | uint sent_idx = 0; | |
| 77 | for (uint s = 0; s < num_sentences; s++) { | |
| 78 | if (tid >= sent_offsets[s] && | |
| 79 | tid < sent_offsets[s] + sent_lengths[s]) { | |
| 80 | sent_idx = s; | |
| 81 | break; | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | uint sent_start = sent_offsets[sent_idx]; | |
| 86 | uint sent_len = sent_lengths[sent_idx]; | |
| 87 | uint pos_in_sent = tid - sent_start; | |
| 88 | ||
| 89 | /* Generate pairs with subsequent words within window */ | |
| 90 | uint max_j = min(pos_in_sent + window_size, sent_len - 1); | |
| 91 | ||
| 92 | for (uint j = pos_in_sent + 1; j <= max_j; j++) | |
| 93 | { | |
| 94 | uint word_j = flat_words[sent_start + j]; | |
| 95 | ||
| 96 | /* Skip self-pairs */ | |
| 97 | if (word_i == word_j) continue; | |
| 98 | ||
| 99 | /* ── Find or create pair ── */ | |
| 100 | uint lo = min(word_i, word_j); | |
| 101 | uint hi = max(word_i, word_j); | |
| 102 | ulong key = ((ulong)lo << 32) | (ulong)hi; | |
| 103 | ||
| 104 | ulong ht_cap = PAIR_HT_CAPACITY; | |
| 105 | ulong mask = ht_cap - 1; | |
| 106 | ulong slot = ht_hash(key) & mask; | |
| 107 | uint pair_idx = HT_EMPTY_VALUE; | |
| 108 | ||
| 109 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 110 | { | |
| 111 | ulong prev = atom_cmpxchg(&pht_keys[slot], HT_EMPTY_KEY, key); | |
| 112 | ||
| 113 | if (prev == HT_EMPTY_KEY) | |
| 114 | { | |
| 115 | /* New pair — allocate from pool */ | |
| 116 | uint idx = atomic_add(pair_next_free, 1U); | |
| 117 | if (idx < PAIR_CAPACITY) { | |
| 118 | pair_word_a[idx] = lo; | |
| 119 | pair_word_b[idx] = hi; | |
| 120 | pair_count[idx] = 0.0; | |
| 121 | pair_mi[idx] = 0.0; | |
| 122 | pair_flags[idx] = 0; | |
| 123 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 124 | pht_values[slot] = idx; | |
| 125 | pair_idx = idx; | |
| 126 | } | |
| 127 | break; | |
| 128 | } | |
| 129 | if (prev == key) | |
| 130 | { | |
| 131 | /* Existing pair — spin for value */ | |
| 132 | uint val = pht_values[slot]; | |
| 133 | while (val == HT_EMPTY_VALUE) { | |
| 134 | val = pht_values[slot]; | |
| 135 | } | |
| 136 | pair_idx = val; | |
| 137 | break; | |
| 138 | } | |
| 139 | ||
| 140 | slot = (slot + 1) & mask; | |
| 141 | } | |
| 142 | ||
| 143 | if (pair_idx == HT_EMPTY_VALUE) continue; | |
| 144 | ||
| 145 | /* ── Increment counts ── */ | |
| 146 | ||
| 147 | /* Pair count */ | |
| 148 | atomic_add_double(&pair_count[pair_idx], 1.0); | |
| 149 | ||
| 150 | /* Word marginals (both words) */ | |
| 151 | atomic_add_double(&word_count[word_i], 1.0); | |
| 152 | atomic_add_double(&word_count[word_j], 1.0); | |
| 153 | ||
| 154 | /* Mark dirty */ | |
| 155 | pair_flags[pair_idx] = 1; | |
| 156 | ||
| 157 | /* Total count */ | |
| 158 | atomic_add(total_pair_count, 1U); | |
| 159 | } | |
| 160 | } | |
| 161 | ||
| 162 | /* ═══════════════════════════════════════════════════════════════ | |
| 163 | * BATCH SENTENCE PROCESSING — BINARY SEARCH VARIANT | |
| 164 | * | |
| 165 | * For large batches (1000+ sentences), uses binary search instead | |
| 166 | * of linear scan to find which sentence a word belongs to. | |
| 167 | * ═══════════════════════════════════════════════════════════════ */ | |
| 168 | ||
| 169 | __kernel void count_sentence_pairs_large( | |
| 170 | __global const uint* flat_words, | |
| 171 | __global const uint* sent_offsets, | |
| 172 | __global const uint* sent_lengths, | |
| 173 | const uint num_sentences, | |
| 174 | const uint total_words, | |
| 175 | const uint window_size, | |
| 176 | __global volatile ulong* pht_keys, | |
| 177 | __global volatile uint* pht_values, | |
| 178 | __global uint* pair_word_a, | |
| 179 | __global uint* pair_word_b, | |
| 180 | __global volatile double* pair_count, | |
| 181 | __global double* pair_mi, | |
| 182 | __global volatile uint* pair_flags, | |
| 183 | __global volatile uint* pair_next_free, | |
| 184 | __global volatile double* word_count, | |
| 185 | __global volatile uint* total_pair_count) | |
| 186 | { | |
| 187 | uint tid = get_global_id(0); | |
| 188 | if (tid >= total_words) return; | |
| 189 | ||
| 190 | uint word_i = flat_words[tid]; | |
| 191 | ||
| 192 | /* Binary search for sentence index */ | |
| 193 | uint lo_s = 0, hi_s = num_sentences; | |
| 194 | while (lo_s < hi_s) { | |
| 195 | uint mid = (lo_s + hi_s) / 2; | |
| 196 | if (sent_offsets[mid] + sent_lengths[mid] <= tid) | |
| 197 | lo_s = mid + 1; | |
| 198 | else | |
| 199 | hi_s = mid; | |
| 200 | } | |
| 201 | uint sent_idx = lo_s; | |
| 202 | if (sent_idx >= num_sentences) return; | |
| 203 | ||
| 204 | uint sent_start = sent_offsets[sent_idx]; | |
| 205 | uint sent_len = sent_lengths[sent_idx]; | |
| 206 | uint pos_in_sent = tid - sent_start; | |
| 207 | ||
| 208 | /* Verify we're actually in this sentence */ | |
| 209 | if (pos_in_sent >= sent_len) return; | |
| 210 | ||
| 211 | uint max_j = min(pos_in_sent + window_size, sent_len - 1); | |
| 212 | ||
| 213 | for (uint j = pos_in_sent + 1; j <= max_j; j++) | |
| 214 | { | |
| 215 | uint word_j = flat_words[sent_start + j]; | |
| 216 | if (word_i == word_j) continue; | |
| 217 | ||
| 218 | uint lo = min(word_i, word_j); | |
| 219 | uint hi = max(word_i, word_j); | |
| 220 | ulong key = ((ulong)lo << 32) | (ulong)hi; | |
| 221 | ||
| 222 | ulong ht_cap = PAIR_HT_CAPACITY; | |
| 223 | ulong mask = ht_cap - 1; | |
| 224 | ulong slot = ht_hash(key) & mask; | |
| 225 | uint pair_idx = HT_EMPTY_VALUE; | |
| 226 | ||
| 227 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 228 | { | |
| 229 | ulong prev = atom_cmpxchg(&pht_keys[slot], HT_EMPTY_KEY, key); | |
| 230 | ||
| 231 | if (prev == HT_EMPTY_KEY) | |
| 232 | { | |
| 233 | uint idx = atomic_add(pair_next_free, 1U); | |
| 234 | if (idx < PAIR_CAPACITY) { | |
| 235 | pair_word_a[idx] = lo; | |
| 236 | pair_word_b[idx] = hi; | |
| 237 | pair_count[idx] = 0.0; | |
| 238 | pair_mi[idx] = 0.0; | |
| 239 | pair_flags[idx] = 0; | |
| 240 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 241 | pht_values[slot] = idx; | |
| 242 | pair_idx = idx; | |
| 243 | } | |
| 244 | break; | |
| 245 | } | |
| 246 | if (prev == key) | |
| 247 | { | |
| 248 | uint val = pht_values[slot]; | |
| 249 | while (val == HT_EMPTY_VALUE) { | |
| 250 | val = pht_values[slot]; | |
| 251 | } | |
| 252 | pair_idx = val; | |
| 253 | break; | |
| 254 | } | |
| 255 | ||
| 256 | slot = (slot + 1) & mask; | |
| 257 | } | |
| 258 | ||
| 259 | if (pair_idx == HT_EMPTY_VALUE) continue; | |
| 260 | ||
| 261 | atomic_add_double(&pair_count[pair_idx], 1.0); | |
| 262 | atomic_add_double(&word_count[word_i], 1.0); | |
| 263 | atomic_add_double(&word_count[word_j], 1.0); | |
| 264 | pair_flags[pair_idx] = 1; | |
| 265 | atomic_add(total_pair_count, 1U); | |
| 266 | } | |
| 267 | } | |
| 268 | ||
| 269 | /* ═══════════════════════════════════════════════════════════════ | |
| 270 | * READBACK: Dump pair data for verification | |
| 271 | * | |
| 272 | * Reads pair pool entries into flat output arrays. | |
| 273 | * ═══════════════════════════════════════════════════════════════ */ | |
| 274 | ||
| 275 | __kernel void read_pairs( | |
| 276 | __global const uint* pair_word_a, | |
| 277 | __global const uint* pair_word_b, | |
| 278 | __global const double* pair_count, | |
| 279 | __global const double* pair_mi, | |
| 280 | __global const uint* pair_flags, | |
| 281 | __global uint* out_word_a, | |
| 282 | __global uint* out_word_b, | |
| 283 | __global double* out_count, | |
| 284 | __global double* out_mi, | |
| 285 | __global uint* out_flags, | |
| 286 | const uint num_pairs) | |
| 287 | { | |
| 288 | uint tid = get_global_id(0); | |
| 289 | if (tid >= num_pairs) return; | |
| 290 | ||
| 291 | out_word_a[tid] = pair_word_a[tid]; | |
| 292 | out_word_b[tid] = pair_word_b[tid]; | |
| 293 | out_count[tid] = pair_count[tid]; | |
| 294 | out_mi[tid] = pair_mi[tid]; | |
| 295 | out_flags[tid] = pair_flags[tid]; | |
| 296 | } |
| … | … | @@ -1,284 +0,0 @@ |
| 1 | /* gpu-divergence.cu — Polysemy detection across compartments | |
| 2 | * | |
| 3 | * MI-neighborhood divergence: for each word present in two compartments, | |
| 4 | * build MI-partner vectors and compute cosine similarity between them. | |
| 5 | * Low cosine → word has different associations → polysemous. | |
| 6 | * | |
| 7 | * Validated by exp9c: MI-neighborhood divergence finds plausible polysemy | |
| 8 | * at 2.1% rate (5/235 words) — "just right" selectivity. | |
| 9 | * | |
| 10 | * Build: nvcc -O2 -arch=sm_75 -rdc=true -c gpu-divergence.cu | |
| 11 | */ | |
| 12 | ||
| 13 | #include <cstdint> | |
| 14 | #include <cstdio> | |
| 15 | #include <cfloat> | |
| 16 | #include <cmath> | |
| 17 | ||
| 18 | /* ─── Constants ─── */ | |
| 19 | ||
| 20 | #ifndef DIV_TOP_K | |
| 21 | #define DIV_TOP_K 32 /* number of top MI partners to compare */ | |
| 22 | #endif | |
| 23 | ||
| 24 | #ifndef DIV_MAX_WORDS | |
| 25 | #define DIV_MAX_WORDS 131072 | |
| 26 | #endif | |
| 27 | ||
| 28 | /* Hash table sentinels */ | |
| 29 | #define DIV_HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 30 | #define DIV_HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 31 | ||
| 32 | /* ─── Hash function ─── */ | |
| 33 | ||
| 34 | __device__ __forceinline__ | |
| 35 | uint64_t div_hash(uint64_t key) { | |
| 36 | key ^= key >> 30; | |
| 37 | key *= 0xBF58476D1CE4E5B9ULL; | |
| 38 | key ^= key >> 27; | |
| 39 | key *= 0x94D049BB133111EBULL; | |
| 40 | key ^= key >> 31; | |
| 41 | return key; | |
| 42 | } | |
| 43 | ||
| 44 | /* ─── Pair key ─── */ | |
| 45 | ||
| 46 | __device__ __forceinline__ | |
| 47 | uint64_t div_pair_key(uint32_t a, uint32_t b) { | |
| 48 | uint32_t lo = (a <= b) ? a : b; | |
| 49 | uint32_t hi = (a <= b) ? b : a; | |
| 50 | return ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 51 | } | |
| 52 | ||
| 53 | /* ═══════════════════════════════════════════════════════════════ | |
| 54 | * KERNEL 1: BUILD MI NEIGHBORHOOD VECTORS | |
| 55 | * | |
| 56 | * One thread per word. For each word, scan all pairs involving | |
| 57 | * that word and collect the top-K MI partners into a dense vector. | |
| 58 | * | |
| 59 | * The "neighborhood vector" is indexed by partner word index. | |
| 60 | * For cosine comparison, we store MI values at partner positions. | |
| 61 | * | |
| 62 | * Simple approach: scan all pairs, keep top-K by MI value using | |
| 63 | * an insertion sort (K is small, 32). | |
| 64 | * ═══════════════════════════════════════════════════════════════ */ | |
| 65 | ||
| 66 | __global__ void build_mi_neighborhood( | |
| 67 | /* Pair data (one compartment) */ | |
| 68 | const uint32_t* __restrict__ pair_word_a, | |
| 69 | const uint32_t* __restrict__ pair_word_b, | |
| 70 | const double* __restrict__ pair_mi, | |
| 71 | const double* __restrict__ pair_count, | |
| 72 | uint32_t num_pairs, | |
| 73 | uint32_t num_words, | |
| 74 | /* Output: top-K neighbors per word */ | |
| 75 | uint32_t* neighbor_ids, /* [word_idx * DIV_TOP_K + k] = partner word */ | |
| 76 | double* neighbor_mi, /* [word_idx * DIV_TOP_K + k] = MI value */ | |
| 77 | uint32_t* neighbor_count /* [word_idx] = actual number of neighbors found */ | |
| 78 | ) { | |
| 79 | uint32_t wid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 80 | if (wid >= num_words) return; | |
| 81 | ||
| 82 | /* Local top-K tracking */ | |
| 83 | uint32_t top_ids[DIV_TOP_K]; | |
| 84 | double top_mi[DIV_TOP_K]; | |
| 85 | int count = 0; | |
| 86 | ||
| 87 | for (int k = 0; k < DIV_TOP_K; k++) { | |
| 88 | top_ids[k] = 0xFFFFFFFFU; | |
| 89 | top_mi[k] = -1e30; | |
| 90 | } | |
| 91 | ||
| 92 | /* Scan all pairs for this word */ | |
| 93 | for (uint32_t p = 0; p < num_pairs; p++) { | |
| 94 | if (pair_count[p] < 1.0) continue; | |
| 95 | ||
| 96 | uint32_t wa = pair_word_a[p]; | |
| 97 | uint32_t wb = pair_word_b[p]; | |
| 98 | ||
| 99 | uint32_t partner; | |
| 100 | if (wa == wid) partner = wb; | |
| 101 | else if (wb == wid) partner = wa; | |
| 102 | else continue; | |
| 103 | ||
| 104 | double mi = pair_mi[p]; | |
| 105 | if (mi <= 0.0) continue; | |
| 106 | ||
| 107 | /* Insert into top-K (insertion sort, K=32 is small) */ | |
| 108 | if (count < DIV_TOP_K) { | |
| 109 | /* Find insertion point */ | |
| 110 | int pos = count; | |
| 111 | while (pos > 0 && top_mi[pos - 1] < mi) { | |
| 112 | if (pos < DIV_TOP_K) { | |
| 113 | top_ids[pos] = top_ids[pos - 1]; | |
| 114 | top_mi[pos] = top_mi[pos - 1]; | |
| 115 | } | |
| 116 | pos--; | |
| 117 | } | |
| 118 | top_ids[pos] = partner; | |
| 119 | top_mi[pos] = mi; | |
| 120 | count++; | |
| 121 | } else if (mi > top_mi[DIV_TOP_K - 1]) { | |
| 122 | /* Replace smallest */ | |
| 123 | int pos = DIV_TOP_K - 1; | |
| 124 | while (pos > 0 && top_mi[pos - 1] < mi) { | |
| 125 | top_ids[pos] = top_ids[pos - 1]; | |
| 126 | top_mi[pos] = top_mi[pos - 1]; | |
| 127 | pos--; | |
| 128 | } | |
| 129 | top_ids[pos] = partner; | |
| 130 | top_mi[pos] = mi; | |
| 131 | } | |
| 132 | } | |
| 133 | ||
| 134 | /* Write to output */ | |
| 135 | uint32_t base = wid * DIV_TOP_K; | |
| 136 | int actual = (count < DIV_TOP_K) ? count : DIV_TOP_K; | |
| 137 | for (int k = 0; k < actual; k++) { | |
| 138 | neighbor_ids[base + k] = top_ids[k]; | |
| 139 | neighbor_mi[base + k] = top_mi[k]; | |
| 140 | } | |
| 141 | for (int k = actual; k < DIV_TOP_K; k++) { | |
| 142 | neighbor_ids[base + k] = 0xFFFFFFFFU; | |
| 143 | neighbor_mi[base + k] = 0.0; | |
| 144 | } | |
| 145 | neighbor_count[wid] = actual; | |
| 146 | } | |
| 147 | ||
| 148 | /* ═══════════════════════════════════════════════════════════════ | |
| 149 | * KERNEL 2: COMPUTE DIVERGENCE (COSINE BETWEEN NEIGHBORHOODS) | |
| 150 | * | |
| 151 | * One thread per word. For each word, compare its MI-neighborhood | |
| 152 | * in compartment A vs compartment B using cosine similarity on | |
| 153 | * the intersection of partner sets. | |
| 154 | * | |
| 155 | * Low cosine = high divergence = polysemy candidate. | |
| 156 | * ═══════════════════════════════════════════════════════════════ */ | |
| 157 | ||
| 158 | __global__ void compute_divergence( | |
| 159 | /* Compartment A neighborhoods */ | |
| 160 | const uint32_t* __restrict__ nbr_a_ids, | |
| 161 | const double* __restrict__ nbr_a_mi, | |
| 162 | const uint32_t* __restrict__ nbr_a_count, | |
| 163 | /* Compartment B neighborhoods */ | |
| 164 | const uint32_t* __restrict__ nbr_b_ids, | |
| 165 | const double* __restrict__ nbr_b_mi, | |
| 166 | const uint32_t* __restrict__ nbr_b_count, | |
| 167 | uint32_t num_words, | |
| 168 | /* Output */ | |
| 169 | double* divergence_score, /* [word_idx] = 1.0 - cosine */ | |
| 170 | uint32_t* polysemy_flag, /* [word_idx] = 1 if polysemous */ | |
| 171 | double polysemy_threshold /* divergence above this → polysemous */ | |
| 172 | ) { | |
| 173 | uint32_t wid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 174 | if (wid >= num_words) return; | |
| 175 | ||
| 176 | uint32_t ca = nbr_a_count[wid]; | |
| 177 | uint32_t cb = nbr_b_count[wid]; | |
| 178 | ||
| 179 | /* Skip words with insufficient data in either compartment */ | |
| 180 | if (ca < 3 || cb < 3) { | |
| 181 | divergence_score[wid] = 0.0; | |
| 182 | polysemy_flag[wid] = 0; | |
| 183 | return; | |
| 184 | } | |
| 185 | ||
| 186 | uint32_t base_a = wid * DIV_TOP_K; | |
| 187 | uint32_t base_b = wid * DIV_TOP_K; | |
| 188 | ||
| 189 | /* Compute cosine between neighborhood vectors. | |
| 190 | * Build dense vectors over the union of partner sets, | |
| 191 | * then compute dot product and norms. | |
| 192 | * | |
| 193 | * Simple O(K²) approach since K=32 is small. */ | |
| 194 | double dot = 0.0; | |
| 195 | double norm_a_sq = 0.0; | |
| 196 | double norm_b_sq = 0.0; | |
| 197 | ||
| 198 | /* For each partner in A, find matching partner in B */ | |
| 199 | for (uint32_t i = 0; i < ca; i++) { | |
| 200 | uint32_t pid_a = nbr_a_ids[base_a + i]; | |
| 201 | double mi_a = nbr_a_mi[base_a + i]; | |
| 202 | norm_a_sq += mi_a * mi_a; | |
| 203 | ||
| 204 | for (uint32_t j = 0; j < cb; j++) { | |
| 205 | if (nbr_b_ids[base_b + j] == pid_a) { | |
| 206 | dot += mi_a * nbr_b_mi[base_b + j]; | |
| 207 | break; | |
| 208 | } | |
| 209 | } | |
| 210 | } | |
| 211 | ||
| 212 | for (uint32_t j = 0; j < cb; j++) { | |
| 213 | double mi_b = nbr_b_mi[base_b + j]; | |
| 214 | norm_b_sq += mi_b * mi_b; | |
| 215 | } | |
| 216 | ||
| 217 | /* Cosine similarity */ | |
| 218 | double denom = sqrt(norm_a_sq) * sqrt(norm_b_sq); | |
| 219 | double cosine = (denom > 1e-10) ? (dot / denom) : 1.0; | |
| 220 | cosine = fmax(-1.0, fmin(1.0, cosine)); /* clamp */ | |
| 221 | ||
| 222 | double div = 1.0 - cosine; | |
| 223 | divergence_score[wid] = div; | |
| 224 | polysemy_flag[wid] = (div > polysemy_threshold) ? 1 : 0; | |
| 225 | } | |
| 226 | ||
| 227 | /* ═══════════════════════════════════════════════════════════════ | |
| 228 | * HOST API | |
| 229 | * ═══════════════════════════════════════════════════════════════ */ | |
| 230 | ||
| 231 | extern "C" | |
| 232 | void divergence_run( | |
| 233 | /* Compartment A pair data */ | |
| 234 | const uint32_t* d_pair_a_word_a, | |
| 235 | const uint32_t* d_pair_a_word_b, | |
| 236 | const double* d_pair_a_mi, | |
| 237 | const double* d_pair_a_count, | |
| 238 | uint32_t num_pairs_a, | |
| 239 | /* Compartment B pair data */ | |
| 240 | const uint32_t* d_pair_b_word_a, | |
| 241 | const uint32_t* d_pair_b_word_b, | |
| 242 | const double* d_pair_b_mi, | |
| 243 | const double* d_pair_b_count, | |
| 244 | uint32_t num_pairs_b, | |
| 245 | /* Common */ | |
| 246 | uint32_t num_words, | |
| 247 | double polysemy_threshold, | |
| 248 | /* Pre-allocated work buffers */ | |
| 249 | uint32_t* d_nbr_a_ids, /* [num_words * DIV_TOP_K] */ | |
| 250 | double* d_nbr_a_mi, | |
| 251 | uint32_t* d_nbr_a_count, /* [num_words] */ | |
| 252 | uint32_t* d_nbr_b_ids, | |
| 253 | double* d_nbr_b_mi, | |
| 254 | uint32_t* d_nbr_b_count, | |
| 255 | /* Output */ | |
| 256 | double* d_divergence, /* [num_words] */ | |
| 257 | uint32_t* d_polysemy_flag /* [num_words] */ | |
| 258 | ) { | |
| 259 | int threads = 256; | |
| 260 | int blocks = (num_words + threads - 1) / threads; | |
| 261 | if (blocks < 1) blocks = 1; | |
| 262 | ||
| 263 | /* Build neighborhoods for compartment A */ | |
| 264 | build_mi_neighborhood<<<blocks, threads>>>( | |
| 265 | d_pair_a_word_a, d_pair_a_word_b, d_pair_a_mi, d_pair_a_count, | |
| 266 | num_pairs_a, num_words, | |
| 267 | d_nbr_a_ids, d_nbr_a_mi, d_nbr_a_count); | |
| 268 | cudaDeviceSynchronize(); | |
| 269 | ||
| 270 | /* Build neighborhoods for compartment B */ | |
| 271 | build_mi_neighborhood<<<blocks, threads>>>( | |
| 272 | d_pair_b_word_a, d_pair_b_word_b, d_pair_b_mi, d_pair_b_count, | |
| 273 | num_pairs_b, num_words, | |
| 274 | d_nbr_b_ids, d_nbr_b_mi, d_nbr_b_count); | |
| 275 | cudaDeviceSynchronize(); | |
| 276 | ||
| 277 | /* Compute divergence */ | |
| 278 | compute_divergence<<<blocks, threads>>>( | |
| 279 | d_nbr_a_ids, d_nbr_a_mi, d_nbr_a_count, | |
| 280 | d_nbr_b_ids, d_nbr_b_mi, d_nbr_b_count, | |
| 281 | num_words, | |
| 282 | d_divergence, d_polysemy_flag, polysemy_threshold); | |
| 283 | cudaDeviceSynchronize(); | |
| 284 | } |
| … | … | @@ -1,303 +0,0 @@ |
| 1 | /* | |
| 2 | * gpu-hashtable.cl -- Lock-free GPU hash table using linear probing | |
| 3 | * | |
| 4 | * Open-addressing hash table for mapping 64-bit keys to 32-bit values. | |
| 5 | * Designed for GPU-resident AtomSpace: maps atom content hashes to | |
| 6 | * pool indices (slots in flat SoA arrays). | |
| 7 | * | |
| 8 | * Based on: nosferalatu/SimpleGPUHashTable (CUDA) and | |
| 9 | * LANL/CompactHash (OpenCL) | |
| 10 | * | |
| 11 | * Key properties: | |
| 12 | * - Lock-free concurrent insert/lookup via atomic CAS | |
| 13 | * - Linear probing with power-of-two table size | |
| 14 | * - 64-bit keys (atom content hashes) | |
| 15 | * - 32-bit values (pool indices) | |
| 16 | * - Sentinel key 0xFFFFFFFFFFFFFFFF marks empty slots | |
| 17 | * - Sentinel value 0xFFFFFFFF marks deleted entries | |
| 18 | * - 50% load factor recommended (trade memory for speed) | |
| 19 | * | |
| 20 | * Requires: cl_khr_int64_base_atomics | |
| 21 | */ | |
| 22 | ||
| 23 | #pragma OPENCL EXTENSION cl_khr_int64_base_atomics : enable | |
| 24 | ||
| 25 | /* ─── Constants ─── */ | |
| 26 | ||
| 27 | /* Empty slot sentinel — no valid atom hash should equal this */ | |
| 28 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFUL | |
| 29 | /* Deleted value sentinel — key stays, value marks as deleted */ | |
| 30 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 31 | /* Max probes before giving up (prevents infinite loops on full table) */ | |
| 32 | #define HT_MAX_PROBES 4096 | |
| 33 | ||
| 34 | /* ─── Hash function ─── */ | |
| 35 | ||
| 36 | /* | |
| 37 | * 64-bit finalizer from splitmix64 / Murmur3. | |
| 38 | * Produces well-distributed hashes from sequential or clustered keys. | |
| 39 | */ | |
| 40 | inline ulong ht_hash(ulong key) | |
| 41 | { | |
| 42 | key ^= key >> 30; | |
| 43 | key *= 0xBF58476D1CE4E5B9UL; | |
| 44 | key ^= key >> 27; | |
| 45 | key *= 0x94D049BB133111EBUL; | |
| 46 | key ^= key >> 31; | |
| 47 | return key; | |
| 48 | } | |
| 49 | ||
| 50 | /* ─── Insert ─── */ | |
| 51 | ||
| 52 | /* | |
| 53 | * Insert key-value pairs into the hash table. | |
| 54 | * | |
| 55 | * Each work-item handles one (key, value) pair. | |
| 56 | * Uses atomic CAS on the key slot: if the slot is empty or already | |
| 57 | * contains our key, we write the value. Otherwise linear-probe forward. | |
| 58 | * | |
| 59 | * Args: | |
| 60 | * table_keys - key array [capacity] (ulong) | |
| 61 | * table_values - value array [capacity] (uint) | |
| 62 | * capacity - table size (must be power of 2) | |
| 63 | * in_keys - keys to insert [num_items] | |
| 64 | * in_values - values to insert [num_items] | |
| 65 | * num_items - number of items to insert | |
| 66 | */ | |
| 67 | __kernel void ht_insert( | |
| 68 | __global volatile ulong* table_keys, | |
| 69 | __global uint* table_values, | |
| 70 | const ulong capacity, | |
| 71 | __global const ulong* in_keys, | |
| 72 | __global const uint* in_values, | |
| 73 | const uint num_items) | |
| 74 | { | |
| 75 | uint tid = get_global_id(0); | |
| 76 | if (tid >= num_items) return; | |
| 77 | ||
| 78 | ulong key = in_keys[tid]; | |
| 79 | uint value = in_values[tid]; | |
| 80 | ||
| 81 | /* Don't insert the sentinel key */ | |
| 82 | if (key == HT_EMPTY_KEY) return; | |
| 83 | ||
| 84 | ulong mask = capacity - 1; /* power-of-2 masking */ | |
| 85 | ulong slot = ht_hash(key) & mask; | |
| 86 | ||
| 87 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 88 | { | |
| 89 | /* Try to claim this slot with our key */ | |
| 90 | ulong prev = atom_cmpxchg( | |
| 91 | &table_keys[slot], HT_EMPTY_KEY, key); | |
| 92 | ||
| 93 | if (prev == HT_EMPTY_KEY || prev == key) | |
| 94 | { | |
| 95 | /* Slot was empty or already ours — write value */ | |
| 96 | table_values[slot] = value; | |
| 97 | return; | |
| 98 | } | |
| 99 | ||
| 100 | /* Slot taken by another key — linear probe */ | |
| 101 | slot = (slot + 1) & mask; | |
| 102 | } | |
| 103 | /* Table full or too many collisions — item not inserted */ | |
| 104 | } | |
| 105 | ||
| 106 | /* ─── Lookup ─── */ | |
| 107 | ||
| 108 | /* | |
| 109 | * Look up keys in the hash table, returning their values. | |
| 110 | * | |
| 111 | * Each work-item looks up one key. If found, writes the value | |
| 112 | * to out_values[tid]. If not found, writes HT_EMPTY_VALUE. | |
| 113 | * | |
| 114 | * Args: | |
| 115 | * table_keys - key array [capacity] (ulong) | |
| 116 | * table_values - value array [capacity] (uint) | |
| 117 | * capacity - table size (must be power of 2) | |
| 118 | * query_keys - keys to look up [num_queries] | |
| 119 | * out_values - results written here [num_queries] | |
| 120 | * num_queries - number of lookups | |
| 121 | */ | |
| 122 | __kernel void ht_lookup( | |
| 123 | __global const ulong* table_keys, | |
| 124 | __global const uint* table_values, | |
| 125 | const ulong capacity, | |
| 126 | __global const ulong* query_keys, | |
| 127 | __global uint* out_values, | |
| 128 | const uint num_queries) | |
| 129 | { | |
| 130 | uint tid = get_global_id(0); | |
| 131 | if (tid >= num_queries) return; | |
| 132 | ||
| 133 | ulong key = query_keys[tid]; | |
| 134 | ulong mask = capacity - 1; | |
| 135 | ulong slot = ht_hash(key) & mask; | |
| 136 | ||
| 137 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 138 | { | |
| 139 | ulong slot_key = table_keys[slot]; | |
| 140 | ||
| 141 | if (slot_key == key) | |
| 142 | { | |
| 143 | /* Found — read value */ | |
| 144 | out_values[tid] = table_values[slot]; | |
| 145 | return; | |
| 146 | } | |
| 147 | if (slot_key == HT_EMPTY_KEY) | |
| 148 | { | |
| 149 | /* Empty slot — key not in table */ | |
| 150 | out_values[tid] = HT_EMPTY_VALUE; | |
| 151 | return; | |
| 152 | } | |
| 153 | ||
| 154 | /* Different key — linear probe */ | |
| 155 | slot = (slot + 1) & mask; | |
| 156 | } | |
| 157 | /* Max probes exhausted — not found */ | |
| 158 | out_values[tid] = HT_EMPTY_VALUE; | |
| 159 | } | |
| 160 | ||
| 161 | /* ─── Delete ─── */ | |
| 162 | ||
| 163 | /* | |
| 164 | * Delete keys from the hash table. | |
| 165 | * | |
| 166 | * Tombstone deletion: key stays in table, value set to HT_EMPTY_VALUE. | |
| 167 | * Preserves probe chains for other keys. Deleted slots are NOT reused | |
| 168 | * for new inserts (keeps the algorithm simple). | |
| 169 | * | |
| 170 | * Args: | |
| 171 | * table_keys - key array [capacity] (ulong) | |
| 172 | * table_values - value array [capacity] (uint) | |
| 173 | * capacity - table size (must be power of 2) | |
| 174 | * del_keys - keys to delete [num_deletes] | |
| 175 | * num_deletes - number of deletions | |
| 176 | */ | |
| 177 | __kernel void ht_delete( | |
| 178 | __global const ulong* table_keys, | |
| 179 | __global uint* table_values, | |
| 180 | const ulong capacity, | |
| 181 | __global const ulong* del_keys, | |
| 182 | const uint num_deletes) | |
| 183 | { | |
| 184 | uint tid = get_global_id(0); | |
| 185 | if (tid >= num_deletes) return; | |
| 186 | ||
| 187 | ulong key = del_keys[tid]; | |
| 188 | ulong mask = capacity - 1; | |
| 189 | ulong slot = ht_hash(key) & mask; | |
| 190 | ||
| 191 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 192 | { | |
| 193 | ulong slot_key = table_keys[slot]; | |
| 194 | ||
| 195 | if (slot_key == key) | |
| 196 | { | |
| 197 | /* Found — tombstone the value */ | |
| 198 | table_values[slot] = HT_EMPTY_VALUE; | |
| 199 | return; | |
| 200 | } | |
| 201 | if (slot_key == HT_EMPTY_KEY) | |
| 202 | { | |
| 203 | /* Not in table */ | |
| 204 | return; | |
| 205 | } | |
| 206 | ||
| 207 | slot = (slot + 1) & mask; | |
| 208 | } | |
| 209 | } | |
| 210 | ||
| 211 | /* ─── Insert-or-increment ─── */ | |
| 212 | ||
| 213 | /* | |
| 214 | * Insert key with value, or atomically increment existing value. | |
| 215 | * This is the core operation for AtomSpace counting: if the pair | |
| 216 | * atom already exists, increment its count. Otherwise create it. | |
| 217 | * | |
| 218 | * IMPORTANT: table_values must be initialized to 0 (not 0xFF) | |
| 219 | * before using this kernel. Both the "new slot" and "existing slot" | |
| 220 | * paths use atomic_add(1), which avoids the race between CAS on | |
| 221 | * the key and write to the value. | |
| 222 | * | |
| 223 | * Args: | |
| 224 | * table_keys - key array [capacity] (ulong) | |
| 225 | * table_values - value array [capacity] (uint, init to 0) | |
| 226 | * capacity - table size (must be power of 2) | |
| 227 | * in_keys - keys to insert/increment [num_items] | |
| 228 | * num_items - number of items | |
| 229 | */ | |
| 230 | __kernel void ht_insert_or_increment( | |
| 231 | __global volatile ulong* table_keys, | |
| 232 | __global volatile uint* table_values, | |
| 233 | const ulong capacity, | |
| 234 | __global const ulong* in_keys, | |
| 235 | const uint num_items) | |
| 236 | { | |
| 237 | uint tid = get_global_id(0); | |
| 238 | if (tid >= num_items) return; | |
| 239 | ||
| 240 | ulong key = in_keys[tid]; | |
| 241 | if (key == HT_EMPTY_KEY) return; | |
| 242 | ||
| 243 | ulong mask = capacity - 1; | |
| 244 | ulong slot = ht_hash(key) & mask; | |
| 245 | ||
| 246 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 247 | { | |
| 248 | ulong prev = atom_cmpxchg( | |
| 249 | &table_keys[slot], HT_EMPTY_KEY, key); | |
| 250 | ||
| 251 | if (prev == HT_EMPTY_KEY || prev == key) | |
| 252 | { | |
| 253 | /* Slot is ours (new or existing) — atomic increment. | |
| 254 | * Both paths do the same thing: no race between | |
| 255 | * key placement and value update. */ | |
| 256 | atomic_add(&table_values[slot], 1U); | |
| 257 | return; | |
| 258 | } | |
| 259 | ||
| 260 | slot = (slot + 1) & mask; | |
| 261 | } | |
| 262 | } | |
| 263 | ||
| 264 | /* ─── Iterate (compact non-empty entries) ─── */ | |
| 265 | ||
| 266 | /* | |
| 267 | * Collect all non-empty, non-deleted entries into output arrays. | |
| 268 | * Uses atomic counter to pack results contiguously. | |
| 269 | * | |
| 270 | * Args: | |
| 271 | * table_keys - key array [capacity] (ulong) | |
| 272 | * table_values - value array [capacity] (uint) | |
| 273 | * capacity - table size | |
| 274 | * out_keys - output keys [max_output] | |
| 275 | * out_values - output values [max_output] | |
| 276 | * out_count - atomic counter (single uint, initialized to 0) | |
| 277 | * max_output - max entries to output | |
| 278 | */ | |
| 279 | __kernel void ht_iterate( | |
| 280 | __global const ulong* table_keys, | |
| 281 | __global const uint* table_values, | |
| 282 | const ulong capacity, | |
| 283 | __global ulong* out_keys, | |
| 284 | __global uint* out_values, | |
| 285 | __global uint* out_count, | |
| 286 | const uint max_output) | |
| 287 | { | |
| 288 | uint tid = get_global_id(0); | |
| 289 | if (tid >= capacity) return; | |
| 290 | ||
| 291 | ulong key = table_keys[tid]; | |
| 292 | uint val = table_values[tid]; | |
| 293 | ||
| 294 | if (key != HT_EMPTY_KEY && val != HT_EMPTY_VALUE) | |
| 295 | { | |
| 296 | uint idx = atomic_add(out_count, 1U); | |
| 297 | if (idx < max_output) | |
| 298 | { | |
| 299 | out_keys[idx] = key; | |
| 300 | out_values[idx] = val; | |
| 301 | } | |
| 302 | } | |
| 303 | } |
| … | … | @@ -1,215 +0,0 @@ |
| 1 | /* gpu-learning-bridge.cu — CUDA bridge for Scheme/C integration | |
| 2 | * | |
| 3 | * Provides a simplified C API that wraps the persistent learning kernel. | |
| 4 | * Manages unified memory allocation, kernel launch, sentence feeding, | |
| 5 | * and result readback. | |
| 6 | * | |
| 7 | * Designed to be compiled into libgpu-learning.so and loaded via | |
| 8 | * Guile's (load-extension) or dlopen. | |
| 9 | * | |
| 10 | * Build: | |
| 11 | * nvcc -O2 -arch=sm_75 -rdc=true --shared -Xcompiler -fPIC \ | |
| 12 | * -o libgpu-learning.so gpu-learning-bridge.cu gpu-learning-loop.cu \ | |
| 13 | * -lcudadevrt -lm | |
| 14 | */ | |
| 15 | ||
| 16 | #include <cstdint> | |
| 17 | #include <cstdio> | |
| 18 | #include <cstdlib> | |
| 19 | #include <cstring> | |
| 20 | #include "gpu-learning-types.h" | |
| 21 | ||
| 22 | extern "C" { | |
| 23 | LearningState* ll_init(uint32_t num_words); | |
| 24 | SentenceRing* ll_init_ring(); | |
| 25 | void ll_feed_sentence(SentenceRing* ring, uint32_t* words, uint32_t length); | |
| 26 | int ll_launch(LearningState* state, SentenceRing* ring, | |
| 27 | int* done_flag, int* pause_flag, | |
| 28 | uint32_t* stats_iteration, uint32_t* stats_pairs, | |
| 29 | uint32_t* stats_classes, double* stats_entropy); | |
| 30 | void ll_wait(); | |
| 31 | void ll_read_classes(LearningState* state, uint32_t* out, uint32_t n); | |
| 32 | void ll_shutdown(LearningState* state, SentenceRing* ring); | |
| 33 | } | |
| 34 | ||
| 35 | /* ═══════════════════════════════════════════════════════════════ | |
| 36 | * BRIDGE: Manages the full lifecycle | |
| 37 | * ═══════════════════════════════════════════════════════════════ */ | |
| 38 | ||
| 39 | static LearningState* g_state = NULL; | |
| 40 | static SentenceRing* g_ring = NULL; | |
| 41 | static int* g_done_flag = NULL; | |
| 42 | static int* g_pause_flag = NULL; | |
| 43 | static uint32_t* g_stats_iteration = NULL; | |
| 44 | static uint32_t* g_stats_pairs = NULL; | |
| 45 | static uint32_t* g_stats_classes = NULL; | |
| 46 | static double* g_stats_entropy = NULL; | |
| 47 | static int g_launched = 0; | |
| 48 | ||
| 49 | extern "C" { | |
| 50 | ||
| 51 | /* Initialize the GPU learning system */ | |
| 52 | int gpu_cuda_init(uint32_t num_words) { | |
| 53 | if (g_state) { | |
| 54 | printf("gpu_cuda_init: already initialized\n"); | |
| 55 | return -1; | |
| 56 | } | |
| 57 | ||
| 58 | /* Check CUDA device */ | |
| 59 | int device_count = 0; | |
| 60 | cudaGetDeviceCount(&device_count); | |
| 61 | if (device_count == 0) { | |
| 62 | printf("gpu_cuda_init: no CUDA devices found\n"); | |
| 63 | return -1; | |
| 64 | } | |
| 65 | ||
| 66 | int dev = 0; | |
| 67 | int supports_coop = 0; | |
| 68 | cudaDeviceGetAttribute(&supports_coop, cudaDevAttrCooperativeLaunch, dev); | |
| 69 | if (!supports_coop) { | |
| 70 | printf("gpu_cuda_init: device does not support cooperative launch\n"); | |
| 71 | return -1; | |
| 72 | } | |
| 73 | ||
| 74 | /* Allocate managed memory for control flags */ | |
| 75 | cudaMallocManaged(&g_done_flag, sizeof(int)); | |
| 76 | cudaMallocManaged(&g_pause_flag, sizeof(int)); | |
| 77 | cudaMallocManaged(&g_stats_iteration, sizeof(uint32_t)); | |
| 78 | cudaMallocManaged(&g_stats_pairs, sizeof(uint32_t)); | |
| 79 | cudaMallocManaged(&g_stats_classes, sizeof(uint32_t)); | |
| 80 | cudaMallocManaged(&g_stats_entropy, sizeof(double)); | |
| 81 | ||
| 82 | *g_done_flag = 0; | |
| 83 | *g_pause_flag = 1; /* start paused */ | |
| 84 | *g_stats_iteration = 0; | |
| 85 | *g_stats_pairs = 0; | |
| 86 | *g_stats_classes = 0; | |
| 87 | *g_stats_entropy = 0.0; | |
| 88 | ||
| 89 | g_state = ll_init(num_words); | |
| 90 | g_ring = ll_init_ring(); | |
| 91 | g_launched = 0; | |
| 92 | ||
| 93 | printf("gpu_cuda_init: initialized for %u words\n", num_words); | |
| 94 | return 0; | |
| 95 | } | |
| 96 | ||
| 97 | /* Launch the persistent learning kernel (runs in background) */ | |
| 98 | int gpu_cuda_launch_learning() { | |
| 99 | if (!g_state || !g_ring) { | |
| 100 | printf("gpu_cuda_launch: not initialized\n"); | |
| 101 | return -1; | |
| 102 | } | |
| 103 | if (g_launched) { | |
| 104 | printf("gpu_cuda_launch: already running\n"); | |
| 105 | return -1; | |
| 106 | } | |
| 107 | ||
| 108 | *g_done_flag = 0; | |
| 109 | *g_pause_flag = 0; /* unpause */ | |
| 110 | ||
| 111 | int err = ll_launch(g_state, g_ring, | |
| 112 | g_done_flag, g_pause_flag, | |
| 113 | g_stats_iteration, g_stats_pairs, | |
| 114 | g_stats_classes, g_stats_entropy); | |
| 115 | if (err == 0) { | |
| 116 | g_launched = 1; | |
| 117 | printf("gpu_cuda_launch: persistent kernel started\n"); | |
| 118 | } | |
| 119 | return err; | |
| 120 | } | |
| 121 | ||
| 122 | /* Feed a sentence to the GPU (non-blocking, CPU writes to unified memory) */ | |
| 123 | void gpu_cuda_feed_sentences(uint32_t* words, uint32_t length) { | |
| 124 | if (!g_ring) return; | |
| 125 | ll_feed_sentence(g_ring, words, length); | |
| 126 | } | |
| 127 | ||
| 128 | /* Pause/resume processing (kernel stays running but skips stages) */ | |
| 129 | void gpu_cuda_pause() { | |
| 130 | if (g_pause_flag) *g_pause_flag = 1; | |
| 131 | } | |
| 132 | ||
| 133 | void gpu_cuda_resume() { | |
| 134 | if (g_pause_flag) *g_pause_flag = 0; | |
| 135 | } | |
| 136 | ||
| 137 | /* Poll current status (non-blocking) */ | |
| 138 | int gpu_cuda_poll_status( | |
| 139 | uint32_t* out_iteration, | |
| 140 | uint32_t* out_pairs, | |
| 141 | uint32_t* out_classes, | |
| 142 | double* out_entropy, | |
| 143 | int* out_done | |
| 144 | ) { | |
| 145 | if (!g_state) return -1; | |
| 146 | ||
| 147 | if (out_iteration) *out_iteration = *g_stats_iteration; | |
| 148 | if (out_pairs) *out_pairs = *g_stats_pairs; | |
| 149 | if (out_classes) *out_classes = *g_stats_classes; | |
| 150 | if (out_entropy) *out_entropy = *g_stats_entropy; | |
| 151 | if (out_done) *out_done = *g_done_flag; | |
| 152 | ||
| 153 | return 0; | |
| 154 | } | |
| 155 | ||
| 156 | /* Signal the kernel to stop */ | |
| 157 | void gpu_cuda_stop() { | |
| 158 | if (g_done_flag) *g_done_flag = 1; | |
| 159 | } | |
| 160 | ||
| 161 | /* Wait for kernel to finish and read class assignments */ | |
| 162 | int gpu_cuda_read_classes(uint32_t* out_class_ids, uint32_t num_words) { | |
| 163 | if (!g_state) return -1; | |
| 164 | ||
| 165 | /* Wait for kernel */ | |
| 166 | ll_wait(); | |
| 167 | g_launched = 0; | |
| 168 | ||
| 169 | ll_read_classes(g_state, out_class_ids, num_words); | |
| 170 | return 0; | |
| 171 | } | |
| 172 | ||
| 173 | /* Read grammar costs (placeholder — actual grammar cost computation | |
| 174 | * is done by gpu-connector-rewrite.cu kernels, called after the | |
| 175 | * persistent kernel finishes) */ | |
| 176 | int gpu_cuda_read_grammar(double* out_costs, uint32_t num_sections) { | |
| 177 | /* Not implemented yet — requires running connector-rewrite pipeline | |
| 178 | * after persistent kernel produces classes */ | |
| 179 | (void)out_costs; | |
| 180 | (void)num_sections; | |
| 181 | return -1; | |
| 182 | } | |
| 183 | ||
| 184 | /* Cleanup everything */ | |
| 185 | void gpu_cuda_shutdown() { | |
| 186 | if (g_launched) { | |
| 187 | gpu_cuda_stop(); | |
| 188 | ll_wait(); | |
| 189 | g_launched = 0; | |
| 190 | } | |
| 191 | ||
| 192 | if (g_state && g_ring) { | |
| 193 | ll_shutdown(g_state, g_ring); | |
| 194 | } | |
| 195 | g_state = NULL; | |
| 196 | g_ring = NULL; | |
| 197 | ||
| 198 | if (g_done_flag) cudaFree(g_done_flag); | |
| 199 | if (g_pause_flag) cudaFree(g_pause_flag); | |
| 200 | if (g_stats_iteration) cudaFree(g_stats_iteration); | |
| 201 | if (g_stats_pairs) cudaFree(g_stats_pairs); | |
| 202 | if (g_stats_classes) cudaFree(g_stats_classes); | |
| 203 | if (g_stats_entropy) cudaFree(g_stats_entropy); | |
| 204 | ||
| 205 | g_done_flag = NULL; | |
| 206 | g_pause_flag = NULL; | |
| 207 | g_stats_iteration = NULL; | |
| 208 | g_stats_pairs = NULL; | |
| 209 | g_stats_classes = NULL; | |
| 210 | g_stats_entropy = NULL; | |
| 211 | ||
| 212 | printf("gpu_cuda_shutdown: cleanup complete\n"); | |
| 213 | } | |
| 214 | ||
| 215 | } /* extern "C" */ |
| … | … | @@ -1,526 +0,0 @@ |
| 1 | /* gpu-learning-loop.cu — Persistent CUDA learning kernel | |
| 2 | * | |
| 3 | * The ENTIRE learning pipeline as ONE persistent kernel launched with | |
| 4 | * cudaLaunchCooperativeKernel. CPU only feeds sentences via unified | |
| 5 | * memory and polls convergence. All data stays GPU-resident. | |
| 6 | * | |
| 7 | * Pipeline stages (conditional execution): | |
| 8 | * 1. Count pairs from new sentences | |
| 9 | * 2. Compute MI on dirty pairs | |
| 10 | * 3. Cosine similarity (norms → chains → dots → filter) | |
| 11 | * 4. Connected components clustering | |
| 12 | * 5. Grammar costs + class MI | |
| 13 | * 6. Parse + surprise | |
| 14 | * 7. Convergence check (entropy plateau) | |
| 15 | * | |
| 16 | * Requires: CUDA cooperative groups (CC 7.5+, RTX 2070) | |
| 17 | * | |
| 18 | * Build: nvcc -O2 -arch=sm_75 -rdc=true -c gpu-learning-loop.cu | |
| 19 | */ | |
| 20 | ||
| 21 | #include <cstdio> | |
| 22 | #include <cfloat> | |
| 23 | #include <cmath> | |
| 24 | #include <cooperative_groups.h> | |
| 25 | #include "gpu-learning-types.h" | |
| 26 | ||
| 27 | namespace cg = cooperative_groups; | |
| 28 | ||
| 29 | /* ─── Hash function ─── */ | |
| 30 | ||
| 31 | __device__ __forceinline__ | |
| 32 | uint64_t ll_hash(uint64_t key) { | |
| 33 | key ^= key >> 30; | |
| 34 | key *= 0xBF58476D1CE4E5B9ULL; | |
| 35 | key ^= key >> 27; | |
| 36 | key *= 0x94D049BB133111EBULL; | |
| 37 | key ^= key >> 31; | |
| 38 | return key; | |
| 39 | } | |
| 40 | ||
| 41 | /* ─── Atomic double add ─── */ | |
| 42 | ||
| 43 | __device__ __forceinline__ | |
| 44 | void ll_atomic_add_double(double* addr, double val) { | |
| 45 | unsigned long long int* a = (unsigned long long int*)addr; | |
| 46 | unsigned long long int old = *a, assumed; | |
| 47 | do { | |
| 48 | assumed = old; | |
| 49 | old = atomicCAS(a, assumed, | |
| 50 | __double_as_longlong(__longlong_as_double(assumed) + val)); | |
| 51 | } while (assumed != old); | |
| 52 | } | |
| 53 | ||
| 54 | /* Structs defined in gpu-learning-types.h */ | |
| 55 | ||
| 56 | /* ═══════════════════════════════════════════════════════════════ | |
| 57 | * DEVICE FUNCTIONS (called inside persistent kernel) | |
| 58 | * ═══════════════════════════════════════════════════════════════ */ | |
| 59 | ||
| 60 | /* ─── Find or create pair in hash table ─── */ | |
| 61 | ||
| 62 | __device__ | |
| 63 | uint32_t ll_find_or_create_pair(LearningState* state, uint32_t wa, uint32_t wb) { | |
| 64 | uint32_t lo = (wa <= wb) ? wa : wb; | |
| 65 | uint32_t hi = (wa <= wb) ? wb : wa; | |
| 66 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 67 | uint64_t mask = LL_PAIR_HT_CAPACITY - 1; | |
| 68 | uint64_t slot = ll_hash(key) & mask; | |
| 69 | ||
| 70 | for (int probe = 0; probe < 4096; probe++) { | |
| 71 | uint64_t existing = atomicCAS( | |
| 72 | (unsigned long long int*)&state->pair_ht_keys[slot], | |
| 73 | LL_EMPTY_KEY, key); | |
| 74 | ||
| 75 | if (existing == LL_EMPTY_KEY) { | |
| 76 | /* New slot — allocate pair */ | |
| 77 | uint32_t idx = atomicAdd(&state->pair_count_u32, 1U); | |
| 78 | if (idx >= LL_MAX_PAIRS) return LL_EMPTY_VALUE; | |
| 79 | state->pair_word_a[idx] = lo; | |
| 80 | state->pair_word_b[idx] = hi; | |
| 81 | state->pair_count[idx] = 0.0; | |
| 82 | state->pair_mi[idx] = 0.0; | |
| 83 | state->pair_dirty[idx] = 0; | |
| 84 | __threadfence(); | |
| 85 | state->pair_ht_values[slot] = idx; | |
| 86 | return idx; | |
| 87 | } | |
| 88 | if (existing == key) { | |
| 89 | /* Existing slot — wait for value if needed */ | |
| 90 | uint32_t val = state->pair_ht_values[slot]; | |
| 91 | int safety = 1000; | |
| 92 | while (val == LL_EMPTY_VALUE && safety-- > 0) { | |
| 93 | val = state->pair_ht_values[slot]; | |
| 94 | } | |
| 95 | return val; | |
| 96 | } | |
| 97 | slot = (slot + 1) & mask; | |
| 98 | } | |
| 99 | return LL_EMPTY_VALUE; | |
| 100 | } | |
| 101 | ||
| 102 | /* ─── Count pairs from one sentence ─── */ | |
| 103 | ||
| 104 | __device__ | |
| 105 | void ll_count_sentence(LearningState* state, uint32_t* words, uint32_t len) { | |
| 106 | /* Sliding window: all pairs within window of 2 */ | |
| 107 | for (uint32_t i = 0; i < len; i++) { | |
| 108 | for (uint32_t j = i + 1; j < len && j <= i + 2; j++) { | |
| 109 | uint32_t idx = ll_find_or_create_pair(state, words[i], words[j]); | |
| 110 | if (idx == LL_EMPTY_VALUE) continue; | |
| 111 | ||
| 112 | ll_atomic_add_double(&state->pair_count[idx], 1.0); | |
| 113 | ll_atomic_add_double(&state->word_marginal[words[i]], 1.0); | |
| 114 | ll_atomic_add_double(&state->word_marginal[words[j]], 1.0); | |
| 115 | ll_atomic_add_double(&state->total_pair_observations, 1.0); | |
| 116 | ||
| 117 | if (atomicExch(&state->pair_dirty[idx], 1U) == 0U) { | |
| 118 | atomicAdd(&state->dirty_count, 1U); | |
| 119 | } | |
| 120 | } | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | /* ─── Compute MI for all dirty pairs ─── */ | |
| 125 | ||
| 126 | __device__ | |
| 127 | void ll_compute_mi_dirty(LearningState* state, cg::grid_group grid) { | |
| 128 | double n = state->total_pair_observations; | |
| 129 | double log2_factor = 1.4426950408889634; | |
| 130 | double eps = 1e-10; | |
| 131 | ||
| 132 | uint32_t num_pairs = state->pair_count_u32; | |
| 133 | uint32_t tid = grid.thread_rank(); | |
| 134 | uint32_t stride = grid.size(); | |
| 135 | ||
| 136 | for (uint32_t p = tid; p < num_pairs; p += stride) { | |
| 137 | if (state->pair_dirty[p] != 1) continue; | |
| 138 | ||
| 139 | double count = state->pair_count[p]; | |
| 140 | if (count < 1.0) { | |
| 141 | state->pair_mi[p] = 0.0; | |
| 142 | state->pair_dirty[p] = 0; | |
| 143 | continue; | |
| 144 | } | |
| 145 | ||
| 146 | uint32_t wa = state->pair_word_a[p]; | |
| 147 | uint32_t wb = state->pair_word_b[p]; | |
| 148 | double left = fmax(state->word_marginal[wa], eps); | |
| 149 | double right = fmax(state->word_marginal[wb], eps); | |
| 150 | double n_safe = fmax(n, eps); | |
| 151 | ||
| 152 | double mi = (log(count) + log(n_safe) - log(left) - log(right)) * log2_factor; | |
| 153 | state->pair_mi[p] = mi; | |
| 154 | state->pair_dirty[p] = 0; | |
| 155 | } | |
| 156 | ||
| 157 | if (tid == 0) { | |
| 158 | state->dirty_count = 0; | |
| 159 | state->mi_updated = 1; | |
| 160 | } | |
| 161 | } | |
| 162 | ||
| 163 | /* ─── Simple connected components (for the persistent kernel) ─── */ | |
| 164 | ||
| 165 | __device__ | |
| 166 | void ll_connected_components(LearningState* state, cg::grid_group grid) { | |
| 167 | uint32_t num_words = state->word_count_u32; | |
| 168 | uint32_t num_pairs = state->pair_count_u32; | |
| 169 | uint32_t tid = grid.thread_rank(); | |
| 170 | uint32_t stride = grid.size(); | |
| 171 | ||
| 172 | /* Build edge list from pairs with positive MI and high cosine */ | |
| 173 | /* For simplicity in the persistent kernel, we use MI-based clustering: | |
| 174 | * connect words that share high MI neighbors (simplified cosine proxy) */ | |
| 175 | if (tid == 0) state->cc_edge_count = 0; | |
| 176 | grid.sync(); | |
| 177 | ||
| 178 | /* Filter pairs with MI > threshold into edge list */ | |
| 179 | for (uint32_t p = tid; p < num_pairs; p += stride) { | |
| 180 | if (state->pair_mi[p] > 2.0 && state->pair_count[p] > 3.0) { | |
| 181 | uint32_t idx = atomicAdd(&state->cc_edge_count, 1U); | |
| 182 | if (idx < LL_CC_MAX_EDGES) { | |
| 183 | state->cc_edge_a[idx] = state->pair_word_a[p]; | |
| 184 | state->cc_edge_b[idx] = state->pair_word_b[p]; | |
| 185 | } | |
| 186 | } | |
| 187 | } | |
| 188 | grid.sync(); | |
| 189 | ||
| 190 | uint32_t num_edges = min(state->cc_edge_count, (uint32_t)LL_CC_MAX_EDGES); | |
| 191 | ||
| 192 | /* Init labels */ | |
| 193 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 194 | state->cc_labels[w] = w; | |
| 195 | } | |
| 196 | grid.sync(); | |
| 197 | ||
| 198 | /* Propagate + compress loop | |
| 199 | * CRITICAL: All threads must execute the same number of grid.sync() calls. | |
| 200 | * Use a global changed flag instead of __shared__ + break. */ | |
| 201 | for (int iter = 0; iter < 32; iter++) { | |
| 202 | /* Reset changed flag */ | |
| 203 | if (tid == 0) state->cc_changed = 0; | |
| 204 | grid.sync(); | |
| 205 | ||
| 206 | for (uint32_t e = tid; e < num_edges; e += stride) { | |
| 207 | uint32_t u = state->cc_edge_a[e]; | |
| 208 | uint32_t v = state->cc_edge_b[e]; | |
| 209 | uint32_t lu = state->cc_labels[u]; | |
| 210 | uint32_t lv = state->cc_labels[v]; | |
| 211 | if (lu != lv) { | |
| 212 | uint32_t hi = (lu > lv) ? lu : lv; | |
| 213 | uint32_t lo = (lu > lv) ? lv : lu; | |
| 214 | uint32_t old = atomicMin(&state->cc_labels[hi], lo); | |
| 215 | if (old != lo) state->cc_changed = 1; | |
| 216 | } | |
| 217 | } | |
| 218 | grid.sync(); | |
| 219 | ||
| 220 | /* Compress */ | |
| 221 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 222 | uint32_t l = state->cc_labels[w]; | |
| 223 | while (state->cc_labels[l] != l) l = state->cc_labels[l]; | |
| 224 | state->cc_labels[w] = l; | |
| 225 | } | |
| 226 | grid.sync(); | |
| 227 | ||
| 228 | /* All threads check the same global flag — no divergent control flow */ | |
| 229 | if (!state->cc_changed) break; | |
| 230 | } | |
| 231 | ||
| 232 | /* Assign class IDs */ | |
| 233 | if (tid == 0) state->num_classes = 0; | |
| 234 | grid.sync(); | |
| 235 | ||
| 236 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 237 | if (state->cc_labels[w] == w) { | |
| 238 | uint32_t cid = atomicAdd(&state->num_classes, 1U); | |
| 239 | state->word_class_id[w] = cid; | |
| 240 | } else { | |
| 241 | state->word_class_id[w] = LL_EMPTY_VALUE; | |
| 242 | } | |
| 243 | } | |
| 244 | grid.sync(); | |
| 245 | ||
| 246 | /* Map non-roots */ | |
| 247 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 248 | if (state->word_class_id[w] == LL_EMPTY_VALUE) { | |
| 249 | uint32_t root = state->cc_labels[w]; | |
| 250 | state->word_class_id[w] = state->word_class_id[root]; | |
| 251 | } | |
| 252 | } | |
| 253 | grid.sync(); | |
| 254 | ||
| 255 | if (tid == 0) state->classes_updated = 1; | |
| 256 | } | |
| 257 | ||
| 258 | /* ─── Compute class distribution entropy ─── */ | |
| 259 | /* Uses the cc_labels array (no longer needed after CC) as scratch space | |
| 260 | * instead of allocating a huge stack array. */ | |
| 261 | ||
| 262 | __device__ | |
| 263 | double ll_compute_entropy(LearningState* state, cg::grid_group grid) { | |
| 264 | uint32_t tid = grid.thread_rank(); | |
| 265 | uint32_t stride = grid.size(); | |
| 266 | uint32_t num_words = state->word_count_u32; | |
| 267 | uint32_t nc = state->num_classes; | |
| 268 | ||
| 269 | if (nc == 0) return 0.0; | |
| 270 | ||
| 271 | /* Reuse cc_labels as class size counters (zeroed by all threads) */ | |
| 272 | for (uint32_t i = tid; i < nc; i += stride) { | |
| 273 | state->cc_labels[i] = 0; | |
| 274 | } | |
| 275 | grid.sync(); | |
| 276 | ||
| 277 | /* Count class sizes in parallel */ | |
| 278 | for (uint32_t w = tid; w < num_words; w += stride) { | |
| 279 | uint32_t cid = state->word_class_id[w]; | |
| 280 | if (cid < nc) { | |
| 281 | atomicAdd(&state->cc_labels[cid], 1U); | |
| 282 | } | |
| 283 | } | |
| 284 | grid.sync(); | |
| 285 | ||
| 286 | /* Compute entropy on thread 0 (nc is typically small) */ | |
| 287 | double entropy = 0.0; | |
| 288 | if (tid == 0) { | |
| 289 | double n = (double)num_words; | |
| 290 | for (uint32_t i = 0; i < nc; i++) { | |
| 291 | uint32_t sz = state->cc_labels[i]; | |
| 292 | if (sz > 0) { | |
| 293 | double p = (double)sz / n; | |
| 294 | entropy -= p * log2(p); | |
| 295 | } | |
| 296 | } | |
| 297 | } | |
| 298 | return entropy; | |
| 299 | } | |
| 300 | ||
| 301 | /* ═══════════════════════════════════════════════════════════════ | |
| 302 | * PERSISTENT KERNEL | |
| 303 | * | |
| 304 | * Launched once with cudaLaunchCooperativeKernel. | |
| 305 | * Runs until convergence or max iterations. | |
| 306 | * CPU feeds sentences via unified memory ring buffer. | |
| 307 | * ═══════════════════════════════════════════════════════════════ */ | |
| 308 | ||
| 309 | __global__ void learning_loop_kernel( | |
| 310 | LearningState* state, | |
| 311 | SentenceRing* ring, | |
| 312 | volatile int* done_flag, | |
| 313 | volatile int* pause_flag, /* CPU can pause processing */ | |
| 314 | volatile uint32_t* stats_iteration, | |
| 315 | volatile uint32_t* stats_pairs, | |
| 316 | volatile uint32_t* stats_classes, | |
| 317 | volatile double* stats_entropy | |
| 318 | ) { | |
| 319 | auto grid = cg::this_grid(); | |
| 320 | uint32_t tid = grid.thread_rank(); | |
| 321 | ||
| 322 | /* CRITICAL: With cooperative groups, ALL threads must hit every grid.sync(). | |
| 323 | * No conditional blocks around grid.sync(). Stages always run; | |
| 324 | * threads skip their work when the stage condition is false. */ | |
| 325 | ||
| 326 | while (!(*done_flag)) { | |
| 327 | /* ─── Stage 1: Ingest sentences (thread 0 only, no grid.sync needed) ─── */ | |
| 328 | if (tid == 0 && !(*pause_flag)) { | |
| 329 | int ingested = 0; | |
| 330 | /* Process up to 16 sentences per iteration to avoid starving GPU */ | |
| 331 | int max_ingest = 16; | |
| 332 | while (ring->read_idx != ring->write_idx && max_ingest-- > 0) { | |
| 333 | uint32_t ridx = ring->read_idx % LL_RING_SIZE; | |
| 334 | SentenceSlot* slot = &ring->slots[ridx]; | |
| 335 | ||
| 336 | if (slot->ready && slot->length >= 2) { | |
| 337 | ll_count_sentence(state, slot->words, slot->length); | |
| 338 | slot->ready = 0; | |
| 339 | ingested = 1; | |
| 340 | } | |
| 341 | ring->read_idx++; | |
| 342 | } | |
| 343 | if (ingested) state->has_new_sentences = 1; | |
| 344 | } | |
| 345 | grid.sync(); | |
| 346 | ||
| 347 | /* ─── Stage 2: MI computation (ALL threads enter, conditional work) ─── */ | |
| 348 | /* All threads enter ll_compute_mi_dirty regardless; threads with no dirty | |
| 349 | * pairs just skip their iterations in the parallel loop. */ | |
| 350 | int do_mi = (state->dirty_count >= LL_MI_THRESHOLD); | |
| 351 | if (do_mi) { | |
| 352 | ll_compute_mi_dirty(state, grid); | |
| 353 | } | |
| 354 | grid.sync(); | |
| 355 | ||
| 356 | /* ─── Stage 3: Connected components + entropy (ALL threads enter) ─── */ | |
| 357 | int do_cc = (state->mi_updated && state->pair_count_u32 > 10); | |
| 358 | if (do_cc) { | |
| 359 | ll_connected_components(state, grid); | |
| 360 | } | |
| 361 | grid.sync(); | |
| 362 | ||
| 363 | /* Entropy computation — all threads participate */ | |
| 364 | double entropy = 0.0; | |
| 365 | if (do_cc) { | |
| 366 | entropy = ll_compute_entropy(state, grid); | |
| 367 | } | |
| 368 | grid.sync(); | |
| 369 | ||
| 370 | /* Stats and convergence (thread 0 only, no grid.sync inside) */ | |
| 371 | if (tid == 0 && do_cc) { | |
| 372 | uint32_t eidx = state->entropy_idx % LL_ENTROPY_WINDOW; | |
| 373 | state->entropy_history[eidx] = entropy; | |
| 374 | state->entropy_idx++; | |
| 375 | ||
| 376 | *stats_iteration = state->iteration; | |
| 377 | *stats_pairs = state->pair_count_u32; | |
| 378 | *stats_classes = state->num_classes; | |
| 379 | *stats_entropy = entropy; | |
| 380 | ||
| 381 | state->iteration++; | |
| 382 | state->mi_updated = 0; | |
| 383 | ||
| 384 | /* Check entropy plateau */ | |
| 385 | if (state->entropy_idx >= LL_ENTROPY_WINDOW) { | |
| 386 | double min_e = 1e30, max_e = -1e30; | |
| 387 | for (int i = 0; i < LL_ENTROPY_WINDOW; i++) { | |
| 388 | double e = state->entropy_history[i]; | |
| 389 | if (e < min_e) min_e = e; | |
| 390 | if (e > max_e) max_e = e; | |
| 391 | } | |
| 392 | if (max_e > 0.0 && (max_e - min_e) / max_e < 0.01) { | |
| 393 | *done_flag = 1; | |
| 394 | } | |
| 395 | } | |
| 396 | ||
| 397 | if (state->iteration >= LL_MAX_ITERATIONS) { | |
| 398 | *done_flag = 1; | |
| 399 | } | |
| 400 | } | |
| 401 | ||
| 402 | if (tid == 0) state->has_new_sentences = 0; | |
| 403 | grid.sync(); | |
| 404 | } | |
| 405 | } | |
| 406 | ||
| 407 | /* ═══════════════════════════════════════════════════════════════ | |
| 408 | * HOST API (C interface for Scheme bridge) | |
| 409 | * ═══════════════════════════════════════════════════════════════ */ | |
| 410 | ||
| 411 | extern "C" { | |
| 412 | ||
| 413 | /* Initialize learning state */ | |
| 414 | LearningState* ll_init(uint32_t num_words) { | |
| 415 | LearningState* state; | |
| 416 | cudaMallocManaged(&state, sizeof(LearningState)); | |
| 417 | memset(state, 0, sizeof(LearningState)); | |
| 418 | ||
| 419 | state->word_count_u32 = num_words; | |
| 420 | state->pair_count_u32 = 0; | |
| 421 | state->dirty_count = 0; | |
| 422 | state->total_pair_observations = 0.0; | |
| 423 | state->num_classes = 0; | |
| 424 | state->iteration = 0; | |
| 425 | state->entropy_idx = 0; | |
| 426 | state->cc_edge_count = 0; | |
| 427 | ||
| 428 | /* Init hash table */ | |
| 429 | memset(state->pair_ht_keys, 0xFF, sizeof(state->pair_ht_keys)); | |
| 430 | memset(state->pair_ht_values, 0xFF, sizeof(state->pair_ht_values)); | |
| 431 | ||
| 432 | /* Init word data */ | |
| 433 | for (uint32_t i = 0; i < num_words; i++) { | |
| 434 | state->word_marginal[i] = 0.0; | |
| 435 | state->word_class_id[i] = LL_EMPTY_VALUE; | |
| 436 | } | |
| 437 | ||
| 438 | return state; | |
| 439 | } | |
| 440 | ||
| 441 | /* Initialize sentence ring buffer */ | |
| 442 | SentenceRing* ll_init_ring() { | |
| 443 | SentenceRing* ring; | |
| 444 | cudaMallocManaged(&ring, sizeof(SentenceRing)); | |
| 445 | memset(ring, 0, sizeof(SentenceRing)); | |
| 446 | return ring; | |
| 447 | } | |
| 448 | ||
| 449 | /* Feed a sentence (CPU writes to unified memory) */ | |
| 450 | void ll_feed_sentence(SentenceRing* ring, uint32_t* words, uint32_t length) { | |
| 451 | if (length > LL_MAX_SENTENCE_LEN) length = LL_MAX_SENTENCE_LEN; | |
| 452 | ||
| 453 | uint32_t widx = ring->write_idx % LL_RING_SIZE; | |
| 454 | SentenceSlot* slot = &ring->slots[widx]; | |
| 455 | ||
| 456 | /* Wait for slot to be consumed */ | |
| 457 | int wait = 0; | |
| 458 | while (slot->ready && wait < 1000000) { wait++; } | |
| 459 | ||
| 460 | memcpy(slot->words, words, length * sizeof(uint32_t)); | |
| 461 | slot->length = length; | |
| 462 | __sync_synchronize(); /* memory fence */ | |
| 463 | slot->ready = 1; | |
| 464 | ring->write_idx++; | |
| 465 | } | |
| 466 | ||
| 467 | /* Launch the persistent kernel */ | |
| 468 | int ll_launch( | |
| 469 | LearningState* state, | |
| 470 | SentenceRing* ring, | |
| 471 | int* done_flag, | |
| 472 | int* pause_flag, | |
| 473 | uint32_t* stats_iteration, | |
| 474 | uint32_t* stats_pairs, | |
| 475 | uint32_t* stats_classes, | |
| 476 | double* stats_entropy | |
| 477 | ) { | |
| 478 | /* Query max blocks for cooperative launch */ | |
| 479 | int num_blocks = 0; | |
| 480 | cudaOccupancyMaxActiveBlocksPerMultiprocessor( | |
| 481 | &num_blocks, learning_loop_kernel, 128, 0); | |
| 482 | int num_sms = 0; | |
| 483 | cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0); | |
| 484 | ||
| 485 | /* Use fewer blocks to leave room for other GPU work */ | |
| 486 | int total_blocks = num_sms; /* 1 block per SM */ | |
| 487 | int block_size = 128; | |
| 488 | ||
| 489 | dim3 grid_dim(total_blocks); | |
| 490 | dim3 block_dim(block_size); | |
| 491 | ||
| 492 | void* args[] = { | |
| 493 | &state, &ring, &done_flag, &pause_flag, | |
| 494 | &stats_iteration, &stats_pairs, &stats_classes, &stats_entropy | |
| 495 | }; | |
| 496 | ||
| 497 | cudaError_t err = cudaLaunchCooperativeKernel( | |
| 498 | (void*)learning_loop_kernel, grid_dim, block_dim, args); | |
| 499 | ||
| 500 | if (err != cudaSuccess) { | |
| 501 | printf("Cooperative kernel launch failed: %s\n", cudaGetErrorString(err)); | |
| 502 | return -1; | |
| 503 | } | |
| 504 | ||
| 505 | return 0; | |
| 506 | } | |
| 507 | ||
| 508 | /* Wait for kernel completion */ | |
| 509 | void ll_wait() { | |
| 510 | cudaDeviceSynchronize(); | |
| 511 | } | |
| 512 | ||
| 513 | /* Read class assignments (from unified memory — no copy needed) */ | |
| 514 | void ll_read_classes(LearningState* state, uint32_t* out_class_ids, uint32_t num_words) { | |
| 515 | /* Unified memory: just memcpy from managed pointer */ | |
| 516 | cudaDeviceSynchronize(); /* ensure GPU is done */ | |
| 517 | memcpy(out_class_ids, state->word_class_id, num_words * sizeof(uint32_t)); | |
| 518 | } | |
| 519 | ||
| 520 | /* Cleanup */ | |
| 521 | void ll_shutdown(LearningState* state, SentenceRing* ring) { | |
| 522 | cudaFree(state); | |
| 523 | cudaFree(ring); | |
| 524 | } | |
| 525 | ||
| 526 | } /* extern "C" */ |
| … | … | @@ -1,138 +0,0 @@ |
| 1 | /* gpu-learning-types.h — Shared type definitions for the learning loop | |
| 2 | * | |
| 3 | * Included by gpu-learning-loop.cu, gpu-learning-bridge.cu, and test files. | |
| 4 | */ | |
| 5 | ||
| 6 | #ifndef GPU_LEARNING_TYPES_H | |
| 7 | #define GPU_LEARNING_TYPES_H | |
| 8 | ||
| 9 | #include <cstdint> | |
| 10 | ||
| 11 | /* ─── Constants ─── */ | |
| 12 | ||
| 13 | #define LL_MAX_WORDS 8192 | |
| 14 | #define LL_MAX_PAIRS 262144 | |
| 15 | #define LL_PAIR_HT_CAPACITY 524288 | |
| 16 | #define LL_MAX_SENTENCE_LEN 64 | |
| 17 | #define LL_RING_SIZE 2048 | |
| 18 | #define LL_MI_THRESHOLD 100 | |
| 19 | #define LL_CC_MAX_EDGES 65536 | |
| 20 | #define LL_CC_THRESHOLD 0.15f | |
| 21 | #define LL_MAX_ITERATIONS 500 | |
| 22 | #define LL_ENTROPY_WINDOW 5 | |
| 23 | #define LL_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 24 | #define LL_EMPTY_VALUE 0xFFFFFFFFU | |
| 25 | ||
| 26 | /* ─── Compartment constants ─── */ | |
| 27 | ||
| 28 | #define LL_MAX_COMPARTMENTS 64 /* max SMs we support */ | |
| 29 | #define LL_COMP_MAX_PAIRS 32768 /* per-compartment pair slots */ | |
| 30 | #define LL_COMP_HT_CAPACITY 65536 /* per-compartment HT (2x pairs) */ | |
| 31 | #ifndef LL_MIN_SENTENCES | |
| 32 | #define LL_MIN_SENTENCES 500 /* min sentences before convergence check */ | |
| 33 | #endif | |
| 34 | #ifndef LL_MIN_CC_RUNS | |
| 35 | #define LL_MIN_CC_RUNS 5 /* min CC rounds before convergence check */ | |
| 36 | #endif | |
| 37 | #ifndef LL_MIN_CLASSES | |
| 38 | #define LL_MIN_CLASSES 10 /* min classes before convergence check */ | |
| 39 | #endif | |
| 40 | ||
| 41 | /* ═══════════════════════════════════════════════════════════════ | |
| 42 | * GPU-RESIDENT STATE (shared/merged) | |
| 43 | * ═══════════════════════════════════════════════════════════════ */ | |
| 44 | ||
| 45 | struct LearningState { | |
| 46 | /* Word pool */ | |
| 47 | uint32_t word_count_u32; | |
| 48 | double word_marginal[LL_MAX_WORDS]; | |
| 49 | uint32_t word_class_id[LL_MAX_WORDS]; | |
| 50 | ||
| 51 | /* Pair pool */ | |
| 52 | uint32_t pair_count_u32; | |
| 53 | uint32_t pair_word_a[LL_MAX_PAIRS]; | |
| 54 | uint32_t pair_word_b[LL_MAX_PAIRS]; | |
| 55 | double pair_count[LL_MAX_PAIRS]; | |
| 56 | double pair_mi[LL_MAX_PAIRS]; | |
| 57 | uint32_t pair_dirty[LL_MAX_PAIRS]; | |
| 58 | ||
| 59 | /* Pair hash table */ | |
| 60 | uint64_t pair_ht_keys[LL_PAIR_HT_CAPACITY]; | |
| 61 | uint32_t pair_ht_values[LL_PAIR_HT_CAPACITY]; | |
| 62 | ||
| 63 | /* Stats */ | |
| 64 | uint32_t dirty_count; | |
| 65 | double total_pair_observations; | |
| 66 | uint32_t num_classes; | |
| 67 | uint32_t iteration; | |
| 68 | ||
| 69 | /* Entropy */ | |
| 70 | double entropy_history[LL_ENTROPY_WINDOW]; | |
| 71 | uint32_t entropy_idx; | |
| 72 | ||
| 73 | /* CC work buffers */ | |
| 74 | uint32_t cc_edge_a[LL_CC_MAX_EDGES]; | |
| 75 | uint32_t cc_edge_b[LL_CC_MAX_EDGES]; | |
| 76 | uint32_t cc_labels[LL_MAX_WORDS]; | |
| 77 | uint32_t cc_edge_count; | |
| 78 | ||
| 79 | /* Pipeline flags */ | |
| 80 | int has_new_sentences; | |
| 81 | int mi_updated; | |
| 82 | int cosine_ready; | |
| 83 | int classes_updated; | |
| 84 | int cc_changed; /* used by CC convergence check */ | |
| 85 | ||
| 86 | /* Phase-gated convergence (compartment kernel) */ | |
| 87 | uint32_t total_sentences; /* cumulative across all rounds */ | |
| 88 | uint32_t cc_runs; /* how many times CC has run */ | |
| 89 | uint32_t batch_size; /* snapshot of available sentences */ | |
| 90 | uint32_t batch_start; /* ring read position at snapshot */ | |
| 91 | uint32_t num_compartments; /* actual number of SMs in use */ | |
| 92 | uint32_t idle_rounds; /* rounds with no new data */ | |
| 93 | }; | |
| 94 | ||
| 95 | /* ═══════════════════════════════════════════════════════════════ | |
| 96 | * PER-COMPARTMENT STATE (one per SM) | |
| 97 | * | |
| 98 | * Each SM accumulates pair counts independently — NO cross-SM | |
| 99 | * atomics during counting. Only the merge phase writes to shared | |
| 100 | * LearningState using atomics. | |
| 101 | * | |
| 102 | * ~1.5 MB per compartment × 36 SMs = ~54 MB total. | |
| 103 | * ═══════════════════════════════════════════════════════════════ */ | |
| 104 | ||
| 105 | struct CompartmentState { | |
| 106 | /* Per-compartment pair accumulation */ | |
| 107 | uint32_t pair_count; | |
| 108 | uint32_t pair_word_a[LL_COMP_MAX_PAIRS]; | |
| 109 | uint32_t pair_word_b[LL_COMP_MAX_PAIRS]; | |
| 110 | double pair_obs[LL_COMP_MAX_PAIRS]; | |
| 111 | ||
| 112 | /* Per-compartment hash table (pair key → local pair index) */ | |
| 113 | uint64_t pair_ht_keys[LL_COMP_HT_CAPACITY]; | |
| 114 | uint32_t pair_ht_values[LL_COMP_HT_CAPACITY]; | |
| 115 | ||
| 116 | /* Per-compartment marginals */ | |
| 117 | double word_marginal[LL_MAX_WORDS]; | |
| 118 | double total_observations; | |
| 119 | ||
| 120 | /* Stats */ | |
| 121 | uint32_t sentences_processed; | |
| 122 | double surprise_score; /* future: for surprise-weighted allocation */ | |
| 123 | }; | |
| 124 | ||
| 125 | /* Sentence ring buffer */ | |
| 126 | struct SentenceSlot { | |
| 127 | uint32_t words[LL_MAX_SENTENCE_LEN]; | |
| 128 | uint32_t length; | |
| 129 | uint32_t ready; | |
| 130 | }; | |
| 131 | ||
| 132 | struct SentenceRing { | |
| 133 | SentenceSlot slots[LL_RING_SIZE]; | |
| 134 | volatile uint32_t write_idx; | |
| 135 | volatile uint32_t read_idx; | |
| 136 | }; | |
| 137 | ||
| 138 | #endif /* GPU_LEARNING_TYPES_H */ |
| … | … | @@ -1,224 +0,0 @@ |
| 1 | /* | |
| 2 | * gpu-mi.cl -- MI computation on GPU-resident atom pools | |
| 3 | * | |
| 4 | * Computes mutual information directly from pair pool and word pool | |
| 5 | * buffers already resident in GPU memory. No CPU↔GPU data transfer | |
| 6 | * needed — the counting kernel (gpu-counting.cl) populates the same | |
| 7 | * buffers that this kernel reads. | |
| 8 | * | |
| 9 | * MI(x,y) = log2(count(x,y) * N / (count(x,*) * count(*,y))) | |
| 10 | * | |
| 11 | * Where: | |
| 12 | * count(x,y) = pair_count[pair_idx] | |
| 13 | * count(x,*) = word_count[pair_word_a[pair_idx]] (left marginal) | |
| 14 | * count(*,y) = word_count[pair_word_b[pair_idx]] (right marginal) | |
| 15 | * N = total pair observations (passed as scalar arg) | |
| 16 | * | |
| 17 | * Appended after gpu-hashtable.cl, gpu-atomspace.cl, gpu-counting.cl | |
| 18 | * at load time. | |
| 19 | */ | |
| 20 | ||
| 21 | /* ═══════════════════════════════════════════════════════════════ | |
| 22 | * COMPUTE MI FOR ALL PAIRS | |
| 23 | * | |
| 24 | * One thread per pair in the pool. Reads pair count and word | |
| 25 | * marginals directly from GPU-resident SoA arrays. | |
| 26 | * | |
| 27 | * Pairs with count < 1 get MI = 0. | |
| 28 | * ═══════════════════════════════════════════════════════════════ */ | |
| 29 | ||
| 30 | __kernel void compute_mi_resident( | |
| 31 | __global const double* pair_count, | |
| 32 | __global const uint* pair_word_a, | |
| 33 | __global const uint* pair_word_b, | |
| 34 | __global double* pair_mi, | |
| 35 | __global const double* word_count, | |
| 36 | const double n_total, | |
| 37 | const uint num_pairs) | |
| 38 | { | |
| 39 | uint tid = get_global_id(0); | |
| 40 | if (tid >= num_pairs) return; | |
| 41 | ||
| 42 | double count = pair_count[tid]; | |
| 43 | if (count < 1.0) { | |
| 44 | pair_mi[tid] = 0.0; | |
| 45 | return; | |
| 46 | } | |
| 47 | ||
| 48 | uint wa = pair_word_a[tid]; | |
| 49 | uint wb = pair_word_b[tid]; | |
| 50 | double left = word_count[wa]; | |
| 51 | double right = word_count[wb]; | |
| 52 | ||
| 53 | /* MI = log2(count * N / (left * right)) | |
| 54 | * = (ln(count) + ln(N) - ln(left) - ln(right)) / ln(2) */ | |
| 55 | double eps = 1e-10; | |
| 56 | left = fmax(left, eps); | |
| 57 | right = fmax(right, eps); | |
| 58 | double n = fmax(n_total, eps); | |
| 59 | ||
| 60 | double log2_factor = 1.4426950408889634; | |
| 61 | double mi = (log(count) + log(n) - log(left) - log(right)) * log2_factor; | |
| 62 | ||
| 63 | pair_mi[tid] = mi; | |
| 64 | } | |
| 65 | ||
| 66 | /* ═══════════════════════════════════════════════════════════════ | |
| 67 | * COMPUTE MI FOR DIRTY PAIRS ONLY | |
| 68 | * | |
| 69 | * Same as above but only processes pairs where flags == 1. | |
| 70 | * Clears the dirty flag after computation. | |
| 71 | * | |
| 72 | * Use this for incremental MI updates after new sentences | |
| 73 | * are counted — avoids recomputing all 2M+ pairs. | |
| 74 | * ═══════════════════════════════════════════════════════════════ */ | |
| 75 | ||
| 76 | __kernel void compute_mi_dirty( | |
| 77 | __global const double* pair_count, | |
| 78 | __global const uint* pair_word_a, | |
| 79 | __global const uint* pair_word_b, | |
| 80 | __global double* pair_mi, | |
| 81 | __global volatile uint* pair_flags, | |
| 82 | __global const double* word_count, | |
| 83 | const double n_total, | |
| 84 | const uint num_pairs) | |
| 85 | { | |
| 86 | uint tid = get_global_id(0); | |
| 87 | if (tid >= num_pairs) return; | |
| 88 | ||
| 89 | if (pair_flags[tid] != 1) return; | |
| 90 | ||
| 91 | double count = pair_count[tid]; | |
| 92 | if (count < 1.0) { | |
| 93 | pair_mi[tid] = 0.0; | |
| 94 | pair_flags[tid] = 0; | |
| 95 | return; | |
| 96 | } | |
| 97 | ||
| 98 | uint wa = pair_word_a[tid]; | |
| 99 | uint wb = pair_word_b[tid]; | |
| 100 | double left = word_count[wa]; | |
| 101 | double right = word_count[wb]; | |
| 102 | ||
| 103 | double eps = 1e-10; | |
| 104 | left = fmax(left, eps); | |
| 105 | right = fmax(right, eps); | |
| 106 | double n = fmax(n_total, eps); | |
| 107 | ||
| 108 | double log2_factor = 1.4426950408889634; | |
| 109 | double mi = (log(count) + log(n) - log(left) - log(right)) * log2_factor; | |
| 110 | ||
| 111 | pair_mi[tid] = mi; | |
| 112 | pair_flags[tid] = 0; | |
| 113 | } | |
| 114 | ||
| 115 | /* ═══════════════════════════════════════════════════════════════ | |
| 116 | * MI STATISTICS | |
| 117 | * | |
| 118 | * Count pairs by MI value. Useful for diagnostics and | |
| 119 | * determining warm-start thresholds. | |
| 120 | * | |
| 121 | * Outputs (all single-uint atomic counters, init to 0): | |
| 122 | * count_nonzero — pairs with count > 0 | |
| 123 | * count_positive_mi — pairs with MI > 0 | |
| 124 | * count_above_thresh — pairs with MI > threshold | |
| 125 | * ═══════════════════════════════════════════════════════════════ */ | |
| 126 | ||
| 127 | __kernel void mi_stats( | |
| 128 | __global const double* pair_mi, | |
| 129 | __global const double* pair_count, | |
| 130 | const uint num_pairs, | |
| 131 | const double threshold, | |
| 132 | __global volatile uint* count_nonzero, | |
| 133 | __global volatile uint* count_positive_mi, | |
| 134 | __global volatile uint* count_above_thresh) | |
| 135 | { | |
| 136 | uint tid = get_global_id(0); | |
| 137 | if (tid >= num_pairs) return; | |
| 138 | ||
| 139 | double cnt = pair_count[tid]; | |
| 140 | if (cnt < 0.5) return; | |
| 141 | ||
| 142 | atomic_add(count_nonzero, 1U); | |
| 143 | ||
| 144 | double mi = pair_mi[tid]; | |
| 145 | if (mi > 0.0) | |
| 146 | atomic_add(count_positive_mi, 1U); | |
| 147 | if (mi > threshold) | |
| 148 | atomic_add(count_above_thresh, 1U); | |
| 149 | } | |
| 150 | ||
| 151 | /* ═══════════════════════════════════════════════════════════════ | |
| 152 | * MI THRESHOLD FILTER | |
| 153 | * | |
| 154 | * Compact pairs above a MI threshold into contiguous output | |
| 155 | * arrays. Returns (pair_index, mi_value) for each passing pair. | |
| 156 | * | |
| 157 | * Use cases: | |
| 158 | * - Warm-start bootstrap (only load high-MI pairs) | |
| 159 | * - Building neighbor index for cosine similarity | |
| 160 | * - Exporting significant pairs for visualization | |
| 161 | * | |
| 162 | * out_count must be initialized to 0 before kernel launch. | |
| 163 | * ═══════════════════════════════════════════════════════════════ */ | |
| 164 | ||
| 165 | __kernel void mi_filter( | |
| 166 | __global const double* pair_mi, | |
| 167 | __global const double* pair_count, | |
| 168 | const uint num_pairs, | |
| 169 | const double mi_threshold, | |
| 170 | __global uint* out_pair_indices, | |
| 171 | __global double* out_mi_values, | |
| 172 | __global volatile uint* out_count, | |
| 173 | const uint max_output) | |
| 174 | { | |
| 175 | uint tid = get_global_id(0); | |
| 176 | if (tid >= num_pairs) return; | |
| 177 | ||
| 178 | double cnt = pair_count[tid]; | |
| 179 | double mi = pair_mi[tid]; | |
| 180 | ||
| 181 | if (cnt > 0.5 && mi > mi_threshold) { | |
| 182 | uint idx = atomic_add(out_count, 1U); | |
| 183 | if (idx < max_output) { | |
| 184 | out_pair_indices[idx] = tid; | |
| 185 | out_mi_values[idx] = mi; | |
| 186 | } | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | /* ═══════════════════════════════════════════════════════════════ | |
| 191 | * READBACK: Dump pair data with MI for verification | |
| 192 | * | |
| 193 | * Extended version of read_pairs from gpu-counting.cl that | |
| 194 | * also includes word indices and MI values. | |
| 195 | * ═══════════════════════════════════════════════════════════════ */ | |
| 196 | ||
| 197 | __kernel void read_pairs_with_mi( | |
| 198 | __global const uint* pair_word_a, | |
| 199 | __global const uint* pair_word_b, | |
| 200 | __global const double* pair_count, | |
| 201 | __global const double* pair_mi, | |
| 202 | __global const uint* pair_flags, | |
| 203 | __global const double* word_count, | |
| 204 | __global uint* out_word_a, | |
| 205 | __global uint* out_word_b, | |
| 206 | __global double* out_count, | |
| 207 | __global double* out_mi, | |
| 208 | __global double* out_left_marginal, | |
| 209 | __global double* out_right_marginal, | |
| 210 | const uint num_pairs) | |
| 211 | { | |
| 212 | uint tid = get_global_id(0); | |
| 213 | if (tid >= num_pairs) return; | |
| 214 | ||
| 215 | uint wa = pair_word_a[tid]; | |
| 216 | uint wb = pair_word_b[tid]; | |
| 217 | ||
| 218 | out_word_a[tid] = wa; | |
| 219 | out_word_b[tid] = wb; | |
| 220 | out_count[tid] = pair_count[tid]; | |
| 221 | out_mi[tid] = pair_mi[tid]; | |
| 222 | out_left_marginal[tid] = word_count[wa]; | |
| 223 | out_right_marginal[tid] = word_count[wb]; | |
| 224 | } |
| … | … | @@ -1,317 +0,0 @@ |
| 1 | /* | |
| 2 | * gpu-sections.cl -- Section extraction from MST/PMFG edges on GPU | |
| 3 | * | |
| 4 | * Given MST edges for a batch of sentences (as position pairs within | |
| 5 | * each sentence), extract sections (word + disjunct) and increment | |
| 6 | * SectionPool counts. Disjunct hashing is done entirely on GPU — | |
| 7 | * no strings involved. | |
| 8 | * | |
| 9 | * A section = (word, disjunct), where the disjunct is the set of | |
| 10 | * connectors for that word in the parse. A connector = (partner_word, | |
| 11 | * direction), where direction is '+' (right) or '-' (left). | |
| 12 | * | |
| 13 | * On GPU: connector = (word_pool_idx, direction_bit). | |
| 14 | * Disjunct = FNV-1a hash of sorted connector sequence. | |
| 15 | * | |
| 16 | * Appended after gpu-hashtable.cl, gpu-atomspace.cl at load time. | |
| 17 | */ | |
| 18 | ||
| 19 | /* Maximum connectors per word position (degree limit). | |
| 20 | * PMFG on a 20-word sentence has at most 54 edges; per word, | |
| 21 | * planar graph average degree < 6. 32 is very safe. */ | |
| 22 | #define MAX_CONNECTORS 32 | |
| 23 | ||
| 24 | /* Connector direction bits */ | |
| 25 | #define DIR_LEFT 0U /* partner is to the LEFT of this word */ | |
| 26 | #define DIR_RIGHT 1U /* partner is to the RIGHT of this word */ | |
| 27 | ||
| 28 | /* ═══════════════════════════════════════════════════════════════ | |
| 29 | * FNV-1a HASH for disjunct encoding | |
| 30 | * | |
| 31 | * Hash a sorted sequence of (word_pool_idx, direction) pairs | |
| 32 | * into a single 64-bit disjunct hash. | |
| 33 | * ═══════════════════════════════════════════════════════════════ */ | |
| 34 | ||
| 35 | inline ulong fnv1a_init(void) | |
| 36 | { | |
| 37 | return 0xcbf29ce484222325UL; | |
| 38 | } | |
| 39 | ||
| 40 | inline ulong fnv1a_mix(ulong hash, ulong val) | |
| 41 | { | |
| 42 | hash ^= val; | |
| 43 | hash *= 0x100000001b3UL; | |
| 44 | return hash; | |
| 45 | } | |
| 46 | ||
| 47 | inline ulong hash_disjunct(uint* conn_words, uint* conn_dirs, uint count) | |
| 48 | { | |
| 49 | ulong h = fnv1a_init(); | |
| 50 | for (uint i = 0; i < count; i++) { | |
| 51 | /* Encode: (word_pool_idx << 1) | direction_bit */ | |
| 52 | ulong encoded = ((ulong)conn_words[i] << 1) | (ulong)conn_dirs[i]; | |
| 53 | h = fnv1a_mix(h, encoded); | |
| 54 | } | |
| 55 | /* Ensure it's never the hash table sentinel */ | |
| 56 | if (h == HT_EMPTY_KEY) h = 0; | |
| 57 | return h; | |
| 58 | } | |
| 59 | ||
| 60 | /* ═══════════════════════════════════════════════════════════════ | |
| 61 | * INSERTION SORT for connectors | |
| 62 | * | |
| 63 | * Sort by: direction first (LEFT=0 before RIGHT=1), | |
| 64 | * then by word_pool_idx (ascending). | |
| 65 | * | |
| 66 | * This gives a deterministic disjunct representation: | |
| 67 | * all left connectors (sorted by partner) then all right | |
| 68 | * connectors (sorted by partner). | |
| 69 | * ═══════════════════════════════════════════════════════════════ */ | |
| 70 | ||
| 71 | inline void sort_connectors(uint* conn_words, uint* conn_dirs, uint count) | |
| 72 | { | |
| 73 | /* Insertion sort — count is small (typically < 10) */ | |
| 74 | for (uint i = 1; i < count; i++) { | |
| 75 | uint w = conn_words[i]; | |
| 76 | uint d = conn_dirs[i]; | |
| 77 | uint j = i; | |
| 78 | while (j > 0) { | |
| 79 | /* Compare: direction first, then word index */ | |
| 80 | int swap = 0; | |
| 81 | if (conn_dirs[j-1] > d) | |
| 82 | swap = 1; | |
| 83 | else if (conn_dirs[j-1] == d && conn_words[j-1] > w) | |
| 84 | swap = 1; | |
| 85 | ||
| 86 | if (!swap) break; | |
| 87 | ||
| 88 | conn_words[j] = conn_words[j-1]; | |
| 89 | conn_dirs[j] = conn_dirs[j-1]; | |
| 90 | j--; | |
| 91 | } | |
| 92 | conn_words[j] = w; | |
| 93 | conn_dirs[j] = d; | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | /* ═══════════════════════════════════════════════════════════════ | |
| 98 | * EXTRACT SECTIONS FROM MST EDGES | |
| 99 | * | |
| 100 | * One thread per word position across all sentences. | |
| 101 | * Each thread: | |
| 102 | * 1. Finds its sentence (binary search) | |
| 103 | * 2. Scans MST edges for this sentence | |
| 104 | * 3. Collects connectors (partner word + direction) | |
| 105 | * 4. Sorts connectors deterministically | |
| 106 | * 5. Hashes sorted sequence → disjunct_hash | |
| 107 | * 6. Find-or-create section in SectionPool | |
| 108 | * 7. Atomically increments section count | |
| 109 | * | |
| 110 | * Words with no edges (isolated) produce no section. | |
| 111 | * | |
| 112 | * Args: | |
| 113 | * -- Sentence data -- | |
| 114 | * flat_words - word pool indices [total_words] | |
| 115 | * sent_offsets - start of each sentence in flat_words [num_sentences] | |
| 116 | * sent_lengths - length of each sentence [num_sentences] | |
| 117 | * num_sentences - number of sentences | |
| 118 | * total_words - total words across all sentences | |
| 119 | * | |
| 120 | * -- MST edge data -- | |
| 121 | * edge_p1 - position 1 within sentence [total_edges] | |
| 122 | * edge_p2 - position 2 within sentence [total_edges] | |
| 123 | * edge_offsets - start of each sentence's edges [num_sentences] | |
| 124 | * edge_counts - number of edges per sentence [num_sentences] | |
| 125 | * | |
| 126 | * -- Section pool + hash table -- | |
| 127 | * sht_keys, sht_values - section hash table | |
| 128 | * sec_word - section pool: word indices | |
| 129 | * sec_disjunct_hash - section pool: disjunct hashes | |
| 130 | * sec_count - section pool: counts | |
| 131 | * sec_next_free - section bump allocator | |
| 132 | * | |
| 133 | * -- Stats -- | |
| 134 | * total_sections_created - atomic counter for new sections | |
| 135 | * ═══════════════════════════════════════════════════════════════ */ | |
| 136 | ||
| 137 | __kernel void extract_sections( | |
| 138 | /* sentence data */ | |
| 139 | __global const uint* flat_words, | |
| 140 | __global const uint* sent_offsets, | |
| 141 | __global const uint* sent_lengths, | |
| 142 | const uint num_sentences, | |
| 143 | const uint total_words, | |
| 144 | /* MST edge data */ | |
| 145 | __global const uint* edge_p1, | |
| 146 | __global const uint* edge_p2, | |
| 147 | __global const uint* edge_offsets, | |
| 148 | __global const uint* edge_counts, | |
| 149 | /* section hash table */ | |
| 150 | __global volatile ulong* sht_keys, | |
| 151 | __global volatile uint* sht_values, | |
| 152 | /* section pool SoA */ | |
| 153 | __global uint* sec_word, | |
| 154 | __global ulong* sec_disjunct_hash, | |
| 155 | __global volatile double* sec_count, | |
| 156 | __global volatile uint* sec_next_free, | |
| 157 | /* stats */ | |
| 158 | __global volatile uint* total_sections_created) | |
| 159 | { | |
| 160 | uint tid = get_global_id(0); | |
| 161 | if (tid >= total_words) return; | |
| 162 | ||
| 163 | /* ── Find which sentence this word belongs to (binary search) ── */ | |
| 164 | ||
| 165 | uint lo_s = 0, hi_s = num_sentences; | |
| 166 | while (lo_s < hi_s) { | |
| 167 | uint mid = (lo_s + hi_s) / 2; | |
| 168 | if (sent_offsets[mid] + sent_lengths[mid] <= tid) | |
| 169 | lo_s = mid + 1; | |
| 170 | else | |
| 171 | hi_s = mid; | |
| 172 | } | |
| 173 | uint sent_idx = lo_s; | |
| 174 | if (sent_idx >= num_sentences) return; | |
| 175 | ||
| 176 | uint sent_start = sent_offsets[sent_idx]; | |
| 177 | uint sent_len = sent_lengths[sent_idx]; | |
| 178 | uint pos_in_sent = tid - sent_start; | |
| 179 | ||
| 180 | /* Verify we're in this sentence */ | |
| 181 | if (pos_in_sent >= sent_len) return; | |
| 182 | ||
| 183 | uint my_word = flat_words[tid]; | |
| 184 | ||
| 185 | /* ── Collect connectors from MST edges ── */ | |
| 186 | ||
| 187 | uint conn_words[MAX_CONNECTORS]; | |
| 188 | uint conn_dirs[MAX_CONNECTORS]; | |
| 189 | uint conn_count = 0; | |
| 190 | ||
| 191 | uint e_start = edge_offsets[sent_idx]; | |
| 192 | uint e_count = edge_counts[sent_idx]; | |
| 193 | ||
| 194 | for (uint e = 0; e < e_count && conn_count < MAX_CONNECTORS; e++) | |
| 195 | { | |
| 196 | uint p1 = edge_p1[e_start + e]; | |
| 197 | uint p2 = edge_p2[e_start + e]; | |
| 198 | ||
| 199 | if (p1 == pos_in_sent) | |
| 200 | { | |
| 201 | /* Edge goes from me to p2 → p2 is to the right */ | |
| 202 | uint partner_word = flat_words[sent_start + p2]; | |
| 203 | conn_words[conn_count] = partner_word; | |
| 204 | conn_dirs[conn_count] = (p2 > pos_in_sent) ? DIR_RIGHT : DIR_LEFT; | |
| 205 | conn_count++; | |
| 206 | } | |
| 207 | else if (p2 == pos_in_sent) | |
| 208 | { | |
| 209 | /* Edge goes from p1 to me → p1 is to the left */ | |
| 210 | uint partner_word = flat_words[sent_start + p1]; | |
| 211 | conn_words[conn_count] = partner_word; | |
| 212 | conn_dirs[conn_count] = (p1 < pos_in_sent) ? DIR_LEFT : DIR_RIGHT; | |
| 213 | conn_count++; | |
| 214 | } | |
| 215 | } | |
| 216 | ||
| 217 | /* No connectors = isolated word → no section */ | |
| 218 | if (conn_count == 0) return; | |
| 219 | ||
| 220 | /* ── Sort connectors deterministically ── */ | |
| 221 | ||
| 222 | sort_connectors(conn_words, conn_dirs, conn_count); | |
| 223 | ||
| 224 | /* ── Hash sorted connectors → disjunct_hash ── */ | |
| 225 | ||
| 226 | ulong djh = hash_disjunct(conn_words, conn_dirs, conn_count); | |
| 227 | ||
| 228 | /* ── Find or create section in SectionPool ── */ | |
| 229 | ||
| 230 | ulong key = section_key(my_word, djh); | |
| 231 | ||
| 232 | ulong ht_cap = SECTION_HT_CAPACITY; | |
| 233 | ulong mask = ht_cap - 1; | |
| 234 | ulong slot = ht_hash(key) & mask; | |
| 235 | uint sec_idx = HT_EMPTY_VALUE; | |
| 236 | ||
| 237 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 238 | { | |
| 239 | ulong prev = atom_cmpxchg(&sht_keys[slot], HT_EMPTY_KEY, key); | |
| 240 | ||
| 241 | if (prev == HT_EMPTY_KEY) | |
| 242 | { | |
| 243 | /* New section — allocate from pool */ | |
| 244 | uint idx = atomic_add(sec_next_free, 1U); | |
| 245 | if (idx < SECTION_CAPACITY) { | |
| 246 | sec_word[idx] = my_word; | |
| 247 | sec_disjunct_hash[idx] = djh; | |
| 248 | sec_count[idx] = 0.0; | |
| 249 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 250 | sht_values[slot] = idx; | |
| 251 | sec_idx = idx; | |
| 252 | atomic_add(total_sections_created, 1U); | |
| 253 | } | |
| 254 | break; | |
| 255 | } | |
| 256 | if (prev == key) | |
| 257 | { | |
| 258 | /* Existing section — spin for value */ | |
| 259 | uint val = sht_values[slot]; | |
| 260 | while (val == HT_EMPTY_VALUE) { | |
| 261 | val = sht_values[slot]; | |
| 262 | } | |
| 263 | sec_idx = val; | |
| 264 | break; | |
| 265 | } | |
| 266 | ||
| 267 | slot = (slot + 1) & mask; | |
| 268 | } | |
| 269 | ||
| 270 | if (sec_idx == HT_EMPTY_VALUE) return; | |
| 271 | ||
| 272 | /* ── Increment section count ── */ | |
| 273 | ||
| 274 | atomic_add_double(&sec_count[sec_idx], 1.0); | |
| 275 | } | |
| 276 | ||
| 277 | /* ═══════════════════════════════════════════════════════════════ | |
| 278 | * READBACK: Dump section data for verification | |
| 279 | * | |
| 280 | * Reads section pool entries into flat output arrays. | |
| 281 | * ═══════════════════════════════════════════════════════════════ */ | |
| 282 | ||
| 283 | __kernel void read_sections( | |
| 284 | __global const uint* sec_word, | |
| 285 | __global const ulong* sec_disjunct_hash, | |
| 286 | __global const double* sec_count, | |
| 287 | __global uint* out_word, | |
| 288 | __global ulong* out_disjunct_hash, | |
| 289 | __global double* out_count, | |
| 290 | const uint num_sections) | |
| 291 | { | |
| 292 | uint tid = get_global_id(0); | |
| 293 | if (tid >= num_sections) return; | |
| 294 | ||
| 295 | out_word[tid] = sec_word[tid]; | |
| 296 | out_disjunct_hash[tid] = sec_disjunct_hash[tid]; | |
| 297 | out_count[tid] = sec_count[tid]; | |
| 298 | } | |
| 299 | ||
| 300 | /* ═══════════════════════════════════════════════════════════════ | |
| 301 | * EXTRACT SECTIONS + COUNT PAIRS (combined kernel) | |
| 302 | * | |
| 303 | * Same as extract_sections but also counts section pairs | |
| 304 | * within a window (for Level 1 vocabulary learning). | |
| 305 | * | |
| 306 | * Each word position produces one section. Adjacent sections | |
| 307 | * (within pair_window) form section pairs. Section pair counts | |
| 308 | * are stored via the pair pool (reusing pair_find_or_create | |
| 309 | * with section indices instead of word indices). | |
| 310 | * | |
| 311 | * This is Phase 4's full pipeline: edges → sections → section pairs | |
| 312 | * all in one kernel launch. | |
| 313 | * ═══════════════════════════════════════════════════════════════ */ | |
| 314 | ||
| 315 | /* Note: Section pair counting is deferred to Phase 5, where the | |
| 316 | * section pool is used for cosine similarity. The basic extract_sections | |
| 317 | * kernel above is sufficient for Phase 4. */ |
| … | … | @@ -1,506 +0,0 @@ |
| 1 | /* gpu-spanning-forest.cu — Batch PMFG parsing + surprise computation | |
| 2 | * | |
| 3 | * Grammar-based parsing and surprise measurement on GPU: | |
| 4 | * 1. parse_with_grammar: Batch PMFG with two-level MI lookup | |
| 5 | * (class-MI first, fallback to word-pair MI) | |
| 6 | * 2. compute_grammar_surprise: Compare word-parse vs grammar-parse | |
| 7 | * edges, score sentences by how unexpected they are | |
| 8 | * | |
| 9 | * One thread per sentence — embarrassingly parallel since sentences | |
| 10 | * are independent. Small n per sentence (≤MAX_SENTENCE_LEN) makes | |
| 11 | * O(n²) greedy PMFG tractable per thread. | |
| 12 | * | |
| 13 | * Build: nvcc -O2 -arch=sm_75 -rdc=true -c gpu-spanning-forest.cu | |
| 14 | */ | |
| 15 | ||
| 16 | #include <cstdint> | |
| 17 | #include <cstdio> | |
| 18 | #include <cfloat> | |
| 19 | #include <cmath> | |
| 20 | ||
| 21 | /* ─── Constants ─── */ | |
| 22 | ||
| 23 | #ifndef MAX_SENTENCE_LEN | |
| 24 | #define MAX_SENTENCE_LEN 64 /* max words per sentence */ | |
| 25 | #endif | |
| 26 | ||
| 27 | #ifndef MAX_SENTENCES | |
| 28 | #define MAX_SENTENCES 4096 /* max sentences in a batch */ | |
| 29 | #endif | |
| 30 | ||
| 31 | #ifndef MAX_TREE_EDGES | |
| 32 | #define MAX_TREE_EDGES (MAX_SENTENCE_LEN - 1) /* MST has n-1 edges */ | |
| 33 | #endif | |
| 34 | ||
| 35 | /* Hash table sentinels */ | |
| 36 | #define SF_HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 37 | #define SF_HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 38 | ||
| 39 | /* ─── Hash function (same splitmix64) ─── */ | |
| 40 | ||
| 41 | __device__ __forceinline__ | |
| 42 | uint64_t sf_hash(uint64_t key) { | |
| 43 | key ^= key >> 30; | |
| 44 | key *= 0xBF58476D1CE4E5B9ULL; | |
| 45 | key ^= key >> 27; | |
| 46 | key *= 0x94D049BB133111EBULL; | |
| 47 | key ^= key >> 31; | |
| 48 | return key; | |
| 49 | } | |
| 50 | ||
| 51 | /* ─── Pair key encoding (matches gpu-atomspace.cl) ─── */ | |
| 52 | ||
| 53 | __device__ __forceinline__ | |
| 54 | uint64_t pair_key(uint32_t a, uint32_t b) { | |
| 55 | uint32_t lo = (a <= b) ? a : b; | |
| 56 | uint32_t hi = (a <= b) ? b : a; | |
| 57 | return ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 58 | } | |
| 59 | ||
| 60 | /* ─── Word-pair MI lookup from pair hash table ─── */ | |
| 61 | ||
| 62 | __device__ | |
| 63 | double word_pair_mi_lookup( | |
| 64 | uint32_t word_a, uint32_t word_b, | |
| 65 | const uint64_t* __restrict__ pair_ht_keys, | |
| 66 | const uint32_t* __restrict__ pair_ht_values, | |
| 67 | const double* __restrict__ pair_mi, | |
| 68 | uint32_t pair_ht_capacity | |
| 69 | ) { | |
| 70 | uint64_t key = pair_key(word_a, word_b); | |
| 71 | uint64_t mask = (uint64_t)(pair_ht_capacity - 1); | |
| 72 | uint64_t slot = sf_hash(key) & mask; | |
| 73 | ||
| 74 | for (int probe = 0; probe < 4096; probe++) { | |
| 75 | uint64_t existing = pair_ht_keys[slot]; | |
| 76 | if (existing == key) { | |
| 77 | uint32_t idx = pair_ht_values[slot]; | |
| 78 | if (idx == SF_HT_EMPTY_VALUE) return 0.0; | |
| 79 | return pair_mi[idx]; | |
| 80 | } | |
| 81 | if (existing == SF_HT_EMPTY_KEY) return 0.0; | |
| 82 | slot = (slot + 1) & mask; | |
| 83 | } | |
| 84 | return 0.0; | |
| 85 | } | |
| 86 | ||
| 87 | /* ─── Class-pair MI lookup from class MI hash table ─── */ | |
| 88 | ||
| 89 | __device__ | |
| 90 | double class_pair_mi_lookup( | |
| 91 | uint32_t class_a, uint32_t class_b, | |
| 92 | const uint64_t* __restrict__ class_ht_keys, | |
| 93 | const double* __restrict__ class_ht_mi, | |
| 94 | uint32_t class_ht_capacity | |
| 95 | ) { | |
| 96 | if (class_a == 0xFFFFFFFFU || class_b == 0xFFFFFFFFU) return 0.0; | |
| 97 | if (class_a == class_b) return 0.0; | |
| 98 | ||
| 99 | uint32_t lo = (class_a <= class_b) ? class_a : class_b; | |
| 100 | uint32_t hi = (class_a <= class_b) ? class_b : class_a; | |
| 101 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 102 | uint64_t mask = (uint64_t)(class_ht_capacity - 1); | |
| 103 | uint64_t slot = sf_hash(key) & mask; | |
| 104 | ||
| 105 | for (int probe = 0; probe < 4096; probe++) { | |
| 106 | uint64_t existing = class_ht_keys[slot]; | |
| 107 | if (existing == key) return class_ht_mi[slot]; | |
| 108 | if (existing == SF_HT_EMPTY_KEY) return 0.0; | |
| 109 | slot = (slot + 1) & mask; | |
| 110 | } | |
| 111 | return 0.0; | |
| 112 | } | |
| 113 | ||
| 114 | /* ─── Two-level MI lookup: class-MI first, fallback to word-pair MI ─── */ | |
| 115 | ||
| 116 | __device__ | |
| 117 | double two_level_mi_lookup( | |
| 118 | uint32_t word_a, uint32_t word_b, | |
| 119 | /* Word → class mapping */ | |
| 120 | const uint32_t* __restrict__ word_class_id, | |
| 121 | /* Class MI hash table */ | |
| 122 | const uint64_t* __restrict__ class_ht_keys, | |
| 123 | const double* __restrict__ class_ht_mi, | |
| 124 | uint32_t class_ht_capacity, | |
| 125 | /* Word-pair MI hash table */ | |
| 126 | const uint64_t* __restrict__ pair_ht_keys, | |
| 127 | const uint32_t* __restrict__ pair_ht_values, | |
| 128 | const double* __restrict__ pair_mi, | |
| 129 | uint32_t pair_ht_capacity | |
| 130 | ) { | |
| 131 | /* Try class MI first */ | |
| 132 | uint32_t ca = word_class_id[word_a]; | |
| 133 | uint32_t cb = word_class_id[word_b]; | |
| 134 | ||
| 135 | if (ca != 0xFFFFFFFFU && cb != 0xFFFFFFFFU && ca != cb) { | |
| 136 | double cmi = class_pair_mi_lookup(ca, cb, | |
| 137 | class_ht_keys, class_ht_mi, class_ht_capacity); | |
| 138 | if (cmi > 0.0) return cmi; | |
| 139 | } | |
| 140 | ||
| 141 | /* Fallback to word-pair MI */ | |
| 142 | return word_pair_mi_lookup(word_a, word_b, | |
| 143 | pair_ht_keys, pair_ht_values, pair_mi, pair_ht_capacity); | |
| 144 | } | |
| 145 | ||
| 146 | /* ═══════════════════════════════════════════════════════════════ | |
| 147 | * KERNEL 1: GREEDY PMFG (Planar Maximally Filtered Graph) | |
| 148 | * | |
| 149 | * One thread per sentence. Greedy maximum spanning tree: | |
| 150 | * repeatedly add the highest-MI edge that doesn't create a cycle. | |
| 151 | * | |
| 152 | * Uses Union-Find (path compression) for cycle detection. | |
| 153 | * Result: n-1 edges per n-word sentence. | |
| 154 | * | |
| 155 | * Sentence format: | |
| 156 | * sentence_words[sent_offset + i] = word pool index | |
| 157 | * sentence_lengths[sent_idx] = number of words | |
| 158 | * sentence_offsets[sent_idx] = offset into sentence_words | |
| 159 | * | |
| 160 | * Output: | |
| 161 | * parse_edges: packed edge lists per sentence | |
| 162 | * parse_edge_mi: MI value for each edge | |
| 163 | * parse_edge_counts: number of edges per sentence | |
| 164 | * ═══════════════════════════════════════════════════════════════ */ | |
| 165 | ||
| 166 | __global__ void parse_with_grammar( | |
| 167 | /* Sentence data */ | |
| 168 | const uint32_t* __restrict__ sentence_words, | |
| 169 | const uint32_t* __restrict__ sentence_lengths, | |
| 170 | const uint32_t* __restrict__ sentence_offsets, | |
| 171 | uint32_t num_sentences, | |
| 172 | /* Word → class mapping */ | |
| 173 | const uint32_t* __restrict__ word_class_id, | |
| 174 | /* Class MI hash table (grammar) */ | |
| 175 | const uint64_t* __restrict__ class_ht_keys, | |
| 176 | const double* __restrict__ class_ht_mi, | |
| 177 | uint32_t class_ht_capacity, | |
| 178 | /* Word-pair MI hash table (fallback) */ | |
| 179 | const uint64_t* __restrict__ pair_ht_keys, | |
| 180 | const uint32_t* __restrict__ pair_ht_values, | |
| 181 | const double* __restrict__ pair_mi, | |
| 182 | uint32_t pair_ht_capacity, | |
| 183 | /* Output: parse edges (packed, MAX_TREE_EDGES per sentence) */ | |
| 184 | uint32_t* parse_edge_a, /* [sent_idx * MAX_TREE_EDGES + edge_idx] */ | |
| 185 | uint32_t* parse_edge_b, | |
| 186 | double* parse_edge_mi, | |
| 187 | uint32_t* parse_edge_count /* [sent_idx] = number of edges found */ | |
| 188 | ) { | |
| 189 | uint32_t sid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 190 | if (sid >= num_sentences) return; | |
| 191 | ||
| 192 | uint32_t len = sentence_lengths[sid]; | |
| 193 | uint32_t off = sentence_offsets[sid]; | |
| 194 | ||
| 195 | if (len < 2 || len > MAX_SENTENCE_LEN) { | |
| 196 | parse_edge_count[sid] = 0; | |
| 197 | return; | |
| 198 | } | |
| 199 | ||
| 200 | /* Local word indices for this sentence */ | |
| 201 | uint32_t words[MAX_SENTENCE_LEN]; | |
| 202 | for (uint32_t i = 0; i < len; i++) { | |
| 203 | words[i] = sentence_words[off + i]; | |
| 204 | } | |
| 205 | ||
| 206 | /* Union-Find for cycle detection */ | |
| 207 | uint32_t parent[MAX_SENTENCE_LEN]; | |
| 208 | for (uint32_t i = 0; i < len; i++) parent[i] = i; | |
| 209 | ||
| 210 | /* Find root with path compression */ | |
| 211 | #define UF_FIND(x) ({ \ | |
| 212 | uint32_t _r = (x); \ | |
| 213 | while (parent[_r] != _r) { \ | |
| 214 | parent[_r] = parent[parent[_r]]; \ | |
| 215 | _r = parent[_r]; \ | |
| 216 | } \ | |
| 217 | _r; \ | |
| 218 | }) | |
| 219 | ||
| 220 | /* Greedy PMFG: pick highest-MI edge that doesn't create cycle */ | |
| 221 | uint32_t num_edges = 0; | |
| 222 | uint32_t max_edges = len - 1; | |
| 223 | uint32_t out_base = sid * MAX_TREE_EDGES; | |
| 224 | ||
| 225 | /* We need to consider all O(n²) edges. Sort by MI descending. | |
| 226 | * For small n (≤64), an O(n²) selection sort approach works: | |
| 227 | * repeatedly find the highest unused non-cycle edge. */ | |
| 228 | ||
| 229 | /* Greedy approach: iterate max_edges times, | |
| 230 | * each time scanning all pairs for the best non-cycle edge. */ | |
| 231 | ||
| 232 | for (uint32_t round = 0; round < max_edges && num_edges < max_edges; round++) { | |
| 233 | double best_mi = -1e30; | |
| 234 | uint32_t best_i = 0, best_j = 0; | |
| 235 | ||
| 236 | for (uint32_t i = 0; i < len; i++) { | |
| 237 | for (uint32_t j = i + 1; j < len; j++) { | |
| 238 | /* Check if adding edge (i,j) would create a cycle */ | |
| 239 | uint32_t ri = UF_FIND(i); | |
| 240 | uint32_t rj = UF_FIND(j); | |
| 241 | if (ri == rj) continue; /* same component → cycle */ | |
| 242 | ||
| 243 | double mi = two_level_mi_lookup( | |
| 244 | words[i], words[j], | |
| 245 | word_class_id, | |
| 246 | class_ht_keys, class_ht_mi, class_ht_capacity, | |
| 247 | pair_ht_keys, pair_ht_values, pair_mi, pair_ht_capacity); | |
| 248 | ||
| 249 | if (mi > best_mi) { | |
| 250 | best_mi = mi; | |
| 251 | best_i = i; | |
| 252 | best_j = j; | |
| 253 | } | |
| 254 | } | |
| 255 | } | |
| 256 | ||
| 257 | if (best_mi <= -1e29) break; /* no more edges available */ | |
| 258 | ||
| 259 | /* Union */ | |
| 260 | uint32_t ri = UF_FIND(best_i); | |
| 261 | uint32_t rj = UF_FIND(best_j); | |
| 262 | parent[ri] = rj; | |
| 263 | ||
| 264 | /* Store edge */ | |
| 265 | parse_edge_a[out_base + num_edges] = words[best_i]; | |
| 266 | parse_edge_b[out_base + num_edges] = words[best_j]; | |
| 267 | parse_edge_mi[out_base + num_edges] = best_mi; | |
| 268 | num_edges++; | |
| 269 | } | |
| 270 | ||
| 271 | parse_edge_count[sid] = num_edges; | |
| 272 | ||
| 273 | #undef UF_FIND | |
| 274 | } | |
| 275 | ||
| 276 | /* ═══════════════════════════════════════════════════════════════ | |
| 277 | * KERNEL 2: WORD-LEVEL PMFG (for comparison) | |
| 278 | * | |
| 279 | * Same as above but ONLY uses word-pair MI (no class-MI). | |
| 280 | * This gives the "true" parse that the grammar should approximate. | |
| 281 | * ═══════════════════════════════════════════════════════════════ */ | |
| 282 | ||
| 283 | __global__ void parse_word_only( | |
| 284 | const uint32_t* __restrict__ sentence_words, | |
| 285 | const uint32_t* __restrict__ sentence_lengths, | |
| 286 | const uint32_t* __restrict__ sentence_offsets, | |
| 287 | uint32_t num_sentences, | |
| 288 | const uint64_t* __restrict__ pair_ht_keys, | |
| 289 | const uint32_t* __restrict__ pair_ht_values, | |
| 290 | const double* __restrict__ pair_mi, | |
| 291 | uint32_t pair_ht_capacity, | |
| 292 | uint32_t* parse_edge_a, | |
| 293 | uint32_t* parse_edge_b, | |
| 294 | double* parse_edge_mi, | |
| 295 | uint32_t* parse_edge_count | |
| 296 | ) { | |
| 297 | uint32_t sid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 298 | if (sid >= num_sentences) return; | |
| 299 | ||
| 300 | uint32_t len = sentence_lengths[sid]; | |
| 301 | uint32_t off = sentence_offsets[sid]; | |
| 302 | ||
| 303 | if (len < 2 || len > MAX_SENTENCE_LEN) { | |
| 304 | parse_edge_count[sid] = 0; | |
| 305 | return; | |
| 306 | } | |
| 307 | ||
| 308 | uint32_t words[MAX_SENTENCE_LEN]; | |
| 309 | for (uint32_t i = 0; i < len; i++) words[i] = sentence_words[off + i]; | |
| 310 | ||
| 311 | uint32_t parent[MAX_SENTENCE_LEN]; | |
| 312 | for (uint32_t i = 0; i < len; i++) parent[i] = i; | |
| 313 | ||
| 314 | #define UF_FIND(x) ({ \ | |
| 315 | uint32_t _r = (x); \ | |
| 316 | while (parent[_r] != _r) { \ | |
| 317 | parent[_r] = parent[parent[_r]]; \ | |
| 318 | _r = parent[_r]; \ | |
| 319 | } \ | |
| 320 | _r; \ | |
| 321 | }) | |
| 322 | ||
| 323 | uint32_t num_edges = 0; | |
| 324 | uint32_t max_edges = len - 1; | |
| 325 | uint32_t out_base = sid * MAX_TREE_EDGES; | |
| 326 | ||
| 327 | for (uint32_t round = 0; round < max_edges && num_edges < max_edges; round++) { | |
| 328 | double best_mi = -1e30; | |
| 329 | uint32_t best_i = 0, best_j = 0; | |
| 330 | ||
| 331 | for (uint32_t i = 0; i < len; i++) { | |
| 332 | for (uint32_t j = i + 1; j < len; j++) { | |
| 333 | uint32_t ri = UF_FIND(i); | |
| 334 | uint32_t rj = UF_FIND(j); | |
| 335 | if (ri == rj) continue; | |
| 336 | ||
| 337 | double mi = word_pair_mi_lookup( | |
| 338 | words[i], words[j], | |
| 339 | pair_ht_keys, pair_ht_values, pair_mi, | |
| 340 | pair_ht_capacity); | |
| 341 | ||
| 342 | if (mi > best_mi) { | |
| 343 | best_mi = mi; | |
| 344 | best_i = i; | |
| 345 | best_j = j; | |
| 346 | } | |
| 347 | } | |
| 348 | } | |
| 349 | ||
| 350 | if (best_mi <= -1e29) break; | |
| 351 | ||
| 352 | uint32_t ri = UF_FIND(best_i); | |
| 353 | uint32_t rj = UF_FIND(best_j); | |
| 354 | parent[ri] = rj; | |
| 355 | ||
| 356 | parse_edge_a[out_base + num_edges] = words[best_i]; | |
| 357 | parse_edge_b[out_base + num_edges] = words[best_j]; | |
| 358 | parse_edge_mi[out_base + num_edges] = best_mi; | |
| 359 | num_edges++; | |
| 360 | } | |
| 361 | ||
| 362 | parse_edge_count[sid] = num_edges; | |
| 363 | #undef UF_FIND | |
| 364 | } | |
| 365 | ||
| 366 | /* ═══════════════════════════════════════════════════════════════ | |
| 367 | * KERNEL 3: GRAMMAR SURPRISE | |
| 368 | * | |
| 369 | * One thread per sentence. Compares word-parse edges vs | |
| 370 | * grammar-parse edges. Surprise = edges in word-parse NOT in | |
| 371 | * grammar-parse, weighted by MI, normalized by sentence length. | |
| 372 | * | |
| 373 | * Higher surprise → grammar doesn't explain this sentence well. | |
| 374 | * ═══════════════════════════════════════════════════════════════ */ | |
| 375 | ||
| 376 | __global__ void compute_grammar_surprise( | |
| 377 | /* Word-parse results */ | |
| 378 | const uint32_t* __restrict__ word_edge_a, | |
| 379 | const uint32_t* __restrict__ word_edge_b, | |
| 380 | const double* __restrict__ word_edge_mi, | |
| 381 | const uint32_t* __restrict__ word_edge_count, | |
| 382 | /* Grammar-parse results */ | |
| 383 | const uint32_t* __restrict__ gram_edge_a, | |
| 384 | const uint32_t* __restrict__ gram_edge_b, | |
| 385 | const uint32_t* __restrict__ gram_edge_count, | |
| 386 | /* Sentence data (for normalization) */ | |
| 387 | const uint32_t* __restrict__ sentence_lengths, | |
| 388 | uint32_t num_sentences, | |
| 389 | /* Output */ | |
| 390 | double* surprise_scores /* [num_sentences] */ | |
| 391 | ) { | |
| 392 | uint32_t sid = blockIdx.x * blockDim.x + threadIdx.x; | |
| 393 | if (sid >= num_sentences) return; | |
| 394 | ||
| 395 | uint32_t w_count = word_edge_count[sid]; | |
| 396 | uint32_t g_count = gram_edge_count[sid]; | |
| 397 | uint32_t len = sentence_lengths[sid]; | |
| 398 | ||
| 399 | if (w_count == 0 || len < 2) { | |
| 400 | surprise_scores[sid] = 0.0; | |
| 401 | return; | |
| 402 | } | |
| 403 | ||
| 404 | uint32_t w_base = sid * MAX_TREE_EDGES; | |
| 405 | uint32_t g_base = sid * MAX_TREE_EDGES; | |
| 406 | ||
| 407 | /* For each edge in word-parse, check if it exists in grammar-parse */ | |
| 408 | double surprise = 0.0; | |
| 409 | for (uint32_t wi = 0; wi < w_count; wi++) { | |
| 410 | uint32_t wa = word_edge_a[w_base + wi]; | |
| 411 | uint32_t wb = word_edge_b[w_base + wi]; | |
| 412 | double wmi = word_edge_mi[w_base + wi]; | |
| 413 | ||
| 414 | /* Normalize edge for comparison (canonical order) */ | |
| 415 | uint32_t wlo = (wa <= wb) ? wa : wb; | |
| 416 | uint32_t whi = (wa <= wb) ? wb : wa; | |
| 417 | ||
| 418 | /* Search grammar parse for this edge */ | |
| 419 | int found = 0; | |
| 420 | for (uint32_t gi = 0; gi < g_count; gi++) { | |
| 421 | uint32_t ga = gram_edge_a[g_base + gi]; | |
| 422 | uint32_t gb = gram_edge_b[g_base + gi]; | |
| 423 | uint32_t glo = (ga <= gb) ? ga : gb; | |
| 424 | uint32_t ghi = (ga <= gb) ? gb : ga; | |
| 425 | ||
| 426 | if (wlo == glo && whi == ghi) { | |
| 427 | found = 1; | |
| 428 | break; | |
| 429 | } | |
| 430 | } | |
| 431 | ||
| 432 | if (!found) { | |
| 433 | /* This word-parse edge is NOT in grammar-parse → surprising */ | |
| 434 | surprise += fabs(wmi); | |
| 435 | } | |
| 436 | } | |
| 437 | ||
| 438 | /* Normalize by sentence length */ | |
| 439 | surprise_scores[sid] = surprise / (double)len; | |
| 440 | } | |
| 441 | ||
| 442 | /* ═══════════════════════════════════════════════════════════════ | |
| 443 | * HOST API | |
| 444 | * ═══════════════════════════════════════════════════════════════ */ | |
| 445 | ||
| 446 | /* Run full parse + surprise pipeline */ | |
| 447 | extern "C" | |
| 448 | void spanning_forest_run( | |
| 449 | /* Sentence data */ | |
| 450 | const uint32_t* d_sentence_words, | |
| 451 | const uint32_t* d_sentence_lengths, | |
| 452 | const uint32_t* d_sentence_offsets, | |
| 453 | uint32_t num_sentences, | |
| 454 | /* Word → class */ | |
| 455 | const uint32_t* d_word_class_id, | |
| 456 | /* Class MI (grammar) */ | |
| 457 | const uint64_t* d_class_ht_keys, | |
| 458 | const double* d_class_ht_mi, | |
| 459 | uint32_t class_ht_capacity, | |
| 460 | /* Word-pair MI */ | |
| 461 | const uint64_t* d_pair_ht_keys, | |
| 462 | const uint32_t* d_pair_ht_values, | |
| 463 | const double* d_pair_mi, | |
| 464 | uint32_t pair_ht_capacity, | |
| 465 | /* Pre-allocated output buffers */ | |
| 466 | uint32_t* d_word_parse_a, /* [num_sentences * MAX_TREE_EDGES] */ | |
| 467 | uint32_t* d_word_parse_b, | |
| 468 | double* d_word_parse_mi, | |
| 469 | uint32_t* d_word_parse_count, /* [num_sentences] */ | |
| 470 | uint32_t* d_gram_parse_a, | |
| 471 | uint32_t* d_gram_parse_b, | |
| 472 | double* d_gram_parse_mi, | |
| 473 | uint32_t* d_gram_parse_count, | |
| 474 | double* d_surprise /* [num_sentences] */ | |
| 475 | ) { | |
| 476 | int threads = 64; /* fewer threads — each does O(n³) work */ | |
| 477 | ||
| 478 | int blocks = (num_sentences + threads - 1) / threads; | |
| 479 | if (blocks < 1) blocks = 1; | |
| 480 | ||
| 481 | /* Step 1: Word-only parse */ | |
| 482 | parse_word_only<<<blocks, threads>>>( | |
| 483 | d_sentence_words, d_sentence_lengths, d_sentence_offsets, | |
| 484 | num_sentences, | |
| 485 | d_pair_ht_keys, d_pair_ht_values, d_pair_mi, pair_ht_capacity, | |
| 486 | d_word_parse_a, d_word_parse_b, d_word_parse_mi, d_word_parse_count); | |
| 487 | cudaDeviceSynchronize(); | |
| 488 | ||
| 489 | /* Step 2: Grammar-based parse */ | |
| 490 | parse_with_grammar<<<blocks, threads>>>( | |
| 491 | d_sentence_words, d_sentence_lengths, d_sentence_offsets, | |
| 492 | num_sentences, | |
| 493 | d_word_class_id, | |
| 494 | d_class_ht_keys, d_class_ht_mi, class_ht_capacity, | |
| 495 | d_pair_ht_keys, d_pair_ht_values, d_pair_mi, pair_ht_capacity, | |
| 496 | d_gram_parse_a, d_gram_parse_b, d_gram_parse_mi, d_gram_parse_count); | |
| 497 | cudaDeviceSynchronize(); | |
| 498 | ||
| 499 | /* Step 3: Surprise */ | |
| 500 | compute_grammar_surprise<<<blocks, threads>>>( | |
| 501 | d_word_parse_a, d_word_parse_b, d_word_parse_mi, d_word_parse_count, | |
| 502 | d_gram_parse_a, d_gram_parse_b, d_gram_parse_count, | |
| 503 | d_sentence_lengths, num_sentences, | |
| 504 | d_surprise); | |
| 505 | cudaDeviceSynchronize(); | |
| 506 | } |
| … | … | @@ -1,232 +0,0 @@ |
| 1 | /* | |
| 2 | * gpu-substitute.cl -- Class substitution on GPU-resident atom pools | |
| 3 | * | |
| 4 | * After clustering merges words into classes, this kernel scans | |
| 5 | * PairPool and SectionPool, replacing word indices with class IDs. | |
| 6 | * Duplicate pairs (same class pair after substitution) are merged | |
| 7 | * by rebuilding the pair hash table. | |
| 8 | * | |
| 9 | * Pipeline: | |
| 10 | * 1. assign_classes — batch set word_class_id for merged words | |
| 11 | * 2. substitute_pairs — replace word indices with class IDs in pairs | |
| 12 | * 3. rebuild_pair_index — rebuild HT, merge duplicate pairs | |
| 13 | * 4. substitute_section_words — replace section head words with class IDs | |
| 14 | * | |
| 15 | * Appended after gpu-hashtable.cl, gpu-atomspace.cl at load time. | |
| 16 | */ | |
| 17 | ||
| 18 | /* ═══════════════════════════════════════════════════════════════ | |
| 19 | * STEP 1: ASSIGN CLASSES | |
| 20 | * | |
| 21 | * Batch-set word_class_id for a list of words. | |
| 22 | * Called after each clustering round with the new merges. | |
| 23 | * | |
| 24 | * class_id = 0 means "no class" (unclassified). | |
| 25 | * ═══════════════════════════════════════════════════════════════ */ | |
| 26 | ||
| 27 | __kernel void assign_classes( | |
| 28 | __global uint* word_class_id, | |
| 29 | __global const uint* in_word_indices, | |
| 30 | __global const uint* in_class_ids, | |
| 31 | const uint num_assignments) | |
| 32 | { | |
| 33 | uint tid = get_global_id(0); | |
| 34 | if (tid >= num_assignments) return; | |
| 35 | ||
| 36 | uint word_idx = in_word_indices[tid]; | |
| 37 | uint class_id = in_class_ids[tid]; | |
| 38 | if (word_idx < WORD_CAPACITY) | |
| 39 | word_class_id[word_idx] = class_id; | |
| 40 | } | |
| 41 | ||
| 42 | /* ═══════════════════════════════════════════════════════════════ | |
| 43 | * STEP 2: SUBSTITUTE PAIRS | |
| 44 | * | |
| 45 | * One thread per pair. For each pair: | |
| 46 | * - Look up class_id for word_a and word_b | |
| 47 | * - If classified (class_id != 0): replace with class_id | |
| 48 | * - Maintain canonical order (lo < hi) | |
| 49 | * - Self-pairs (both words → same class) are eliminated | |
| 50 | * - Mark changed pairs as dirty for MI recompute | |
| 51 | * | |
| 52 | * After this kernel, the pair hash table is INVALID (keys changed). | |
| 53 | * Must run rebuild_pair_index to restore it. | |
| 54 | * ═══════════════════════════════════════════════════════════════ */ | |
| 55 | ||
| 56 | __kernel void substitute_pairs( | |
| 57 | __global uint* pair_word_a, | |
| 58 | __global uint* pair_word_b, | |
| 59 | __global volatile double* pair_count, | |
| 60 | __global double* pair_mi, | |
| 61 | __global uint* pair_flags, | |
| 62 | __global const uint* word_class_id, | |
| 63 | __global volatile uint* num_changed, | |
| 64 | __global volatile uint* num_eliminated, | |
| 65 | const uint num_pairs) | |
| 66 | { | |
| 67 | uint tid = get_global_id(0); | |
| 68 | if (tid >= num_pairs) return; | |
| 69 | ||
| 70 | double count = pair_count[tid]; | |
| 71 | if (count < 0.5) return; /* skip empty pairs */ | |
| 72 | ||
| 73 | uint wa = pair_word_a[tid]; | |
| 74 | uint wb = pair_word_b[tid]; | |
| 75 | ||
| 76 | uint ca = word_class_id[wa]; | |
| 77 | uint cb = word_class_id[wb]; | |
| 78 | ||
| 79 | uint new_a = (ca != 0) ? ca : wa; | |
| 80 | uint new_b = (cb != 0) ? cb : wb; | |
| 81 | ||
| 82 | /* Self-pair after substitution: both words map to same class → drop */ | |
| 83 | if (new_a == new_b) { | |
| 84 | pair_count[tid] = 0.0; | |
| 85 | pair_mi[tid] = 0.0; | |
| 86 | pair_flags[tid] = 0; | |
| 87 | atomic_add(num_eliminated, 1U); | |
| 88 | return; | |
| 89 | } | |
| 90 | ||
| 91 | /* Canonical order */ | |
| 92 | uint lo = min(new_a, new_b); | |
| 93 | uint hi = max(new_a, new_b); | |
| 94 | ||
| 95 | if (lo != wa || hi != wb) { | |
| 96 | pair_word_a[tid] = lo; | |
| 97 | pair_word_b[tid] = hi; | |
| 98 | pair_flags[tid] = 1; /* dirty: needs MI recompute */ | |
| 99 | atomic_add(num_changed, 1U); | |
| 100 | } | |
| 101 | } | |
| 102 | ||
| 103 | /* ═══════════════════════════════════════════════════════════════ | |
| 104 | * STEP 3: REBUILD PAIR INDEX | |
| 105 | * | |
| 106 | * After substitute_pairs changed pair keys, the pair hash table | |
| 107 | * is stale. This kernel rebuilds it AND merges duplicate pairs. | |
| 108 | * | |
| 109 | * Algorithm: one thread per pair. | |
| 110 | * - Compute key from (word_a, word_b) | |
| 111 | * - CAS into hash table | |
| 112 | * - If we claim the slot: we're the "primary" copy | |
| 113 | * - If slot already has our key: duplicate! | |
| 114 | * → atomically add our count to the primary's count | |
| 115 | * → zero ourselves (mark as merged) | |
| 116 | * | |
| 117 | * IMPORTANT: The pair hash table must be cleared (filled with | |
| 118 | * 0xFF) before running this kernel. | |
| 119 | * ═══════════════════════════════════════════════════════════════ */ | |
| 120 | ||
| 121 | __kernel void rebuild_pair_index( | |
| 122 | __global const uint* pair_word_a, | |
| 123 | __global const uint* pair_word_b, | |
| 124 | __global volatile double* pair_count, | |
| 125 | __global double* pair_mi, | |
| 126 | __global uint* pair_flags, | |
| 127 | __global volatile ulong* pht_keys, | |
| 128 | __global volatile uint* pht_values, | |
| 129 | __global volatile uint* num_merged, | |
| 130 | const uint num_pairs) | |
| 131 | { | |
| 132 | uint tid = get_global_id(0); | |
| 133 | if (tid >= num_pairs) return; | |
| 134 | ||
| 135 | double count = pair_count[tid]; | |
| 136 | if (count < 0.5) return; /* skip empty/eliminated pairs */ | |
| 137 | ||
| 138 | uint wa = pair_word_a[tid]; | |
| 139 | uint wb = pair_word_b[tid]; | |
| 140 | uint lo = min(wa, wb); | |
| 141 | uint hi = max(wa, wb); | |
| 142 | ulong key = ((ulong)lo << 32) | (ulong)hi; | |
| 143 | ||
| 144 | ulong cap = PAIR_HT_CAPACITY; | |
| 145 | ulong mask = cap - 1; | |
| 146 | ulong slot = ht_hash(key) & mask; | |
| 147 | ||
| 148 | for (uint probe = 0; probe < HT_MAX_PROBES; probe++) | |
| 149 | { | |
| 150 | ulong prev = atom_cmpxchg(&pht_keys[slot], HT_EMPTY_KEY, key); | |
| 151 | ||
| 152 | if (prev == HT_EMPTY_KEY) | |
| 153 | { | |
| 154 | /* We claim this slot — we're the primary copy */ | |
| 155 | mem_fence(CLK_GLOBAL_MEM_FENCE); | |
| 156 | pht_values[slot] = tid; | |
| 157 | return; | |
| 158 | } | |
| 159 | if (prev == key) | |
| 160 | { | |
| 161 | /* Duplicate! Merge into the primary. */ | |
| 162 | uint primary = pht_values[slot]; | |
| 163 | while (primary == HT_EMPTY_VALUE) { | |
| 164 | primary = pht_values[slot]; | |
| 165 | } | |
| 166 | ||
| 167 | /* Add our count to primary */ | |
| 168 | atomic_add_double(&pair_count[primary], count); | |
| 169 | ||
| 170 | /* Mark primary as dirty */ | |
| 171 | pair_flags[primary] = 1; | |
| 172 | ||
| 173 | /* Zero ourselves */ | |
| 174 | pair_count[tid] = 0.0; | |
| 175 | pair_mi[tid] = 0.0; | |
| 176 | pair_flags[tid] = 0; | |
| 177 | ||
| 178 | atomic_add(num_merged, 1U); | |
| 179 | return; | |
| 180 | } | |
| 181 | ||
| 182 | slot = (slot + 1) & mask; | |
| 183 | } | |
| 184 | } | |
| 185 | ||
| 186 | /* ═══════════════════════════════════════════════════════════════ | |
| 187 | * STEP 4: SUBSTITUTE SECTION WORDS | |
| 188 | * | |
| 189 | * Replace section head word with its class ID. | |
| 190 | * | |
| 191 | * NOTE: This only replaces the section's word field. The | |
| 192 | * disjunct_hash (which encodes connector words) is NOT updated | |
| 193 | * here. Full connector rewriting happens naturally on the next | |
| 194 | * call to extract_sections (Phase 4), which uses the updated | |
| 195 | * word pool indices. This matches the iterative pipeline: | |
| 196 | * each round substitutes, then re-extracts fresh sections. | |
| 197 | * ═══════════════════════════════════════════════════════════════ */ | |
| 198 | ||
| 199 | __kernel void substitute_section_words( | |
| 200 | __global uint* sec_word, | |
| 201 | __global const double* sec_count, | |
| 202 | __global const uint* word_class_id, | |
| 203 | __global volatile uint* num_changed, | |
| 204 | const uint num_sections) | |
| 205 | { | |
| 206 | uint tid = get_global_id(0); | |
| 207 | if (tid >= num_sections) return; | |
| 208 | ||
| 209 | if (sec_count[tid] < 0.5) return; | |
| 210 | ||
| 211 | uint word = sec_word[tid]; | |
| 212 | uint cls = word_class_id[word]; | |
| 213 | ||
| 214 | if (cls != 0 && cls != word) { | |
| 215 | sec_word[tid] = cls; | |
| 216 | atomic_add(num_changed, 1U); | |
| 217 | } | |
| 218 | } | |
| 219 | ||
| 220 | /* ═══════════════════════════════════════════════════════════════ | |
| 221 | * READBACK: Dump class assignments and pair data | |
| 222 | * ═══════════════════════════════════════════════════════════════ */ | |
| 223 | ||
| 224 | __kernel void read_class_assignments( | |
| 225 | __global const uint* word_class_id, | |
| 226 | __global uint* out_classes, | |
| 227 | const uint num_words) | |
| 228 | { | |
| 229 | uint tid = get_global_id(0); | |
| 230 | if (tid >= num_words) return; | |
| 231 | out_classes[tid] = word_class_id[tid]; | |
| 232 | } |
| … | … | @@ -1,4 +1,5 @@ |
| 1 | # Generate the atom types | |
| 1 | # Generate the GPU atom types | |
| 2 | ||
| 2 | 3 | OPENCOG_GEN_CXX_ATOMTYPES(atom_types.script |
| 3 | 4 | atom_types.h |
| 4 | 5 | atom_types.definitions |
| … | … | @@ -11,7 +12,7 @@ ADD_CUSTOM_TARGET(persist_gpu_atom_types |
| 11 | 12 | DEPENDS atom_types.h persist_gpu_types.scm persist_gpu_types.pyx) |
| 12 | 13 | |
| 13 | 14 | # Build the shared library |
| 14 | ADD_LIBRARY (persist-gpu-types SHARED | |
| 15 | ADD_LIBRARY(persist-gpu-types SHARED | |
| 15 | 16 | atom_types_init.cc |
| 16 | 17 | ) |
| 17 | 18 | |
| … | … | @@ -26,19 +27,17 @@ TARGET_LINK_LIBRARIES(persist-gpu-types |
| 26 | 27 | ADD_DEPENDENCIES(persist-gpu-types persist_gpu_atom_types) |
| 27 | 28 | |
| 28 | 29 | # Install targets |
| 29 | INSTALL (TARGETS persist-gpu-types EXPORT AtomSpaceGPUTargets | |
| 30 | INSTALL(TARGETS persist-gpu-types EXPORT AtomSpaceGPUTargets | |
| 30 | 31 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/opencog" |
| 31 | 32 | ) |
| 32 | 33 | |
| 33 | INSTALL (FILES | |
| 34 | INSTALL(FILES | |
| 34 | 35 | ${CMAKE_CURRENT_BINARY_DIR}/atom_types.h |
| 35 | 36 | DESTINATION "include/opencog/persist/gpu-types" |
| 36 | 37 | ) |
| 37 | 38 | |
| 38 | ADD_GUILE_MODULE (FILES | |
| 39 | ADD_GUILE_MODULE(FILES | |
| 39 | 40 | ${CMAKE_CURRENT_BINARY_DIR}/persist_gpu_types.scm |
| 40 | 41 | MODULE_DESTINATION "${GUILE_SITE_DIR}/opencog/persist/gpu-types" |
| 41 | 42 | DEPENDS persist_gpu_atom_types |
| 42 | 43 | ) |
| 43 | ||
| 44 | ADD_GUILE_EXTENSION(SCM_CONFIG persist-gpu-types "opencog-ext-path-persist-gpu-types") |
| … | … | @@ -2,5 +2,3 @@ |
| 2 | 2 | // Script for generating the GpuStorageNode atom type. |
| 3 | 3 | // |
| 4 | 4 | GPU_STORAGE_NODE <- STORAGE_NODE |
| 5 | ||
| 6 | // That's all folks! Nothing more is needed here! |
| … | … | @@ -1,23 +1,8 @@ |
| 1 | 1 | /* |
| 2 | 2 | * opencog/persist/gpu-types/atom_types_init.cc |
| 3 | 3 | * |
| 4 | * Copyright (C) 2025 OpenCog Foundation | |
| 5 | * All Rights Reserved | |
| 6 | * | |
| 7 | * This program is free software; you can redistribute it and/or modify | |
| 8 | * it under the terms of the GNU Affero General Public License v3 as | |
| 9 | * published by the Free Software Foundation and including the exceptions | |
| 10 | * at http://opencog.org/wiki/Licenses | |
| 11 | * | |
| 12 | * This program is distributed in the hope that it will be useful, | |
| 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 15 | * GNU General Public License for more details. | |
| 16 | * | |
| 17 | * You should have received a copy of the GNU Affero General Public License | |
| 18 | * along with this program; if not, write to: | |
| 19 | * Free Software Foundation, Inc., | |
| 20 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| 4 | * Copyright (C) 2026 OpenCog Foundation | |
| 5 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 21 | 6 | */ |
| 22 | 7 | |
| 23 | 8 | #include <opencog/persist/storage/storage_types.h> |
| … | … | @@ -1,43 +1,49 @@ |
| 1 | 1 | # |
| 2 | # Build the GPU driver for the AtomSpace. | |
| 2 | # Build the GPU AtomTable library. | |
| 3 | 3 | # Supports CUDA (preferred) and OpenCL (fallback). |
| 4 | 4 | # |
| 5 | # Step 2: AoS atom table (GpuAtomTable) | |
| 6 | # Step 3: BackingStore wrapper (GpuStorageNode) | |
| 7 | # | |
| 5 | 8 | |
| 6 | # -- Core StorageNode (backend-agnostic) -- | |
| 7 | SET(GPU_SOURCES GpuStorageNode.cc) | |
| 9 | # -- Sources: dispatch layer always compiled, backends conditional -- | |
| 10 | SET(GPU_SOURCES | |
| 11 | GpuAtomTable_dispatch.cc | |
| 12 | GpuStorageNode.cc | |
| 13 | ) | |
| 8 | 14 | SET(GPU_LINK_LIBS |
| 9 | 15 | persist-gpu-types |
| 10 | persist | |
| 11 | 16 | ${ATOMSPACE_STORAGE_LIBRARIES} |
| 12 | 17 | ${ATOMSPACE_LIBRARIES} |
| 13 | 18 | ) |
| 14 | 19 | SET(GPU_INSTALL_HEADERS |
| 20 | GpuAtomTable.h | |
| 15 | 21 | GpuStorageNode.h |
| 16 | GpuBackend.h | |
| 17 | gpu-pool-defs.h | |
| 18 | 22 | ) |
| 19 | 23 | |
| 20 | # -- OpenCL backend (optional) -- | |
| 24 | IF (HAVE_CUDA) | |
| 25 | LIST(APPEND GPU_SOURCES GpuAtomTable.cu) | |
| 26 | ENDIF (HAVE_CUDA) | |
| 27 | ||
| 21 | 28 | IF (HAVE_OPENCL) |
| 22 | LIST(APPEND GPU_SOURCES OpenCLBackend.cc) | |
| 29 | LIST(APPEND GPU_SOURCES GpuAtomTable_ocl.cc) | |
| 23 | 30 | LIST(APPEND GPU_LINK_LIBS OpenCL::OpenCL) |
| 24 | LIST(APPEND GPU_INSTALL_HEADERS opencl-headers.h OpenCLBackend.h) | |
| 31 | LIST(APPEND GPU_INSTALL_HEADERS opencl-headers.h) | |
| 25 | 32 | ENDIF (HAVE_OPENCL) |
| 26 | 33 | |
| 27 | # -- CUDA backend (optional, preferred) -- | |
| 28 | IF (HAVE_CUDA) | |
| 29 | LIST(APPEND GPU_SOURCES CudaBackend.cu) | |
| 30 | LIST(APPEND GPU_INSTALL_HEADERS CudaBackend.h) | |
| 31 | ENDIF (HAVE_CUDA) | |
| 32 | ||
| 33 | 34 | # -- Build the library -- |
| 34 | 35 | ADD_LIBRARY(persist-gpu SHARED ${GPU_SOURCES}) |
| 35 | 36 | |
| 36 | # The atom_types.h file is written to the build directory | |
| 37 | TARGET_INCLUDE_DIRECTORIES(persist-gpu PRIVATE ${CMAKE_BINARY_DIR}) | |
| 37 | TARGET_INCLUDE_DIRECTORIES(persist-gpu PRIVATE | |
| 38 | ${PROJECT_SOURCE_DIR} | |
| 39 | ${CMAKE_BINARY_DIR} | |
| 40 | ) | |
| 38 | 41 | |
| 39 | 42 | TARGET_LINK_LIBRARIES(persist-gpu ${GPU_LINK_LIBS}) |
| 40 | 43 | |
| 44 | # GpuStorageNode depends on generated atom types header. | |
| 45 | ADD_DEPENDENCIES(persist-gpu persist_gpu_atom_types) | |
| 46 | ||
| 41 | 47 | IF (HAVE_CUDA) |
| 42 | 48 | SET_TARGET_PROPERTIES(persist-gpu PROPERTIES |
| 43 | 49 | CUDA_SEPARABLE_COMPILATION ON |
| … | … | @@ -45,13 +51,6 @@ IF (HAVE_CUDA) |
| 45 | 51 | ) |
| 46 | 52 | ENDIF (HAVE_CUDA) |
| 47 | 53 | |
| 48 | ADD_DEPENDENCIES(persist-gpu persist_gpu_atom_types) | |
| 49 | ||
| 50 | ADD_GUILE_EXTENSION(SCM_CONFIG persist-gpu | |
| 51 | "opencog-ext-path-persist-gpu") | |
| 52 | ||
| 53 | # The EXPORT is needed to autogenerate CMake boilerplate in the lib | |
| 54 | # directory that lets other packages FIND_PACKAGE(AtomSpaceGPU) | |
| 55 | 54 | INSTALL (TARGETS persist-gpu |
| 56 | 55 | EXPORT AtomSpaceGPUTargets |
| 57 | 56 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/opencog" |
| … | … | @@ -60,4 +59,3 @@ INSTALL (TARGETS persist-gpu |
| 60 | 59 | INSTALL (FILES ${GPU_INSTALL_HEADERS} |
| 61 | 60 | DESTINATION "include/opencog/persist/gpu" |
| 62 | 61 | ) |
| 63 |
| … | … | @@ -1,642 +0,0 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/CudaBackend.cu | |
| 3 | * | |
| 4 | * CUDA implementation of GpuBackend. | |
| 5 | * Uses device memory with CUDA kernels for hash table operations, | |
| 6 | * and cudaMemcpy for direct value read/write. | |
| 7 | * | |
| 8 | * Managed staging buffers avoid per-operation cudaMalloc overhead. | |
| 9 | * | |
| 10 | * Copyright (C) 2025 OpenCog Foundation | |
| 11 | * | |
| 12 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 13 | */ | |
| 14 | ||
| 15 | #include <cstdio> | |
| 16 | #include <cstring> | |
| 17 | ||
| 18 | #include <opencog/util/exceptions.h> | |
| 19 | #include <opencog/util/Logger.h> | |
| 20 | ||
| 21 | #include "CudaBackend.h" | |
| 22 | ||
| 23 | using namespace opencog; | |
| 24 | ||
| 25 | // ============================================================== | |
| 26 | // CUDA Device Kernels | |
| 27 | // ============================================================== | |
| 28 | ||
| 29 | // splitmix64 hash function (same as OpenCL gpu-hashtable.cl) | |
| 30 | __device__ __forceinline__ | |
| 31 | uint64_t cuda_hash(uint64_t key) | |
| 32 | { | |
| 33 | key ^= key >> 30; | |
| 34 | key *= 0xBF58476D1CE4E5B9ULL; | |
| 35 | key ^= key >> 27; | |
| 36 | key *= 0x94D049BB133111EBULL; | |
| 37 | key ^= key >> 31; | |
| 38 | return key; | |
| 39 | } | |
| 40 | ||
| 41 | #define CUDA_HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 42 | #define CUDA_HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 43 | #define CUDA_MAX_PROBES 4096 | |
| 44 | ||
| 45 | // ---------------------------------------------------------- | |
| 46 | // Generic hash table lookup kernel | |
| 47 | __global__ void cuda_ht_lookup( | |
| 48 | const uint64_t* __restrict__ keys, | |
| 49 | const uint32_t* __restrict__ values, | |
| 50 | uint64_t capacity, | |
| 51 | const uint64_t* __restrict__ query_keys, | |
| 52 | uint32_t* __restrict__ results, | |
| 53 | uint32_t num_queries) | |
| 54 | { | |
| 55 | uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x; | |
| 56 | if (idx >= num_queries) return; | |
| 57 | ||
| 58 | uint64_t key = query_keys[idx]; | |
| 59 | uint64_t slot = cuda_hash(key) % capacity; | |
| 60 | ||
| 61 | for (int probe = 0; probe < CUDA_MAX_PROBES; probe++) | |
| 62 | { | |
| 63 | uint64_t existing = keys[slot]; | |
| 64 | if (existing == key) | |
| 65 | { | |
| 66 | results[idx] = values[slot]; | |
| 67 | return; | |
| 68 | } | |
| 69 | if (existing == CUDA_HT_EMPTY_KEY) | |
| 70 | { | |
| 71 | results[idx] = CUDA_HT_EMPTY_VALUE; | |
| 72 | return; | |
| 73 | } | |
| 74 | slot = (slot + 1) % capacity; | |
| 75 | } | |
| 76 | results[idx] = CUDA_HT_EMPTY_VALUE; | |
| 77 | } | |
| 78 | ||
| 79 | // ---------------------------------------------------------- | |
| 80 | // Generic hash table delete kernel (tombstone) | |
| 81 | __global__ void cuda_ht_delete( | |
| 82 | uint64_t* __restrict__ keys, | |
| 83 | uint32_t* __restrict__ values, | |
| 84 | uint64_t capacity, | |
| 85 | const uint64_t* __restrict__ del_keys, | |
| 86 | uint32_t num_items) | |
| 87 | { | |
| 88 | uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x; | |
| 89 | if (idx >= num_items) return; | |
| 90 | ||
| 91 | uint64_t key = del_keys[idx]; | |
| 92 | uint64_t slot = cuda_hash(key) % capacity; | |
| 93 | ||
| 94 | for (int probe = 0; probe < CUDA_MAX_PROBES; probe++) | |
| 95 | { | |
| 96 | uint64_t existing = keys[slot]; | |
| 97 | if (existing == key) | |
| 98 | { | |
| 99 | // Tombstone: mark as deleted (use EMPTY-1 as tombstone) | |
| 100 | keys[slot] = 0xFFFFFFFFFFFFFFFEULL; | |
| 101 | values[slot] = CUDA_HT_EMPTY_VALUE; | |
| 102 | return; | |
| 103 | } | |
| 104 | if (existing == CUDA_HT_EMPTY_KEY) return; // not found | |
| 105 | slot = (slot + 1) % capacity; | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | // ---------------------------------------------------------- | |
| 110 | // Word find-or-create: hash table insert + pool allocation | |
| 111 | __global__ void cuda_word_find_or_create( | |
| 112 | uint64_t* __restrict__ ht_keys, | |
| 113 | uint32_t* __restrict__ ht_values, | |
| 114 | uint64_t* __restrict__ name_hashes, | |
| 115 | double* __restrict__ word_counts, | |
| 116 | uint32_t* __restrict__ class_ids, | |
| 117 | uint32_t* __restrict__ next_free, | |
| 118 | const uint64_t* __restrict__ in_hashes, | |
| 119 | uint32_t* __restrict__ out_indices, | |
| 120 | uint32_t num_items) | |
| 121 | { | |
| 122 | uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x; | |
| 123 | if (idx >= num_items) return; | |
| 124 | ||
| 125 | uint64_t key = in_hashes[idx]; | |
| 126 | uint64_t slot = cuda_hash(key) % GPU_WORD_HT_CAPACITY; | |
| 127 | ||
| 128 | for (int probe = 0; probe < CUDA_MAX_PROBES; probe++) | |
| 129 | { | |
| 130 | uint64_t old = atomicCAS( | |
| 131 | (unsigned long long*)&ht_keys[slot], | |
| 132 | (unsigned long long)CUDA_HT_EMPTY_KEY, | |
| 133 | (unsigned long long)key); | |
| 134 | ||
| 135 | if (old == key) | |
| 136 | { | |
| 137 | // Already exists | |
| 138 | out_indices[idx] = ht_values[slot]; | |
| 139 | return; | |
| 140 | } | |
| 141 | if (old == CUDA_HT_EMPTY_KEY) | |
| 142 | { | |
| 143 | // We won the slot; allocate pool entry | |
| 144 | uint32_t pool_idx = atomicAdd(next_free, 1); | |
| 145 | ht_values[slot] = pool_idx; | |
| 146 | name_hashes[pool_idx] = key; | |
| 147 | word_counts[pool_idx] = 0.0; | |
| 148 | class_ids[pool_idx] = 0; | |
| 149 | out_indices[idx] = pool_idx; | |
| 150 | return; | |
| 151 | } | |
| 152 | slot = (slot + 1) % GPU_WORD_HT_CAPACITY; | |
| 153 | } | |
| 154 | out_indices[idx] = CUDA_HT_EMPTY_VALUE; | |
| 155 | } | |
| 156 | ||
| 157 | // ---------------------------------------------------------- | |
| 158 | // Pair find-or-create: hash table insert + pool allocation | |
| 159 | __global__ void cuda_pair_find_or_create( | |
| 160 | uint64_t* __restrict__ ht_keys, | |
| 161 | uint32_t* __restrict__ ht_values, | |
| 162 | uint32_t* __restrict__ pair_wa, | |
| 163 | uint32_t* __restrict__ pair_wb, | |
| 164 | double* __restrict__ pair_counts, | |
| 165 | double* __restrict__ pair_mis, | |
| 166 | uint32_t* __restrict__ pair_flags, | |
| 167 | uint32_t* __restrict__ next_free, | |
| 168 | const uint32_t* __restrict__ in_wa, | |
| 169 | const uint32_t* __restrict__ in_wb, | |
| 170 | uint32_t* __restrict__ out_indices, | |
| 171 | uint32_t num_items) | |
| 172 | { | |
| 173 | uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x; | |
| 174 | if (idx >= num_items) return; | |
| 175 | ||
| 176 | uint32_t wa = in_wa[idx]; | |
| 177 | uint32_t wb = in_wb[idx]; | |
| 178 | uint32_t lo = min(wa, wb); | |
| 179 | uint32_t hi = max(wa, wb); | |
| 180 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 181 | ||
| 182 | uint64_t slot = cuda_hash(key) % GPU_PAIR_HT_CAPACITY; | |
| 183 | ||
| 184 | for (int probe = 0; probe < CUDA_MAX_PROBES; probe++) | |
| 185 | { | |
| 186 | uint64_t old = atomicCAS( | |
| 187 | (unsigned long long*)&ht_keys[slot], | |
| 188 | (unsigned long long)CUDA_HT_EMPTY_KEY, | |
| 189 | (unsigned long long)key); | |
| 190 | ||
| 191 | if (old == key) | |
| 192 | { | |
| 193 | out_indices[idx] = ht_values[slot]; | |
| 194 | return; | |
| 195 | } | |
| 196 | if (old == CUDA_HT_EMPTY_KEY) | |
| 197 | { | |
| 198 | uint32_t pool_idx = atomicAdd(next_free, 1); | |
| 199 | ht_values[slot] = pool_idx; | |
| 200 | pair_wa[pool_idx] = wa; | |
| 201 | pair_wb[pool_idx] = wb; | |
| 202 | pair_counts[pool_idx] = 0.0; | |
| 203 | pair_mis[pool_idx] = 0.0; | |
| 204 | pair_flags[pool_idx] = 0; | |
| 205 | out_indices[idx] = pool_idx; | |
| 206 | return; | |
| 207 | } | |
| 208 | slot = (slot + 1) % GPU_PAIR_HT_CAPACITY; | |
| 209 | } | |
| 210 | out_indices[idx] = CUDA_HT_EMPTY_VALUE; | |
| 211 | } | |
| 212 | ||
| 213 | // ============================================================== | |
| 214 | // Host-side Implementation | |
| 215 | // ============================================================== | |
| 216 | ||
| 217 | CudaBackend::CudaBackend() | |
| 218 | : _device_id(-1), | |
| 219 | _d_word_name_hash(nullptr), _d_word_count(nullptr), | |
| 220 | _d_word_mi_marginal(nullptr), _d_word_class_id(nullptr), | |
| 221 | _d_word_next_free(nullptr), | |
| 222 | _d_pair_word_a(nullptr), _d_pair_word_b(nullptr), | |
| 223 | _d_pair_count(nullptr), _d_pair_mi(nullptr), | |
| 224 | _d_pair_flags(nullptr), _d_pair_next_free(nullptr), | |
| 225 | _d_sec_word(nullptr), _d_sec_djh(nullptr), | |
| 226 | _d_sec_count(nullptr), _d_sec_next_free(nullptr), | |
| 227 | _d_word_ht_keys(nullptr), _d_word_ht_values(nullptr), | |
| 228 | _d_pair_ht_keys(nullptr), _d_pair_ht_values(nullptr), | |
| 229 | _d_sec_ht_keys(nullptr), _d_sec_ht_values(nullptr), | |
| 230 | _staging_key(nullptr), _staging_result(nullptr), | |
| 231 | _staging_wa(nullptr), _staging_wb(nullptr) | |
| 232 | { | |
| 233 | } | |
| 234 | ||
| 235 | CudaBackend::~CudaBackend() | |
| 236 | { | |
| 237 | shutdown(); | |
| 238 | } | |
| 239 | ||
| 240 | void CudaBackend::check_error(cudaError_t err, const char* context) | |
| 241 | { | |
| 242 | if (err != cudaSuccess) | |
| 243 | throw RuntimeException(TRACE_INFO, | |
| 244 | "CudaBackend: %s failed: %s\n", | |
| 245 | context, cudaGetErrorString(err)); | |
| 246 | } | |
| 247 | ||
| 248 | // ============================================================== | |
| 249 | // Lifecycle | |
| 250 | ||
| 251 | void CudaBackend::init(const std::string& platform_hint, | |
| 252 | const std::string& device_hint) | |
| 253 | { | |
| 254 | int device_count = 0; | |
| 255 | check_error(cudaGetDeviceCount(&device_count), "cudaGetDeviceCount"); | |
| 256 | ||
| 257 | if (device_count == 0) | |
| 258 | throw RuntimeException(TRACE_INFO, | |
| 259 | "CudaBackend: no CUDA devices found\n"); | |
| 260 | ||
| 261 | // Find a matching device (by name substring) | |
| 262 | _device_id = -1; | |
| 263 | for (int i = 0; i < device_count; i++) | |
| 264 | { | |
| 265 | cudaDeviceProp prop; | |
| 266 | check_error(cudaGetDeviceProperties(&prop, i), | |
| 267 | "cudaGetDeviceProperties"); | |
| 268 | ||
| 269 | std::string dname(prop.name); | |
| 270 | ||
| 271 | // Check platform hint (vendor match) | |
| 272 | if (0 < platform_hint.size()) | |
| 273 | { | |
| 274 | // NVIDIA is always the CUDA platform, so skip platform matching | |
| 275 | // unless the user explicitly asked for non-NVIDIA | |
| 276 | if (dname.find(platform_hint) == std::string::npos and | |
| 277 | platform_hint != "NVIDIA" and platform_hint != "nvidia") | |
| 278 | continue; | |
| 279 | } | |
| 280 | ||
| 281 | // Check device hint (model match) | |
| 282 | if (0 < device_hint.size() and | |
| 283 | dname.find(device_hint) == std::string::npos) | |
| 284 | continue; | |
| 285 | ||
| 286 | _device_id = i; | |
| 287 | _device_name = dname; | |
| 288 | break; | |
| 289 | } | |
| 290 | ||
| 291 | if (_device_id < 0) | |
| 292 | throw RuntimeException(TRACE_INFO, | |
| 293 | "CudaBackend: no matching CUDA device for '%s:%s'\n", | |
| 294 | platform_hint.c_str(), device_hint.c_str()); | |
| 295 | ||
| 296 | check_error(cudaSetDevice(_device_id), "cudaSetDevice"); | |
| 297 | ||
| 298 | logger().info("CudaBackend: Using device %d '%s'\n", | |
| 299 | _device_id, _device_name.c_str()); | |
| 300 | } | |
| 301 | ||
| 302 | void CudaBackend::shutdown() | |
| 303 | { | |
| 304 | if (_device_id < 0) return; | |
| 305 | ||
| 306 | cudaDeviceSynchronize(); | |
| 307 | ||
| 308 | // Free device memory | |
| 309 | if (_d_word_name_hash) cudaFree(_d_word_name_hash); | |
| 310 | if (_d_word_count) cudaFree(_d_word_count); | |
| 311 | if (_d_word_mi_marginal) cudaFree(_d_word_mi_marginal); | |
| 312 | if (_d_word_class_id) cudaFree(_d_word_class_id); | |
| 313 | if (_d_word_next_free) cudaFree(_d_word_next_free); | |
| 314 | ||
| 315 | if (_d_pair_word_a) cudaFree(_d_pair_word_a); | |
| 316 | if (_d_pair_word_b) cudaFree(_d_pair_word_b); | |
| 317 | if (_d_pair_count) cudaFree(_d_pair_count); | |
| 318 | if (_d_pair_mi) cudaFree(_d_pair_mi); | |
| 319 | if (_d_pair_flags) cudaFree(_d_pair_flags); | |
| 320 | if (_d_pair_next_free) cudaFree(_d_pair_next_free); | |
| 321 | ||
| 322 | if (_d_sec_word) cudaFree(_d_sec_word); | |
| 323 | if (_d_sec_djh) cudaFree(_d_sec_djh); | |
| 324 | if (_d_sec_count) cudaFree(_d_sec_count); | |
| 325 | if (_d_sec_next_free) cudaFree(_d_sec_next_free); | |
| 326 | ||
| 327 | if (_d_word_ht_keys) cudaFree(_d_word_ht_keys); | |
| 328 | if (_d_word_ht_values) cudaFree(_d_word_ht_values); | |
| 329 | if (_d_pair_ht_keys) cudaFree(_d_pair_ht_keys); | |
| 330 | if (_d_pair_ht_values) cudaFree(_d_pair_ht_values); | |
| 331 | if (_d_sec_ht_keys) cudaFree(_d_sec_ht_keys); | |
| 332 | if (_d_sec_ht_values) cudaFree(_d_sec_ht_values); | |
| 333 | ||
| 334 | // Free managed staging buffers | |
| 335 | if (_staging_key) cudaFree(_staging_key); | |
| 336 | if (_staging_result) cudaFree(_staging_result); | |
| 337 | if (_staging_wa) cudaFree(_staging_wa); | |
| 338 | if (_staging_wb) cudaFree(_staging_wb); | |
| 339 | ||
| 340 | // Reset all pointers | |
| 341 | _d_word_name_hash = nullptr; | |
| 342 | _d_word_count = nullptr; | |
| 343 | _d_word_mi_marginal = nullptr; | |
| 344 | _d_word_class_id = nullptr; | |
| 345 | _d_word_next_free = nullptr; | |
| 346 | _d_pair_word_a = nullptr; | |
| 347 | _d_pair_word_b = nullptr; | |
| 348 | _d_pair_count = nullptr; | |
| 349 | _d_pair_mi = nullptr; | |
| 350 | _d_pair_flags = nullptr; | |
| 351 | _d_pair_next_free = nullptr; | |
| 352 | _d_sec_word = nullptr; | |
| 353 | _d_sec_djh = nullptr; | |
| 354 | _d_sec_count = nullptr; | |
| 355 | _d_sec_next_free = nullptr; | |
| 356 | _d_word_ht_keys = nullptr; | |
| 357 | _d_word_ht_values = nullptr; | |
| 358 | _d_pair_ht_keys = nullptr; | |
| 359 | _d_pair_ht_values = nullptr; | |
| 360 | _d_sec_ht_keys = nullptr; | |
| 361 | _d_sec_ht_values = nullptr; | |
| 362 | _staging_key = nullptr; | |
| 363 | _staging_result = nullptr; | |
| 364 | _staging_wa = nullptr; | |
| 365 | _staging_wb = nullptr; | |
| 366 | ||
| 367 | _device_id = -1; | |
| 368 | } | |
| 369 | ||
| 370 | std::string CudaBackend::device_info() | |
| 371 | { | |
| 372 | return "CUDA / " + _device_name; | |
| 373 | } | |
| 374 | ||
| 375 | // ============================================================== | |
| 376 | // Pool allocation | |
| 377 | ||
| 378 | void CudaBackend::alloc_pools() | |
| 379 | { | |
| 380 | // -- WordPool -- | |
| 381 | check_error(cudaMalloc(&_d_word_name_hash, | |
| 382 | sizeof(uint64_t) * GPU_WORD_CAPACITY), "alloc word_name_hash"); | |
| 383 | check_error(cudaMalloc(&_d_word_count, | |
| 384 | sizeof(double) * GPU_WORD_CAPACITY), "alloc word_count"); | |
| 385 | check_error(cudaMalloc(&_d_word_mi_marginal, | |
| 386 | sizeof(double) * GPU_WORD_CAPACITY), "alloc word_mi_marginal"); | |
| 387 | check_error(cudaMalloc(&_d_word_class_id, | |
| 388 | sizeof(uint32_t) * GPU_WORD_CAPACITY), "alloc word_class_id"); | |
| 389 | check_error(cudaMalloc(&_d_word_next_free, | |
| 390 | sizeof(uint32_t)), "alloc word_next_free"); | |
| 391 | ||
| 392 | // -- PairPool -- | |
| 393 | check_error(cudaMalloc(&_d_pair_word_a, | |
| 394 | sizeof(uint32_t) * GPU_PAIR_CAPACITY), "alloc pair_word_a"); | |
| 395 | check_error(cudaMalloc(&_d_pair_word_b, | |
| 396 | sizeof(uint32_t) * GPU_PAIR_CAPACITY), "alloc pair_word_b"); | |
| 397 | check_error(cudaMalloc(&_d_pair_count, | |
| 398 | sizeof(double) * GPU_PAIR_CAPACITY), "alloc pair_count"); | |
| 399 | check_error(cudaMalloc(&_d_pair_mi, | |
| 400 | sizeof(double) * GPU_PAIR_CAPACITY), "alloc pair_mi"); | |
| 401 | check_error(cudaMalloc(&_d_pair_flags, | |
| 402 | sizeof(uint32_t) * GPU_PAIR_CAPACITY), "alloc pair_flags"); | |
| 403 | check_error(cudaMalloc(&_d_pair_next_free, | |
| 404 | sizeof(uint32_t)), "alloc pair_next_free"); | |
| 405 | ||
| 406 | // -- SectionPool -- | |
| 407 | check_error(cudaMalloc(&_d_sec_word, | |
| 408 | sizeof(uint32_t) * GPU_SECTION_CAPACITY), "alloc sec_word"); | |
| 409 | check_error(cudaMalloc(&_d_sec_djh, | |
| 410 | sizeof(uint64_t) * GPU_SECTION_CAPACITY), "alloc sec_djh"); | |
| 411 | check_error(cudaMalloc(&_d_sec_count, | |
| 412 | sizeof(double) * GPU_SECTION_CAPACITY), "alloc sec_count"); | |
| 413 | check_error(cudaMalloc(&_d_sec_next_free, | |
| 414 | sizeof(uint32_t)), "alloc sec_next_free"); | |
| 415 | ||
| 416 | // -- Hash tables -- | |
| 417 | check_error(cudaMalloc(&_d_word_ht_keys, | |
| 418 | sizeof(uint64_t) * GPU_WORD_HT_CAPACITY), "alloc word_ht_keys"); | |
| 419 | check_error(cudaMalloc(&_d_word_ht_values, | |
| 420 | sizeof(uint32_t) * GPU_WORD_HT_CAPACITY), "alloc word_ht_values"); | |
| 421 | check_error(cudaMalloc(&_d_pair_ht_keys, | |
| 422 | sizeof(uint64_t) * GPU_PAIR_HT_CAPACITY), "alloc pair_ht_keys"); | |
| 423 | check_error(cudaMalloc(&_d_pair_ht_values, | |
| 424 | sizeof(uint32_t) * GPU_PAIR_HT_CAPACITY), "alloc pair_ht_values"); | |
| 425 | check_error(cudaMalloc(&_d_sec_ht_keys, | |
| 426 | sizeof(uint64_t) * GPU_SECTION_HT_CAPACITY), "alloc sec_ht_keys"); | |
| 427 | check_error(cudaMalloc(&_d_sec_ht_values, | |
| 428 | sizeof(uint32_t) * GPU_SECTION_HT_CAPACITY), "alloc sec_ht_values"); | |
| 429 | ||
| 430 | // -- Managed staging buffers (CPU+GPU accessible) -- | |
| 431 | check_error(cudaMallocManaged(&_staging_key, sizeof(uint64_t)), | |
| 432 | "alloc staging_key"); | |
| 433 | check_error(cudaMallocManaged(&_staging_result, sizeof(uint32_t)), | |
| 434 | "alloc staging_result"); | |
| 435 | check_error(cudaMallocManaged(&_staging_wa, sizeof(uint32_t)), | |
| 436 | "alloc staging_wa"); | |
| 437 | check_error(cudaMallocManaged(&_staging_wb, sizeof(uint32_t)), | |
| 438 | "alloc staging_wb"); | |
| 439 | } | |
| 440 | ||
| 441 | void CudaBackend::init_pools() | |
| 442 | { | |
| 443 | // Zero bump allocators | |
| 444 | uint32_t zero = 0; | |
| 445 | cudaMemcpy(_d_word_next_free, &zero, sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 446 | cudaMemcpy(_d_pair_next_free, &zero, sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 447 | cudaMemcpy(_d_sec_next_free, &zero, sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 448 | ||
| 449 | // Initialize hash table keys to EMPTY (0xFF bytes) | |
| 450 | cudaMemset(_d_word_ht_keys, 0xFF, sizeof(uint64_t) * GPU_WORD_HT_CAPACITY); | |
| 451 | cudaMemset(_d_word_ht_values, 0xFF, sizeof(uint32_t) * GPU_WORD_HT_CAPACITY); | |
| 452 | cudaMemset(_d_pair_ht_keys, 0xFF, sizeof(uint64_t) * GPU_PAIR_HT_CAPACITY); | |
| 453 | cudaMemset(_d_pair_ht_values, 0xFF, sizeof(uint32_t) * GPU_PAIR_HT_CAPACITY); | |
| 454 | cudaMemset(_d_sec_ht_keys, 0xFF, sizeof(uint64_t) * GPU_SECTION_HT_CAPACITY); | |
| 455 | cudaMemset(_d_sec_ht_values, 0xFF, sizeof(uint32_t) * GPU_SECTION_HT_CAPACITY); | |
| 456 | ||
| 457 | cudaDeviceSynchronize(); | |
| 458 | ||
| 459 | logger().info("CudaBackend: GPU pools initialized\n"); | |
| 460 | } | |
| 461 | ||
| 462 | // ============================================================== | |
| 463 | // Word pool operations | |
| 464 | ||
| 465 | uint32_t CudaBackend::word_find_or_create(uint64_t name_hash) | |
| 466 | { | |
| 467 | *_staging_key = name_hash; | |
| 468 | *_staging_result = GPU_NOT_FOUND; | |
| 469 | ||
| 470 | cuda_word_find_or_create<<<1, 1>>>( | |
| 471 | _d_word_ht_keys, _d_word_ht_values, | |
| 472 | _d_word_name_hash, _d_word_count, _d_word_class_id, | |
| 473 | _d_word_next_free, | |
| 474 | _staging_key, _staging_result, 1); | |
| 475 | cudaDeviceSynchronize(); | |
| 476 | ||
| 477 | return *_staging_result; | |
| 478 | } | |
| 479 | ||
| 480 | uint32_t CudaBackend::word_lookup(uint64_t name_hash) | |
| 481 | { | |
| 482 | *_staging_key = name_hash; | |
| 483 | *_staging_result = GPU_NOT_FOUND; | |
| 484 | ||
| 485 | cuda_ht_lookup<<<1, 1>>>( | |
| 486 | _d_word_ht_keys, _d_word_ht_values, | |
| 487 | (uint64_t)GPU_WORD_HT_CAPACITY, | |
| 488 | _staging_key, _staging_result, 1); | |
| 489 | cudaDeviceSynchronize(); | |
| 490 | ||
| 491 | return *_staging_result; | |
| 492 | } | |
| 493 | ||
| 494 | void CudaBackend::word_write_count(uint32_t idx, double count) | |
| 495 | { | |
| 496 | cudaMemcpy(_d_word_count + idx, &count, | |
| 497 | sizeof(double), cudaMemcpyHostToDevice); | |
| 498 | } | |
| 499 | ||
| 500 | void CudaBackend::word_write_marginal(uint32_t idx, double marginal) | |
| 501 | { | |
| 502 | cudaMemcpy(_d_word_mi_marginal + idx, &marginal, | |
| 503 | sizeof(double), cudaMemcpyHostToDevice); | |
| 504 | } | |
| 505 | ||
| 506 | void CudaBackend::word_read_values(uint32_t idx, | |
| 507 | double& count, double& marginal) | |
| 508 | { | |
| 509 | cudaMemcpy(&count, _d_word_count + idx, | |
| 510 | sizeof(double), cudaMemcpyDeviceToHost); | |
| 511 | cudaMemcpy(&marginal, _d_word_mi_marginal + idx, | |
| 512 | sizeof(double), cudaMemcpyDeviceToHost); | |
| 513 | } | |
| 514 | ||
| 515 | void CudaBackend::word_delete(uint64_t name_hash) | |
| 516 | { | |
| 517 | *_staging_key = name_hash; | |
| 518 | ||
| 519 | cuda_ht_delete<<<1, 1>>>( | |
| 520 | _d_word_ht_keys, _d_word_ht_values, | |
| 521 | (uint64_t)GPU_WORD_HT_CAPACITY, | |
| 522 | _staging_key, 1); | |
| 523 | cudaDeviceSynchronize(); | |
| 524 | } | |
| 525 | ||
| 526 | uint32_t CudaBackend::word_pool_count() | |
| 527 | { | |
| 528 | uint32_t cnt = 0; | |
| 529 | cudaMemcpy(&cnt, _d_word_next_free, | |
| 530 | sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 531 | return cnt; | |
| 532 | } | |
| 533 | ||
| 534 | void CudaBackend::word_read_bulk(uint32_t n, uint64_t* hashes, | |
| 535 | double* counts, double* marginals) | |
| 536 | { | |
| 537 | cudaMemcpy(hashes, _d_word_name_hash, | |
| 538 | sizeof(uint64_t) * n, cudaMemcpyDeviceToHost); | |
| 539 | cudaMemcpy(counts, _d_word_count, | |
| 540 | sizeof(double) * n, cudaMemcpyDeviceToHost); | |
| 541 | cudaMemcpy(marginals, _d_word_mi_marginal, | |
| 542 | sizeof(double) * n, cudaMemcpyDeviceToHost); | |
| 543 | } | |
| 544 | ||
| 545 | // ============================================================== | |
| 546 | // Pair pool operations | |
| 547 | ||
| 548 | uint32_t CudaBackend::pair_find_or_create(uint32_t word_a, uint32_t word_b) | |
| 549 | { | |
| 550 | *_staging_wa = word_a; | |
| 551 | *_staging_wb = word_b; | |
| 552 | *_staging_result = GPU_NOT_FOUND; | |
| 553 | ||
| 554 | cuda_pair_find_or_create<<<1, 1>>>( | |
| 555 | _d_pair_ht_keys, _d_pair_ht_values, | |
| 556 | _d_pair_word_a, _d_pair_word_b, | |
| 557 | _d_pair_count, _d_pair_mi, _d_pair_flags, | |
| 558 | _d_pair_next_free, | |
| 559 | _staging_wa, _staging_wb, _staging_result, 1); | |
| 560 | cudaDeviceSynchronize(); | |
| 561 | ||
| 562 | return *_staging_result; | |
| 563 | } | |
| 564 | ||
| 565 | uint32_t CudaBackend::pair_lookup(uint32_t word_a, uint32_t word_b) | |
| 566 | { | |
| 567 | uint32_t lo = std::min(word_a, word_b); | |
| 568 | uint32_t hi = std::max(word_a, word_b); | |
| 569 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 570 | ||
| 571 | *_staging_key = key; | |
| 572 | *_staging_result = GPU_NOT_FOUND; | |
| 573 | ||
| 574 | cuda_ht_lookup<<<1, 1>>>( | |
| 575 | _d_pair_ht_keys, _d_pair_ht_values, | |
| 576 | (uint64_t)GPU_PAIR_HT_CAPACITY, | |
| 577 | _staging_key, _staging_result, 1); | |
| 578 | cudaDeviceSynchronize(); | |
| 579 | ||
| 580 | return *_staging_result; | |
| 581 | } | |
| 582 | ||
| 583 | void CudaBackend::pair_write_count(uint32_t idx, double count) | |
| 584 | { | |
| 585 | cudaMemcpy(_d_pair_count + idx, &count, | |
| 586 | sizeof(double), cudaMemcpyHostToDevice); | |
| 587 | } | |
| 588 | ||
| 589 | void CudaBackend::pair_write_mi(uint32_t idx, double mi) | |
| 590 | { | |
| 591 | cudaMemcpy(_d_pair_mi + idx, &mi, | |
| 592 | sizeof(double), cudaMemcpyHostToDevice); | |
| 593 | } | |
| 594 | ||
| 595 | void CudaBackend::pair_read_values(uint32_t idx, | |
| 596 | double& count, double& mi) | |
| 597 | { | |
| 598 | cudaMemcpy(&count, _d_pair_count + idx, | |
| 599 | sizeof(double), cudaMemcpyDeviceToHost); | |
| 600 | cudaMemcpy(&mi, _d_pair_mi + idx, | |
| 601 | sizeof(double), cudaMemcpyDeviceToHost); | |
| 602 | } | |
| 603 | ||
| 604 | uint32_t CudaBackend::pair_pool_count() | |
| 605 | { | |
| 606 | uint32_t cnt = 0; | |
| 607 | cudaMemcpy(&cnt, _d_pair_next_free, | |
| 608 | sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 609 | return cnt; | |
| 610 | } | |
| 611 | ||
| 612 | // ============================================================== | |
| 613 | // Section pool operations | |
| 614 | ||
| 615 | uint32_t CudaBackend::section_pool_count() | |
| 616 | { | |
| 617 | uint32_t cnt = 0; | |
| 618 | cudaMemcpy(&cnt, _d_sec_next_free, | |
| 619 | sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 620 | return cnt; | |
| 621 | } | |
| 622 | ||
| 623 | // ============================================================== | |
| 624 | ||
| 625 | void CudaBackend::barrier() | |
| 626 | { | |
| 627 | cudaDeviceSynchronize(); | |
| 628 | } | |
| 629 | ||
| 630 | // ============================================================== | |
| 631 | // Factory function | |
| 632 | ||
| 633 | namespace opencog { | |
| 634 | ||
| 635 | GpuBackend* create_cuda_backend() | |
| 636 | { | |
| 637 | return new CudaBackend(); | |
| 638 | } | |
| 639 | ||
| 640 | } // namespace opencog | |
| 641 | ||
| 642 | // ============================================================== |
| … | … | @@ -1,109 +0,0 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/CudaBackend.h | |
| 3 | * | |
| 4 | * CUDA implementation of the GpuBackend interface. | |
| 5 | * Primary backend for NVIDIA GPUs (more performant than OpenCL). | |
| 6 | * | |
| 7 | * NOTE: This header is only included by CudaBackend.cu, which is | |
| 8 | * compiled by nvcc. It must NOT be included by regular C++ files. | |
| 9 | * | |
| 10 | * Copyright (C) 2025 OpenCog Foundation | |
| 11 | * | |
| 12 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 13 | */ | |
| 14 | ||
| 15 | #ifndef _OPENCOG_CUDA_BACKEND_H | |
| 16 | #define _OPENCOG_CUDA_BACKEND_H | |
| 17 | ||
| 18 | #include "GpuBackend.h" | |
| 19 | #include "gpu-pool-defs.h" | |
| 20 | ||
| 21 | #include <cuda_runtime.h> | |
| 22 | ||
| 23 | namespace opencog | |
| 24 | { | |
| 25 | ||
| 26 | class CudaBackend : public GpuBackend | |
| 27 | { | |
| 28 | private: | |
| 29 | int _device_id; | |
| 30 | std::string _device_name; | |
| 31 | ||
| 32 | // Device memory -- WordPool SoA | |
| 33 | uint64_t* _d_word_name_hash; | |
| 34 | double* _d_word_count; | |
| 35 | double* _d_word_mi_marginal; | |
| 36 | uint32_t* _d_word_class_id; | |
| 37 | uint32_t* _d_word_next_free; | |
| 38 | ||
| 39 | // Device memory -- PairPool SoA | |
| 40 | uint32_t* _d_pair_word_a; | |
| 41 | uint32_t* _d_pair_word_b; | |
| 42 | double* _d_pair_count; | |
| 43 | double* _d_pair_mi; | |
| 44 | uint32_t* _d_pair_flags; | |
| 45 | uint32_t* _d_pair_next_free; | |
| 46 | ||
| 47 | // Device memory -- SectionPool SoA | |
| 48 | uint32_t* _d_sec_word; | |
| 49 | uint64_t* _d_sec_djh; | |
| 50 | double* _d_sec_count; | |
| 51 | uint32_t* _d_sec_next_free; | |
| 52 | ||
| 53 | // Device memory -- Hash tables | |
| 54 | uint64_t* _d_word_ht_keys; | |
| 55 | uint32_t* _d_word_ht_values; | |
| 56 | uint64_t* _d_pair_ht_keys; | |
| 57 | uint32_t* _d_pair_ht_values; | |
| 58 | uint64_t* _d_sec_ht_keys; | |
| 59 | uint32_t* _d_sec_ht_values; | |
| 60 | ||
| 61 | // Managed staging buffers (CPU+GPU accessible, no cudaMemcpy) | |
| 62 | uint64_t* _staging_key; | |
| 63 | uint32_t* _staging_result; | |
| 64 | uint32_t* _staging_wa; | |
| 65 | uint32_t* _staging_wb; | |
| 66 | ||
| 67 | void check_error(cudaError_t err, const char* context); | |
| 68 | ||
| 69 | public: | |
| 70 | CudaBackend(); | |
| 71 | ~CudaBackend() override; | |
| 72 | ||
| 73 | void init(const std::string& platform_hint, | |
| 74 | const std::string& device_hint) override; | |
| 75 | void shutdown() override; | |
| 76 | std::string device_info() override; | |
| 77 | std::string backend_name() override { return "CUDA"; } | |
| 78 | ||
| 79 | void alloc_pools() override; | |
| 80 | void init_pools() override; | |
| 81 | ||
| 82 | uint32_t word_find_or_create(uint64_t name_hash) override; | |
| 83 | uint32_t word_lookup(uint64_t name_hash) override; | |
| 84 | void word_write_count(uint32_t idx, double count) override; | |
| 85 | void word_write_marginal(uint32_t idx, double marginal) override; | |
| 86 | void word_read_values(uint32_t idx, | |
| 87 | double& count, double& marginal) override; | |
| 88 | void word_delete(uint64_t name_hash) override; | |
| 89 | uint32_t word_pool_count() override; | |
| 90 | void word_read_bulk(uint32_t n, uint64_t* hashes, | |
| 91 | double* counts, double* marginals) override; | |
| 92 | ||
| 93 | uint32_t pair_find_or_create(uint32_t word_a, | |
| 94 | uint32_t word_b) override; | |
| 95 | uint32_t pair_lookup(uint32_t word_a, uint32_t word_b) override; | |
| 96 | void pair_write_count(uint32_t idx, double count) override; | |
| 97 | void pair_write_mi(uint32_t idx, double mi) override; | |
| 98 | void pair_read_values(uint32_t idx, | |
| 99 | double& count, double& mi) override; | |
| 100 | uint32_t pair_pool_count() override; | |
| 101 | ||
| 102 | uint32_t section_pool_count() override; | |
| 103 | ||
| 104 | void barrier() override; | |
| 105 | }; | |
| 106 | ||
| 107 | } // namespace opencog | |
| 108 | ||
| 109 | #endif // _OPENCOG_CUDA_BACKEND_H |
| … | … | @@ -0,0 +1,232 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/GpuAtomTable.cu | |
| 3 | * | |
| 4 | * CUDA implementation of the GpuAtomTable API. | |
| 5 | * | |
| 6 | * AoS for per-atom fixed fields: one GpuAtom struct per slot, | |
| 7 | * one cudaMemcpy to store/fetch all fields of one atom. | |
| 8 | * Variable-length data (names, outgoing sets) in side pools. | |
| 9 | * | |
| 10 | * Copyright (C) 2026 OpenCog Foundation | |
| 11 | * | |
| 12 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 13 | */ | |
| 14 | ||
| 15 | #include "GpuAtomTable.h" | |
| 16 | ||
| 17 | #include <cuda_runtime.h> | |
| 18 | #include <cstdio> | |
| 19 | #include <cstring> | |
| 20 | ||
| 21 | /* ---------------------------------------------------------------- */ | |
| 22 | #define CUDA_CHECK(call, ctx) do { \ | |
| 23 | cudaError_t _e = (call); \ | |
| 24 | if (_e != cudaSuccess) { \ | |
| 25 | fprintf(stderr, "CUDA error in %s: %s\n", \ | |
| 26 | (ctx), cudaGetErrorString(_e)); \ | |
| 27 | return -1; \ | |
| 28 | } \ | |
| 29 | } while(0) | |
| 30 | ||
| 31 | #define CUDA_CHECK_VOID(call, ctx) do { \ | |
| 32 | cudaError_t _e = (call); \ | |
| 33 | if (_e != cudaSuccess) { \ | |
| 34 | fprintf(stderr, "CUDA error in %s: %s\n", \ | |
| 35 | (ctx), cudaGetErrorString(_e)); \ | |
| 36 | } \ | |
| 37 | } while(0) | |
| 38 | ||
| 39 | /* ================================================================ | |
| 40 | * gpu_table_alloc | |
| 41 | * ================================================================ */ | |
| 42 | extern "C" | |
| 43 | int cuda_table_alloc(GpuAtomTable* t) | |
| 44 | { | |
| 45 | memset(t, 0, sizeof(GpuAtomTable)); | |
| 46 | ||
| 47 | CUDA_CHECK(cudaMalloc(&t->atoms, GPU_ATOM_CAPACITY * sizeof(GpuAtom)), | |
| 48 | "alloc atoms"); | |
| 49 | CUDA_CHECK(cudaMalloc(&t->name_pool, GPU_NAME_POOL_BYTES), | |
| 50 | "alloc name_pool"); | |
| 51 | CUDA_CHECK(cudaMalloc(&t->out_pool, GPU_OUT_POOL_SLOTS * sizeof(uint32_t)), | |
| 52 | "alloc out_pool"); | |
| 53 | ||
| 54 | CUDA_CHECK(cudaMemset(t->atoms, 0, GPU_ATOM_CAPACITY * sizeof(GpuAtom)), | |
| 55 | "zero atoms"); | |
| 56 | CUDA_CHECK(cudaMemset(t->name_pool, 0, GPU_NAME_POOL_BYTES), | |
| 57 | "zero name_pool"); | |
| 58 | CUDA_CHECK(cudaMemset(t->out_pool, 0, GPU_OUT_POOL_SLOTS * sizeof(uint32_t)), | |
| 59 | "zero out_pool"); | |
| 60 | ||
| 61 | t->atom_count = 0; | |
| 62 | t->name_pool_used = 0; | |
| 63 | t->out_pool_used = 0; | |
| 64 | ||
| 65 | return 0; | |
| 66 | } | |
| 67 | ||
| 68 | /* ================================================================ | |
| 69 | * gpu_table_free | |
| 70 | * ================================================================ */ | |
| 71 | extern "C" | |
| 72 | void cuda_table_free(GpuAtomTable* t) | |
| 73 | { | |
| 74 | if (t->atoms) cudaFree(t->atoms); | |
| 75 | if (t->name_pool) cudaFree(t->name_pool); | |
| 76 | if (t->out_pool) cudaFree(t->out_pool); | |
| 77 | ||
| 78 | memset(t, 0, sizeof(GpuAtomTable)); | |
| 79 | } | |
| 80 | ||
| 81 | /* ================================================================ | |
| 82 | * gpu_table_clear | |
| 83 | * ================================================================ */ | |
| 84 | extern "C" | |
| 85 | void cuda_table_clear(GpuAtomTable* t) | |
| 86 | { | |
| 87 | CUDA_CHECK_VOID(cudaMemset(t->atoms, 0, GPU_ATOM_CAPACITY * sizeof(GpuAtom)), | |
| 88 | "clear atoms"); | |
| 89 | CUDA_CHECK_VOID(cudaMemset(t->name_pool, 0, GPU_NAME_POOL_BYTES), | |
| 90 | "clear name_pool"); | |
| 91 | CUDA_CHECK_VOID(cudaMemset(t->out_pool, 0, GPU_OUT_POOL_SLOTS * sizeof(uint32_t)), | |
| 92 | "clear out_pool"); | |
| 93 | ||
| 94 | t->atom_count = 0; | |
| 95 | t->name_pool_used = 0; | |
| 96 | t->out_pool_used = 0; | |
| 97 | } | |
| 98 | ||
| 99 | /* ================================================================ | |
| 100 | * gpu_store_node -- store type + name at slot. | |
| 101 | * ================================================================ */ | |
| 102 | extern "C" | |
| 103 | int cuda_store_node(GpuAtomTable* t, uint32_t slot, | |
| 104 | uint16_t type, const char* name, uint16_t name_len) | |
| 105 | { | |
| 106 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 107 | if (t->name_pool_used + name_len > GPU_NAME_POOL_BYTES) return -1; | |
| 108 | ||
| 109 | uint32_t off = t->name_pool_used; | |
| 110 | ||
| 111 | /* Build the atom struct on host, copy in one shot */ | |
| 112 | GpuAtom a; | |
| 113 | memset(&a, 0, sizeof(a)); | |
| 114 | a.type = type; | |
| 115 | a.is_node = 1; | |
| 116 | a.data_offset = off; | |
| 117 | a.data_len = name_len; | |
| 118 | ||
| 119 | CUDA_CHECK(cudaMemcpy(t->atoms + slot, &a, sizeof(GpuAtom), | |
| 120 | cudaMemcpyHostToDevice), "store node atom"); | |
| 121 | ||
| 122 | /* Append name to pool */ | |
| 123 | if (name_len > 0) | |
| 124 | { | |
| 125 | CUDA_CHECK(cudaMemcpy(t->name_pool + off, name, name_len, | |
| 126 | cudaMemcpyHostToDevice), "store node name"); | |
| 127 | } | |
| 128 | ||
| 129 | t->name_pool_used += name_len; | |
| 130 | if (slot >= t->atom_count) t->atom_count = slot + 1; | |
| 131 | ||
| 132 | return 0; | |
| 133 | } | |
| 134 | ||
| 135 | /* ================================================================ | |
| 136 | * gpu_store_link -- store type + outgoing set at slot. | |
| 137 | * ================================================================ */ | |
| 138 | extern "C" | |
| 139 | int cuda_store_link(GpuAtomTable* t, uint32_t slot, | |
| 140 | uint16_t type, | |
| 141 | const uint32_t* outgoing, uint16_t arity) | |
| 142 | { | |
| 143 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 144 | if (t->out_pool_used + arity > GPU_OUT_POOL_SLOTS) return -1; | |
| 145 | ||
| 146 | uint32_t off = t->out_pool_used; | |
| 147 | ||
| 148 | GpuAtom a; | |
| 149 | memset(&a, 0, sizeof(a)); | |
| 150 | a.type = type; | |
| 151 | a.is_node = 0; | |
| 152 | a.data_offset = off; | |
| 153 | a.data_len = arity; | |
| 154 | ||
| 155 | CUDA_CHECK(cudaMemcpy(t->atoms + slot, &a, sizeof(GpuAtom), | |
| 156 | cudaMemcpyHostToDevice), "store link atom"); | |
| 157 | ||
| 158 | /* Append outgoing set to pool */ | |
| 159 | if (arity > 0) | |
| 160 | { | |
| 161 | CUDA_CHECK(cudaMemcpy(t->out_pool + off, outgoing, | |
| 162 | arity * sizeof(uint32_t), | |
| 163 | cudaMemcpyHostToDevice), "store link outgoing"); | |
| 164 | } | |
| 165 | ||
| 166 | t->out_pool_used += arity; | |
| 167 | if (slot >= t->atom_count) t->atom_count = slot + 1; | |
| 168 | ||
| 169 | return 0; | |
| 170 | } | |
| 171 | ||
| 172 | /* ================================================================ | |
| 173 | * gpu_fetch_node -- read node back to host. | |
| 174 | * ================================================================ */ | |
| 175 | extern "C" | |
| 176 | int cuda_fetch_node(const GpuAtomTable* t, uint32_t slot, | |
| 177 | uint16_t* type, char* name_buf, uint16_t* name_len) | |
| 178 | { | |
| 179 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 180 | ||
| 181 | GpuAtom a; | |
| 182 | CUDA_CHECK(cudaMemcpy(&a, t->atoms + slot, sizeof(GpuAtom), | |
| 183 | cudaMemcpyDeviceToHost), "fetch node atom"); | |
| 184 | ||
| 185 | *type = a.type; | |
| 186 | *name_len = a.data_len; | |
| 187 | ||
| 188 | if (a.data_len > 0 && name_buf) | |
| 189 | { | |
| 190 | CUDA_CHECK(cudaMemcpy(name_buf, t->name_pool + a.data_offset, | |
| 191 | a.data_len, | |
| 192 | cudaMemcpyDeviceToHost), "fetch node name"); | |
| 193 | } | |
| 194 | ||
| 195 | return 0; | |
| 196 | } | |
| 197 | ||
| 198 | /* ================================================================ | |
| 199 | * gpu_fetch_link -- read link back to host. | |
| 200 | * ================================================================ */ | |
| 201 | extern "C" | |
| 202 | int cuda_fetch_link(const GpuAtomTable* t, uint32_t slot, | |
| 203 | uint16_t* type, uint32_t* out_buf, uint16_t* arity) | |
| 204 | { | |
| 205 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 206 | ||
| 207 | GpuAtom a; | |
| 208 | CUDA_CHECK(cudaMemcpy(&a, t->atoms + slot, sizeof(GpuAtom), | |
| 209 | cudaMemcpyDeviceToHost), "fetch link atom"); | |
| 210 | ||
| 211 | *type = a.type; | |
| 212 | *arity = a.data_len; | |
| 213 | ||
| 214 | if (a.data_len > 0 && out_buf) | |
| 215 | { | |
| 216 | CUDA_CHECK(cudaMemcpy(out_buf, t->out_pool + a.data_offset, | |
| 217 | a.data_len * sizeof(uint32_t), | |
| 218 | cudaMemcpyDeviceToHost), "fetch link outgoing"); | |
| 219 | } | |
| 220 | ||
| 221 | return 0; | |
| 222 | } | |
| 223 | ||
| 224 | /* ================================================================ | |
| 225 | * gpu_table_barrier | |
| 226 | * ================================================================ */ | |
| 227 | extern "C" | |
| 228 | void cuda_table_barrier(GpuAtomTable* t) | |
| 229 | { | |
| 230 | (void)t; | |
| 231 | cudaDeviceSynchronize(); | |
| 232 | } |
| … | … | @@ -0,0 +1,169 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/GpuAtomTable.h | |
| 3 | * | |
| 4 | * GPU-resident atom table: holds nodes and links in GPU RAM. | |
| 5 | * | |
| 6 | * AoS layout for per-atom fixed fields (one struct per atom), | |
| 7 | * with separate pools for variable-length data (names, outgoing | |
| 8 | * sets). The graph is accessed per-atom (store one, fetch one), | |
| 9 | * so AoS is the natural layout. SoA slicing is reserved for | |
| 10 | * Values (Step 4, slice-table) where GPU kernels process one | |
| 11 | * field across many atoms in parallel. | |
| 12 | * | |
| 13 | * Step 2 of Linas' plan: hold nodes and links in GPU RAM. | |
| 14 | * No ContentHash, no flags, no Values, no incoming sets -- | |
| 15 | * just type + name (nodes) and type + outgoing (links). | |
| 16 | * | |
| 17 | * CUDA is preferred; OpenCL is fallback. Both can be compiled | |
| 18 | * in; gpu_table_alloc() tries CUDA first, then OpenCL. | |
| 19 | * | |
| 20 | * Copyright (C) 2026 OpenCog Foundation | |
| 21 | * | |
| 22 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 23 | */ | |
| 24 | ||
| 25 | #ifndef _OPENCOG_GPU_ATOM_TABLE_H | |
| 26 | #define _OPENCOG_GPU_ATOM_TABLE_H | |
| 27 | ||
| 28 | #include <stdint.h> | |
| 29 | ||
| 30 | /* ----------------------------------------------------------- | |
| 31 | * Capacity defaults (overridable at compile time with -D). | |
| 32 | * | |
| 33 | * GPU memory at defaults: | |
| 34 | * atoms 1M * 12B = 12 MB | |
| 35 | * name_pool = 64 MB | |
| 36 | * out_pool 4M * 4B = 16 MB | |
| 37 | * Total ~ 92 MB | |
| 38 | * ----------------------------------------------------------- */ | |
| 39 | ||
| 40 | #ifndef GPU_ATOM_CAPACITY | |
| 41 | #define GPU_ATOM_CAPACITY 1048576 /* 1M atoms */ | |
| 42 | #endif | |
| 43 | ||
| 44 | #ifndef GPU_NAME_POOL_BYTES | |
| 45 | #define GPU_NAME_POOL_BYTES 67108864 /* 64 MB for packed names */ | |
| 46 | #endif | |
| 47 | ||
| 48 | #ifndef GPU_OUT_POOL_SLOTS | |
| 49 | #define GPU_OUT_POOL_SLOTS 4194304 /* 4M outgoing-set entries */ | |
| 50 | #endif | |
| 51 | ||
| 52 | /* ----------------------------------------------------------- | |
| 53 | * GpuBackendType -- which GPU backend is active for a table. | |
| 54 | * ----------------------------------------------------------- */ | |
| 55 | typedef enum GpuBackendType | |
| 56 | { | |
| 57 | GPU_BACKEND_NONE = 0, | |
| 58 | GPU_BACKEND_CUDA = 1, | |
| 59 | GPU_BACKEND_OPENCL = 2 | |
| 60 | } GpuBackendType; | |
| 61 | ||
| 62 | /* ----------------------------------------------------------- | |
| 63 | * GpuAtom -- fixed-size per-atom record (12 bytes). | |
| 64 | * One cudaMemcpy to store/fetch all fields of one atom. | |
| 65 | * ----------------------------------------------------------- */ | |
| 66 | typedef struct GpuAtom | |
| 67 | { | |
| 68 | uint16_t type; /* atom type id */ | |
| 69 | uint8_t is_node; /* 1 = node, 0 = link */ | |
| 70 | uint8_t _pad; /* alignment padding */ | |
| 71 | uint32_t data_offset; /* byte offset into name_pool (node) | |
| 72 | or slot index into out_pool (link) */ | |
| 73 | uint16_t data_len; /* name length in bytes (node) | |
| 74 | or arity (link) */ | |
| 75 | uint16_t _pad2; /* pad to 12 bytes */ | |
| 76 | } GpuAtom; | |
| 77 | ||
| 78 | /* ----------------------------------------------------------- | |
| 79 | * GpuAtomTable -- device pointers to atom array + pools. | |
| 80 | * The struct itself lives on the host. | |
| 81 | * ----------------------------------------------------------- */ | |
| 82 | typedef struct GpuAtomTable | |
| 83 | { | |
| 84 | GpuBackendType backend; /* which GPU backend owns this table */ | |
| 85 | ||
| 86 | /* AoS: one GpuAtom per slot */ | |
| 87 | GpuAtom* atoms; /* [CAPACITY] device array */ | |
| 88 | ||
| 89 | /* Variable-length pools (shared across all atoms) */ | |
| 90 | char* name_pool; /* [NAME_POOL_BYTES] packed node names */ | |
| 91 | uint32_t* out_pool; /* [OUT_POOL_SLOTS] packed outgoing sets */ | |
| 92 | ||
| 93 | /* Host-side bookkeeping (NOT on device) */ | |
| 94 | uint32_t atom_count; /* next free slot index */ | |
| 95 | uint32_t name_pool_used; /* bytes used in name_pool */ | |
| 96 | uint32_t out_pool_used; /* slots used in out_pool */ | |
| 97 | } GpuAtomTable; | |
| 98 | ||
| 99 | /* ----------------------------------------------------------- | |
| 100 | * Public C-linkage API -- dispatches to CUDA or OpenCL backend. | |
| 101 | * ----------------------------------------------------------- */ | |
| 102 | #ifdef __cplusplus | |
| 103 | extern "C" { | |
| 104 | #endif | |
| 105 | ||
| 106 | /* Lifecycle */ | |
| 107 | int gpu_table_alloc(GpuAtomTable* t); | |
| 108 | void gpu_table_free(GpuAtomTable* t); | |
| 109 | void gpu_table_clear(GpuAtomTable* t); | |
| 110 | ||
| 111 | /* Store (host -> device). Slot assignment is the caller's job. */ | |
| 112 | int gpu_store_node(GpuAtomTable* t, uint32_t slot, | |
| 113 | uint16_t type, const char* name, uint16_t name_len); | |
| 114 | ||
| 115 | int gpu_store_link(GpuAtomTable* t, uint32_t slot, | |
| 116 | uint16_t type, | |
| 117 | const uint32_t* outgoing, uint16_t arity); | |
| 118 | ||
| 119 | /* Fetch (device -> host). Caller provides buffers. */ | |
| 120 | int gpu_fetch_node(const GpuAtomTable* t, uint32_t slot, | |
| 121 | uint16_t* type, char* name_buf, uint16_t* name_len); | |
| 122 | ||
| 123 | int gpu_fetch_link(const GpuAtomTable* t, uint32_t slot, | |
| 124 | uint16_t* type, uint32_t* out_buf, uint16_t* arity); | |
| 125 | ||
| 126 | /* Synchronization */ | |
| 127 | void gpu_table_barrier(GpuAtomTable* t); | |
| 128 | ||
| 129 | /* ----------------------------------------------------------- | |
| 130 | * Backend-specific functions (internal, called by dispatch). | |
| 131 | * ----------------------------------------------------------- */ | |
| 132 | ||
| 133 | #ifdef HAVE_CUDA | |
| 134 | int cuda_table_alloc(GpuAtomTable* t); | |
| 135 | void cuda_table_free(GpuAtomTable* t); | |
| 136 | void cuda_table_clear(GpuAtomTable* t); | |
| 137 | int cuda_store_node(GpuAtomTable* t, uint32_t slot, | |
| 138 | uint16_t type, const char* name, uint16_t name_len); | |
| 139 | int cuda_store_link(GpuAtomTable* t, uint32_t slot, | |
| 140 | uint16_t type, | |
| 141 | const uint32_t* outgoing, uint16_t arity); | |
| 142 | int cuda_fetch_node(const GpuAtomTable* t, uint32_t slot, | |
| 143 | uint16_t* type, char* name_buf, uint16_t* name_len); | |
| 144 | int cuda_fetch_link(const GpuAtomTable* t, uint32_t slot, | |
| 145 | uint16_t* type, uint32_t* out_buf, uint16_t* arity); | |
| 146 | void cuda_table_barrier(GpuAtomTable* t); | |
| 147 | #endif | |
| 148 | ||
| 149 | #ifdef HAVE_OPENCL | |
| 150 | int ocl_table_alloc(GpuAtomTable* t); | |
| 151 | void ocl_table_free(GpuAtomTable* t); | |
| 152 | void ocl_table_clear(GpuAtomTable* t); | |
| 153 | int ocl_store_node(GpuAtomTable* t, uint32_t slot, | |
| 154 | uint16_t type, const char* name, uint16_t name_len); | |
| 155 | int ocl_store_link(GpuAtomTable* t, uint32_t slot, | |
| 156 | uint16_t type, | |
| 157 | const uint32_t* outgoing, uint16_t arity); | |
| 158 | int ocl_fetch_node(const GpuAtomTable* t, uint32_t slot, | |
| 159 | uint16_t* type, char* name_buf, uint16_t* name_len); | |
| 160 | int ocl_fetch_link(const GpuAtomTable* t, uint32_t slot, | |
| 161 | uint16_t* type, uint32_t* out_buf, uint16_t* arity); | |
| 162 | void ocl_table_barrier(GpuAtomTable* t); | |
| 163 | #endif | |
| 164 | ||
| 165 | #ifdef __cplusplus | |
| 166 | } | |
| 167 | #endif | |
| 168 | ||
| 169 | #endif /* _OPENCOG_GPU_ATOM_TABLE_H */ |
| … | … | @@ -0,0 +1,188 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/GpuAtomTable_dispatch.cc | |
| 3 | * | |
| 4 | * Runtime dispatch for GpuAtomTable: tries CUDA first, falls | |
| 5 | * back to OpenCL. All public gpu_* functions route through here. | |
| 6 | * | |
| 7 | * Copyright (C) 2026 OpenCog Foundation | |
| 8 | * | |
| 9 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 10 | */ | |
| 11 | ||
| 12 | #include "GpuAtomTable.h" | |
| 13 | ||
| 14 | #include <cstdio> | |
| 15 | #include <cstring> | |
| 16 | ||
| 17 | /* ================================================================ | |
| 18 | * gpu_table_alloc -- try CUDA, fall back to OpenCL. | |
| 19 | * ================================================================ */ | |
| 20 | extern "C" | |
| 21 | int gpu_table_alloc(GpuAtomTable* t) | |
| 22 | { | |
| 23 | memset(t, 0, sizeof(GpuAtomTable)); | |
| 24 | t->backend = GPU_BACKEND_NONE; | |
| 25 | ||
| 26 | #ifdef HAVE_CUDA | |
| 27 | if (cuda_table_alloc(t) == 0) | |
| 28 | { | |
| 29 | t->backend = GPU_BACKEND_CUDA; | |
| 30 | fprintf(stderr, "GpuAtomTable: using CUDA backend\n"); | |
| 31 | return 0; | |
| 32 | } | |
| 33 | fprintf(stderr, "GpuAtomTable: CUDA failed, trying OpenCL...\n"); | |
| 34 | #endif | |
| 35 | ||
| 36 | #ifdef HAVE_OPENCL | |
| 37 | if (ocl_table_alloc(t) == 0) | |
| 38 | { | |
| 39 | t->backend = GPU_BACKEND_OPENCL; | |
| 40 | fprintf(stderr, "GpuAtomTable: using OpenCL backend\n"); | |
| 41 | return 0; | |
| 42 | } | |
| 43 | fprintf(stderr, "GpuAtomTable: OpenCL failed\n"); | |
| 44 | #endif | |
| 45 | ||
| 46 | fprintf(stderr, "GpuAtomTable: no GPU backend available\n"); | |
| 47 | return -1; | |
| 48 | } | |
| 49 | ||
| 50 | /* ================================================================ | |
| 51 | * gpu_table_free | |
| 52 | * ================================================================ */ | |
| 53 | extern "C" | |
| 54 | void gpu_table_free(GpuAtomTable* t) | |
| 55 | { | |
| 56 | switch (t->backend) | |
| 57 | { | |
| 58 | #ifdef HAVE_CUDA | |
| 59 | case GPU_BACKEND_CUDA: cuda_table_free(t); break; | |
| 60 | #endif | |
| 61 | #ifdef HAVE_OPENCL | |
| 62 | case GPU_BACKEND_OPENCL: ocl_table_free(t); break; | |
| 63 | #endif | |
| 64 | default: break; | |
| 65 | } | |
| 66 | t->backend = GPU_BACKEND_NONE; | |
| 67 | } | |
| 68 | ||
| 69 | /* ================================================================ | |
| 70 | * gpu_table_clear | |
| 71 | * ================================================================ */ | |
| 72 | extern "C" | |
| 73 | void gpu_table_clear(GpuAtomTable* t) | |
| 74 | { | |
| 75 | switch (t->backend) | |
| 76 | { | |
| 77 | #ifdef HAVE_CUDA | |
| 78 | case GPU_BACKEND_CUDA: cuda_table_clear(t); break; | |
| 79 | #endif | |
| 80 | #ifdef HAVE_OPENCL | |
| 81 | case GPU_BACKEND_OPENCL: ocl_table_clear(t); break; | |
| 82 | #endif | |
| 83 | default: break; | |
| 84 | } | |
| 85 | } | |
| 86 | ||
| 87 | /* ================================================================ | |
| 88 | * gpu_store_node | |
| 89 | * ================================================================ */ | |
| 90 | extern "C" | |
| 91 | int gpu_store_node(GpuAtomTable* t, uint32_t slot, | |
| 92 | uint16_t type, const char* name, uint16_t name_len) | |
| 93 | { | |
| 94 | switch (t->backend) | |
| 95 | { | |
| 96 | #ifdef HAVE_CUDA | |
| 97 | case GPU_BACKEND_CUDA: | |
| 98 | return cuda_store_node(t, slot, type, name, name_len); | |
| 99 | #endif | |
| 100 | #ifdef HAVE_OPENCL | |
| 101 | case GPU_BACKEND_OPENCL: | |
| 102 | return ocl_store_node(t, slot, type, name, name_len); | |
| 103 | #endif | |
| 104 | default: return -1; | |
| 105 | } | |
| 106 | } | |
| 107 | ||
| 108 | /* ================================================================ | |
| 109 | * gpu_store_link | |
| 110 | * ================================================================ */ | |
| 111 | extern "C" | |
| 112 | int gpu_store_link(GpuAtomTable* t, uint32_t slot, | |
| 113 | uint16_t type, | |
| 114 | const uint32_t* outgoing, uint16_t arity) | |
| 115 | { | |
| 116 | switch (t->backend) | |
| 117 | { | |
| 118 | #ifdef HAVE_CUDA | |
| 119 | case GPU_BACKEND_CUDA: | |
| 120 | return cuda_store_link(t, slot, type, outgoing, arity); | |
| 121 | #endif | |
| 122 | #ifdef HAVE_OPENCL | |
| 123 | case GPU_BACKEND_OPENCL: | |
| 124 | return ocl_store_link(t, slot, type, outgoing, arity); | |
| 125 | #endif | |
| 126 | default: return -1; | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | /* ================================================================ | |
| 131 | * gpu_fetch_node | |
| 132 | * ================================================================ */ | |
| 133 | extern "C" | |
| 134 | int gpu_fetch_node(const GpuAtomTable* t, uint32_t slot, | |
| 135 | uint16_t* type, char* name_buf, uint16_t* name_len) | |
| 136 | { | |
| 137 | switch (t->backend) | |
| 138 | { | |
| 139 | #ifdef HAVE_CUDA | |
| 140 | case GPU_BACKEND_CUDA: | |
| 141 | return cuda_fetch_node(t, slot, type, name_buf, name_len); | |
| 142 | #endif | |
| 143 | #ifdef HAVE_OPENCL | |
| 144 | case GPU_BACKEND_OPENCL: | |
| 145 | return ocl_fetch_node(t, slot, type, name_buf, name_len); | |
| 146 | #endif | |
| 147 | default: return -1; | |
| 148 | } | |
| 149 | } | |
| 150 | ||
| 151 | /* ================================================================ | |
| 152 | * gpu_fetch_link | |
| 153 | * ================================================================ */ | |
| 154 | extern "C" | |
| 155 | int gpu_fetch_link(const GpuAtomTable* t, uint32_t slot, | |
| 156 | uint16_t* type, uint32_t* out_buf, uint16_t* arity) | |
| 157 | { | |
| 158 | switch (t->backend) | |
| 159 | { | |
| 160 | #ifdef HAVE_CUDA | |
| 161 | case GPU_BACKEND_CUDA: | |
| 162 | return cuda_fetch_link(t, slot, type, out_buf, arity); | |
| 163 | #endif | |
| 164 | #ifdef HAVE_OPENCL | |
| 165 | case GPU_BACKEND_OPENCL: | |
| 166 | return ocl_fetch_link(t, slot, type, out_buf, arity); | |
| 167 | #endif | |
| 168 | default: return -1; | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | /* ================================================================ | |
| 173 | * gpu_table_barrier | |
| 174 | * ================================================================ */ | |
| 175 | extern "C" | |
| 176 | void gpu_table_barrier(GpuAtomTable* t) | |
| 177 | { | |
| 178 | switch (t->backend) | |
| 179 | { | |
| 180 | #ifdef HAVE_CUDA | |
| 181 | case GPU_BACKEND_CUDA: cuda_table_barrier(t); break; | |
| 182 | #endif | |
| 183 | #ifdef HAVE_OPENCL | |
| 184 | case GPU_BACKEND_OPENCL: ocl_table_barrier(t); break; | |
| 185 | #endif | |
| 186 | default: break; | |
| 187 | } | |
| 188 | } |
| … | … | @@ -0,0 +1,343 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/GpuAtomTable_ocl.cc | |
| 3 | * | |
| 4 | * OpenCL implementation of the GpuAtomTable API. | |
| 5 | * | |
| 6 | * AoS for per-atom fixed fields: one GpuAtom struct per slot, | |
| 7 | * one enqueueWriteBuffer/enqueueReadBuffer per atom. | |
| 8 | * Variable-length data (names, outgoing sets) in side pools. | |
| 9 | * | |
| 10 | * Copyright (C) 2026 OpenCog Foundation | |
| 11 | * | |
| 12 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 13 | */ | |
| 14 | ||
| 15 | #ifdef HAVE_OPENCL | |
| 16 | ||
| 17 | #include "GpuAtomTable.h" | |
| 18 | #include "opencl-headers.h" | |
| 19 | ||
| 20 | #include <cstdio> | |
| 21 | #include <cstring> | |
| 22 | #include <vector> | |
| 23 | ||
| 24 | /* ---------------------------------------------------------------- | |
| 25 | * Internal state: OpenCL handles stored alongside the table. | |
| 26 | * ---------------------------------------------------------------- */ | |
| 27 | struct OclAtomTableState | |
| 28 | { | |
| 29 | cl::Context context; | |
| 30 | cl::CommandQueue queue; | |
| 31 | ||
| 32 | cl::Buffer b_atoms; /* GpuAtom[CAPACITY] */ | |
| 33 | cl::Buffer b_name_pool; /* char[NAME_POOL_BYTES] */ | |
| 34 | cl::Buffer b_out_pool; /* uint32_t[OUT_POOL_SLOTS] */ | |
| 35 | }; | |
| 36 | ||
| 37 | static GpuAtomTable* s_active_table = nullptr; | |
| 38 | static OclAtomTableState* s_state = nullptr; | |
| 39 | ||
| 40 | static OclAtomTableState* get_state(const GpuAtomTable* t) | |
| 41 | { | |
| 42 | if (s_active_table == t) return s_state; | |
| 43 | return nullptr; | |
| 44 | } | |
| 45 | ||
| 46 | static bool pick_device(cl::Platform& plat, cl::Device& dev) | |
| 47 | { | |
| 48 | std::vector<cl::Platform> platforms; | |
| 49 | cl::Platform::get(&platforms); | |
| 50 | if (platforms.empty()) return false; | |
| 51 | ||
| 52 | for (auto& p : platforms) | |
| 53 | { | |
| 54 | std::vector<cl::Device> devs; | |
| 55 | p.getDevices(CL_DEVICE_TYPE_GPU, &devs); | |
| 56 | if (!devs.empty()) { plat = p; dev = devs[0]; return true; } | |
| 57 | } | |
| 58 | for (auto& p : platforms) | |
| 59 | { | |
| 60 | std::vector<cl::Device> devs; | |
| 61 | p.getDevices(CL_DEVICE_TYPE_ALL, &devs); | |
| 62 | if (!devs.empty()) { plat = p; dev = devs[0]; return true; } | |
| 63 | } | |
| 64 | return false; | |
| 65 | } | |
| 66 | ||
| 67 | /* ================================================================ | |
| 68 | * gpu_table_alloc | |
| 69 | * ================================================================ */ | |
| 70 | extern "C" | |
| 71 | int ocl_table_alloc(GpuAtomTable* t) | |
| 72 | { | |
| 73 | memset(t, 0, sizeof(GpuAtomTable)); | |
| 74 | ||
| 75 | cl::Platform plat; | |
| 76 | cl::Device dev; | |
| 77 | if (!pick_device(plat, dev)) | |
| 78 | { | |
| 79 | fprintf(stderr, "OpenCL: no device found\n"); | |
| 80 | return -1; | |
| 81 | } | |
| 82 | ||
| 83 | auto* st = new OclAtomTableState(); | |
| 84 | ||
| 85 | try { | |
| 86 | st->context = cl::Context(dev); | |
| 87 | st->queue = cl::CommandQueue(st->context, dev); | |
| 88 | ||
| 89 | cl_int err; | |
| 90 | ||
| 91 | #define MAKE_BUF(field, bytes) \ | |
| 92 | st->field = cl::Buffer(st->context, CL_MEM_READ_WRITE, (bytes), nullptr, &err); \ | |
| 93 | if (err != CL_SUCCESS) { fprintf(stderr, "OpenCL alloc " #field " failed\n"); delete st; return -1; } | |
| 94 | ||
| 95 | MAKE_BUF(b_atoms, (size_t)GPU_ATOM_CAPACITY * sizeof(GpuAtom)) | |
| 96 | MAKE_BUF(b_name_pool, GPU_NAME_POOL_BYTES) | |
| 97 | MAKE_BUF(b_out_pool, (size_t)GPU_OUT_POOL_SLOTS * sizeof(uint32_t)) | |
| 98 | ||
| 99 | #undef MAKE_BUF | |
| 100 | ||
| 101 | /* Zero all buffers */ | |
| 102 | uint8_t zero = 0; | |
| 103 | st->queue.enqueueFillBuffer(st->b_atoms, zero, 0, | |
| 104 | (size_t)GPU_ATOM_CAPACITY * sizeof(GpuAtom)); | |
| 105 | st->queue.enqueueFillBuffer(st->b_name_pool, zero, 0, GPU_NAME_POOL_BYTES); | |
| 106 | st->queue.enqueueFillBuffer(st->b_out_pool, (uint32_t)0, 0, | |
| 107 | (size_t)GPU_OUT_POOL_SLOTS * sizeof(uint32_t)); | |
| 108 | st->queue.finish(); | |
| 109 | ||
| 110 | } catch (cl::Error& e) { | |
| 111 | fprintf(stderr, "OpenCL exception in alloc: %s (%d)\n", | |
| 112 | e.what(), e.err()); | |
| 113 | delete st; | |
| 114 | return -1; | |
| 115 | } | |
| 116 | ||
| 117 | /* Opaque non-null markers */ | |
| 118 | t->atoms = (GpuAtom*)(uintptr_t)1; | |
| 119 | t->name_pool = (char*)(uintptr_t)1; | |
| 120 | t->out_pool = (uint32_t*)(uintptr_t)1; | |
| 121 | ||
| 122 | t->atom_count = 0; | |
| 123 | t->name_pool_used = 0; | |
| 124 | t->out_pool_used = 0; | |
| 125 | ||
| 126 | s_active_table = t; | |
| 127 | s_state = st; | |
| 128 | ||
| 129 | return 0; | |
| 130 | } | |
| 131 | ||
| 132 | /* ================================================================ | |
| 133 | * gpu_table_free | |
| 134 | * ================================================================ */ | |
| 135 | extern "C" | |
| 136 | void ocl_table_free(GpuAtomTable* t) | |
| 137 | { | |
| 138 | if (s_active_table == t && s_state) | |
| 139 | { | |
| 140 | delete s_state; | |
| 141 | s_state = nullptr; | |
| 142 | s_active_table = nullptr; | |
| 143 | } | |
| 144 | memset(t, 0, sizeof(GpuAtomTable)); | |
| 145 | } | |
| 146 | ||
| 147 | /* ================================================================ | |
| 148 | * gpu_table_clear | |
| 149 | * ================================================================ */ | |
| 150 | extern "C" | |
| 151 | void ocl_table_clear(GpuAtomTable* t) | |
| 152 | { | |
| 153 | auto* st = get_state(t); | |
| 154 | if (!st) return; | |
| 155 | ||
| 156 | try { | |
| 157 | uint8_t zero = 0; | |
| 158 | st->queue.enqueueFillBuffer(st->b_atoms, zero, 0, | |
| 159 | (size_t)GPU_ATOM_CAPACITY * sizeof(GpuAtom)); | |
| 160 | st->queue.enqueueFillBuffer(st->b_name_pool, zero, 0, GPU_NAME_POOL_BYTES); | |
| 161 | st->queue.enqueueFillBuffer(st->b_out_pool, (uint32_t)0, 0, | |
| 162 | (size_t)GPU_OUT_POOL_SLOTS * sizeof(uint32_t)); | |
| 163 | st->queue.finish(); | |
| 164 | } catch (cl::Error& e) { | |
| 165 | fprintf(stderr, "OpenCL exception in clear: %s (%d)\n", | |
| 166 | e.what(), e.err()); | |
| 167 | } | |
| 168 | ||
| 169 | t->atom_count = 0; | |
| 170 | t->name_pool_used = 0; | |
| 171 | t->out_pool_used = 0; | |
| 172 | } | |
| 173 | ||
| 174 | /* ================================================================ | |
| 175 | * gpu_store_node | |
| 176 | * ================================================================ */ | |
| 177 | extern "C" | |
| 178 | int ocl_store_node(GpuAtomTable* t, uint32_t slot, | |
| 179 | uint16_t type, const char* name, uint16_t name_len) | |
| 180 | { | |
| 181 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 182 | if (t->name_pool_used + name_len > GPU_NAME_POOL_BYTES) return -1; | |
| 183 | ||
| 184 | auto* st = get_state(t); | |
| 185 | if (!st) return -1; | |
| 186 | ||
| 187 | uint32_t off = t->name_pool_used; | |
| 188 | ||
| 189 | GpuAtom a; | |
| 190 | memset(&a, 0, sizeof(a)); | |
| 191 | a.type = type; | |
| 192 | a.is_node = 1; | |
| 193 | a.data_offset = off; | |
| 194 | a.data_len = name_len; | |
| 195 | ||
| 196 | try { | |
| 197 | st->queue.enqueueWriteBuffer(st->b_atoms, CL_FALSE, | |
| 198 | slot * sizeof(GpuAtom), sizeof(GpuAtom), &a); | |
| 199 | ||
| 200 | if (name_len > 0) | |
| 201 | st->queue.enqueueWriteBuffer(st->b_name_pool, CL_FALSE, | |
| 202 | off, name_len, name); | |
| 203 | ||
| 204 | st->queue.finish(); | |
| 205 | } catch (cl::Error& e) { | |
| 206 | fprintf(stderr, "OpenCL exception in store_node: %s (%d)\n", | |
| 207 | e.what(), e.err()); | |
| 208 | return -1; | |
| 209 | } | |
| 210 | ||
| 211 | t->name_pool_used += name_len; | |
| 212 | if (slot >= t->atom_count) t->atom_count = slot + 1; | |
| 213 | return 0; | |
| 214 | } | |
| 215 | ||
| 216 | /* ================================================================ | |
| 217 | * gpu_store_link | |
| 218 | * ================================================================ */ | |
| 219 | extern "C" | |
| 220 | int ocl_store_link(GpuAtomTable* t, uint32_t slot, | |
| 221 | uint16_t type, | |
| 222 | const uint32_t* outgoing, uint16_t arity) | |
| 223 | { | |
| 224 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 225 | if (t->out_pool_used + arity > GPU_OUT_POOL_SLOTS) return -1; | |
| 226 | ||
| 227 | auto* st = get_state(t); | |
| 228 | if (!st) return -1; | |
| 229 | ||
| 230 | uint32_t off = t->out_pool_used; | |
| 231 | ||
| 232 | GpuAtom a; | |
| 233 | memset(&a, 0, sizeof(a)); | |
| 234 | a.type = type; | |
| 235 | a.is_node = 0; | |
| 236 | a.data_offset = off; | |
| 237 | a.data_len = arity; | |
| 238 | ||
| 239 | try { | |
| 240 | st->queue.enqueueWriteBuffer(st->b_atoms, CL_FALSE, | |
| 241 | slot * sizeof(GpuAtom), sizeof(GpuAtom), &a); | |
| 242 | ||
| 243 | if (arity > 0) | |
| 244 | st->queue.enqueueWriteBuffer(st->b_out_pool, CL_FALSE, | |
| 245 | off * sizeof(uint32_t), arity * sizeof(uint32_t), outgoing); | |
| 246 | ||
| 247 | st->queue.finish(); | |
| 248 | } catch (cl::Error& e) { | |
| 249 | fprintf(stderr, "OpenCL exception in store_link: %s (%d)\n", | |
| 250 | e.what(), e.err()); | |
| 251 | return -1; | |
| 252 | } | |
| 253 | ||
| 254 | t->out_pool_used += arity; | |
| 255 | if (slot >= t->atom_count) t->atom_count = slot + 1; | |
| 256 | return 0; | |
| 257 | } | |
| 258 | ||
| 259 | /* ================================================================ | |
| 260 | * gpu_fetch_node | |
| 261 | * ================================================================ */ | |
| 262 | extern "C" | |
| 263 | int ocl_fetch_node(const GpuAtomTable* t, uint32_t slot, | |
| 264 | uint16_t* type, char* name_buf, uint16_t* name_len) | |
| 265 | { | |
| 266 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 267 | ||
| 268 | auto* st = get_state(t); | |
| 269 | if (!st) return -1; | |
| 270 | ||
| 271 | try { | |
| 272 | GpuAtom a; | |
| 273 | st->queue.enqueueReadBuffer(st->b_atoms, CL_TRUE, | |
| 274 | slot * sizeof(GpuAtom), sizeof(GpuAtom), &a); | |
| 275 | ||
| 276 | *type = a.type; | |
| 277 | *name_len = a.data_len; | |
| 278 | ||
| 279 | if (a.data_len > 0 && name_buf) | |
| 280 | { | |
| 281 | st->queue.enqueueReadBuffer(st->b_name_pool, CL_TRUE, | |
| 282 | a.data_offset, a.data_len, name_buf); | |
| 283 | } | |
| 284 | } catch (cl::Error& e) { | |
| 285 | fprintf(stderr, "OpenCL exception in fetch_node: %s (%d)\n", | |
| 286 | e.what(), e.err()); | |
| 287 | return -1; | |
| 288 | } | |
| 289 | return 0; | |
| 290 | } | |
| 291 | ||
| 292 | /* ================================================================ | |
| 293 | * gpu_fetch_link | |
| 294 | * ================================================================ */ | |
| 295 | extern "C" | |
| 296 | int ocl_fetch_link(const GpuAtomTable* t, uint32_t slot, | |
| 297 | uint16_t* type, uint32_t* out_buf, uint16_t* arity) | |
| 298 | { | |
| 299 | if (slot >= GPU_ATOM_CAPACITY) return -1; | |
| 300 | ||
| 301 | auto* st = get_state(t); | |
| 302 | if (!st) return -1; | |
| 303 | ||
| 304 | try { | |
| 305 | GpuAtom a; | |
| 306 | st->queue.enqueueReadBuffer(st->b_atoms, CL_TRUE, | |
| 307 | slot * sizeof(GpuAtom), sizeof(GpuAtom), &a); | |
| 308 | ||
| 309 | *type = a.type; | |
| 310 | *arity = a.data_len; | |
| 311 | ||
| 312 | if (a.data_len > 0 && out_buf) | |
| 313 | { | |
| 314 | st->queue.enqueueReadBuffer(st->b_out_pool, CL_TRUE, | |
| 315 | a.data_offset * sizeof(uint32_t), | |
| 316 | a.data_len * sizeof(uint32_t), out_buf); | |
| 317 | } | |
| 318 | } catch (cl::Error& e) { | |
| 319 | fprintf(stderr, "OpenCL exception in fetch_link: %s (%d)\n", | |
| 320 | e.what(), e.err()); | |
| 321 | return -1; | |
| 322 | } | |
| 323 | return 0; | |
| 324 | } | |
| 325 | ||
| 326 | /* ================================================================ | |
| 327 | * gpu_table_barrier | |
| 328 | * ================================================================ */ | |
| 329 | extern "C" | |
| 330 | void ocl_table_barrier(GpuAtomTable* t) | |
| 331 | { | |
| 332 | auto* st = get_state(t); | |
| 333 | if (st) | |
| 334 | { | |
| 335 | try { st->queue.finish(); } | |
| 336 | catch (cl::Error& e) { | |
| 337 | fprintf(stderr, "OpenCL exception in barrier: %s (%d)\n", | |
| 338 | e.what(), e.err()); | |
| 339 | } | |
| 340 | } | |
| 341 | } | |
| 342 | ||
| 343 | #endif /* HAVE_OPENCL */ |
| … | … | @@ -1,83 +0,0 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/GpuBackend.h | |
| 3 | * | |
| 4 | * Abstract interface for GPU compute backends (CUDA, OpenCL). | |
| 5 | * GpuStorageNode delegates all GPU operations to a concrete | |
| 6 | * GpuBackend implementation, selected at open() time based on | |
| 7 | * hardware availability and URI hints. | |
| 8 | * | |
| 9 | * Priority: CUDA first (more performant on NVIDIA), OpenCL | |
| 10 | * as fallback (works on AMD/Intel/NVIDIA). | |
| 11 | * | |
| 12 | * Copyright (C) 2025 OpenCog Foundation | |
| 13 | * | |
| 14 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 15 | */ | |
| 16 | ||
| 17 | #ifndef _OPENCOG_GPU_BACKEND_H | |
| 18 | #define _OPENCOG_GPU_BACKEND_H | |
| 19 | ||
| 20 | #include <cstdint> | |
| 21 | #include <string> | |
| 22 | ||
| 23 | namespace opencog | |
| 24 | { | |
| 25 | ||
| 26 | class GpuBackend | |
| 27 | { | |
| 28 | public: | |
| 29 | virtual ~GpuBackend() = default; | |
| 30 | ||
| 31 | // -- Lifecycle -- | |
| 32 | virtual void init(const std::string& platform_hint, | |
| 33 | const std::string& device_hint) = 0; | |
| 34 | virtual void shutdown() = 0; | |
| 35 | virtual std::string device_info() = 0; | |
| 36 | virtual std::string backend_name() = 0; // "CUDA" or "OpenCL" | |
| 37 | ||
| 38 | // -- Pool allocation and initialization -- | |
| 39 | virtual void alloc_pools() = 0; | |
| 40 | virtual void init_pools() = 0; | |
| 41 | ||
| 42 | // -- Word pool operations -- | |
| 43 | virtual uint32_t word_find_or_create(uint64_t name_hash) = 0; | |
| 44 | virtual uint32_t word_lookup(uint64_t name_hash) = 0; | |
| 45 | virtual void word_write_count(uint32_t idx, double count) = 0; | |
| 46 | virtual void word_write_marginal(uint32_t idx, double marginal) = 0; | |
| 47 | virtual void word_read_values(uint32_t idx, | |
| 48 | double& count, double& marginal) = 0; | |
| 49 | virtual void word_delete(uint64_t name_hash) = 0; | |
| 50 | virtual uint32_t word_pool_count() = 0; | |
| 51 | virtual void word_read_bulk(uint32_t n, uint64_t* hashes, | |
| 52 | double* counts, double* marginals) = 0; | |
| 53 | ||
| 54 | // -- Pair pool operations -- | |
| 55 | virtual uint32_t pair_find_or_create(uint32_t word_a, | |
| 56 | uint32_t word_b) = 0; | |
| 57 | virtual uint32_t pair_lookup(uint32_t word_a, uint32_t word_b) = 0; | |
| 58 | virtual void pair_write_count(uint32_t idx, double count) = 0; | |
| 59 | virtual void pair_write_mi(uint32_t idx, double mi) = 0; | |
| 60 | virtual void pair_read_values(uint32_t idx, | |
| 61 | double& count, double& mi) = 0; | |
| 62 | virtual uint32_t pair_pool_count() = 0; | |
| 63 | ||
| 64 | // -- Section pool operations -- | |
| 65 | virtual uint32_t section_pool_count() = 0; | |
| 66 | ||
| 67 | // -- Synchronization -- | |
| 68 | virtual void barrier() = 0; | |
| 69 | }; | |
| 70 | ||
| 71 | // Backend factory functions (defined in respective .cc/.cu files). | |
| 72 | // Availability depends on compile-time HAVE_CUDA / HAVE_OPENCL. | |
| 73 | #ifdef HAVE_CUDA | |
| 74 | GpuBackend* create_cuda_backend(); | |
| 75 | #endif | |
| 76 | ||
| 77 | #ifdef HAVE_OPENCL | |
| 78 | GpuBackend* create_opencl_backend(); | |
| 79 | #endif | |
| 80 | ||
| 81 | } // namespace opencog | |
| 82 | ||
| 83 | #endif // _OPENCOG_GPU_BACKEND_H |
| … | … | @@ -1,646 +1,285 @@ |
| 1 | 1 | /* |
| 2 | 2 | * opencog/persist/gpu/GpuStorageNode.cc |
| 3 | 3 | * |
| 4 | * StorageNode backed by GPU SoA pools. | |
| 5 | * Delegates all GPU operations to a GpuBackend (CUDA or OpenCL). | |
| 4 | * BackingStore wrapper for GpuAtomTable. Bridges the AtomSpace | |
| 5 | * persistence API to the GPU atom table. | |
| 6 | 6 | * |
| 7 | * Phase 1: Store/Fetch round-trip for atoms and values. | |
| 8 | * | |
| 9 | * Copyright (C) 2025 OpenCog Foundation | |
| 7 | * Step 3 of Linas' plan: storeAtom/getAtom via BackingStore.h. | |
| 10 | 8 | * |
| 9 | * Copyright (C) 2026 OpenCog Foundation | |
| 11 | 10 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 12 | 11 | */ |
| 13 | 12 | |
| 14 | #include <sstream> | |
| 15 | #include <functional> | |
| 16 | #include <cstring> | |
| 17 | ||
| 18 | #include <opencog/util/exceptions.h> | |
| 19 | #include <opencog/util/Logger.h> | |
| 20 | #include <opencog/atomspace/AtomSpace.h> | |
| 21 | #include <opencog/atoms/base/Node.h> | |
| 22 | 13 | #include <opencog/atoms/base/Link.h> |
| 23 | #include <opencog/atoms/value/FloatValue.h> | |
| 24 | #include <opencog/atoms/atom_types/atom_types.h> | |
| 25 | #include <opencog/persist/gpu-types/atom_types.h> | |
| 26 | ||
| 27 | #include "GpuStorageNode.h" | |
| 14 | #include <opencog/atoms/base/Node.h> | |
| 15 | #include <opencog/util/exceptions.h> | |
| 16 | #include <opencog/persist/gpu/GpuStorageNode.h> | |
| 28 | 17 | |
| 29 | 18 | using namespace opencog; |
| 30 | 19 | |
| 31 | // ============================================================== | |
| 32 | // Constructors / Destructor | |
| 33 | ||
| 34 | GpuStorageNode::GpuStorageNode(Type t, const std::string&& uri) : | |
| 35 | StorageNode(t, std::move(uri)), | |
| 36 | _connected(false), | |
| 37 | _num_stores(0), | |
| 38 | _num_fetches(0) | |
| 20 | GpuStorageNode::GpuStorageNode(Type t, const std::string&& uri) | |
| 21 | : StorageNode(t, std::move(uri)), _is_open(false) | |
| 39 | 22 | { |
| 40 | _uri = get_name(); | |
| 41 | parse_uri(); | |
| 23 | memset(&_gpu_table, 0, sizeof(_gpu_table)); | |
| 42 | 24 | } |
| 43 | 25 | |
| 44 | GpuStorageNode::GpuStorageNode(const std::string&& uri) : | |
| 45 | StorageNode(GPU_STORAGE_NODE, std::move(uri)), | |
| 46 | _connected(false), | |
| 47 | _num_stores(0), | |
| 48 | _num_fetches(0) | |
| 26 | GpuStorageNode::GpuStorageNode(const std::string& uri) | |
| 27 | : StorageNode(GPU_STORAGE_NODE, uri), _is_open(false) | |
| 49 | 28 | { |
| 50 | _uri = get_name(); | |
| 51 | parse_uri(); | |
| 29 | memset(&_gpu_table, 0, sizeof(_gpu_table)); | |
| 52 | 30 | } |
| 53 | 31 | |
| 54 | 32 | GpuStorageNode::~GpuStorageNode() |
| 55 | 33 | { |
| 56 | if (_connected) close(); | |
| 57 | } | |
| 58 | ||
| 59 | // ============================================================== | |
| 60 | // URI parsing: "gpu://[backend:]platform:device" | |
| 61 | // | |
| 62 | // Extended URI format: | |
| 63 | // "gpu://:" → auto backend, any device | |
| 64 | // "gpu://NVIDIA:RTX" → auto backend, NVIDIA RTX | |
| 65 | // "gpu://cuda::" → force CUDA, any device | |
| 66 | // "gpu://opencl:Intel:" → force OpenCL, Intel device | |
| 67 | // "gpu://cuda:NVIDIA:RTX" → CUDA, NVIDIA RTX | |
| 68 | ||
| 69 | void GpuStorageNode::parse_uri(void) | |
| 70 | { | |
| 71 | const std::string& url = _uri; | |
| 72 | if (0 != url.compare(0, 6, "gpu://")) | |
| 73 | throw RuntimeException(TRACE_INFO, | |
| 74 | "Unsupported URL \"%s\"\n" | |
| 75 | "\tExpecting 'gpu://[backend:]platform:device'", | |
| 76 | url.c_str()); | |
| 77 | ||
| 78 | std::string rest = url.substr(6); | |
| 79 | ||
| 80 | // Check for explicit backend hint: "cuda:" or "opencl:" | |
| 81 | _backend_hint = ""; | |
| 82 | if (rest.compare(0, 5, "cuda:") == 0) | |
| 83 | { | |
| 84 | _backend_hint = "cuda"; | |
| 85 | rest = rest.substr(5); | |
| 86 | } | |
| 87 | else if (rest.compare(0, 7, "opencl:") == 0) | |
| 88 | { | |
| 89 | _backend_hint = "opencl"; | |
| 90 | rest = rest.substr(7); | |
| 91 | } | |
| 92 | ||
| 93 | // Parse platform:device from remainder | |
| 94 | size_t colon = rest.find(':'); | |
| 95 | if (std::string::npos == colon) | |
| 96 | { | |
| 97 | _splat = rest; | |
| 98 | _sdev = ""; | |
| 99 | } | |
| 100 | else | |
| 101 | { | |
| 102 | _splat = rest.substr(0, colon); | |
| 103 | _sdev = rest.substr(colon + 1); | |
| 104 | } | |
| 34 | if (_is_open) close(); | |
| 105 | 35 | } |
| 106 | 36 | |
| 107 | // ============================================================== | |
| 108 | // Connection management | |
| 37 | // ================================================================ | |
| 38 | // Lifecycle | |
| 109 | 39 | |
| 110 | 40 | void GpuStorageNode::open(void) |
| 111 | 41 | { |
| 112 | if (_connected) | |
| 113 | throw RuntimeException(TRACE_INFO, | |
| 114 | "GpuStorageNode: already open! %s\n", _uri.c_str()); | |
| 42 | if (_is_open) return; | |
| 115 | 43 | |
| 116 | // Select backend: CUDA preferred, OpenCL fallback | |
| 117 | bool want_cuda = (_backend_hint == "cuda" or _backend_hint.empty()); | |
| 118 | bool want_opencl = (_backend_hint == "opencl" or _backend_hint.empty()); | |
| 44 | int rc = gpu_table_alloc(&_gpu_table); | |
| 45 | if (rc != 0) | |
| 46 | throw IOException(TRACE_INFO, "GPU: failed to allocate atom table"); | |
| 119 | 47 | |
| 120 | GpuBackend* be = nullptr; | |
| 121 | ||
| 122 | #ifdef HAVE_CUDA | |
| 123 | if (want_cuda and nullptr == be) | |
| 124 | { | |
| 125 | try | |
| 126 | { | |
| 127 | be = create_cuda_backend(); | |
| 128 | be->init(_splat, _sdev); | |
| 129 | } | |
| 130 | catch (const RuntimeException& e) | |
| 131 | { | |
| 132 | delete be; | |
| 133 | be = nullptr; | |
| 134 | if (_backend_hint == "cuda") | |
| 135 | throw; // User explicitly asked for CUDA | |
| 136 | logger().info("GpuStorageNode: CUDA unavailable, " | |
| 137 | "trying OpenCL...\n"); | |
| 138 | } | |
| 139 | } | |
| 140 | #endif | |
| 141 | ||
| 142 | #ifdef HAVE_OPENCL | |
| 143 | if (want_opencl and nullptr == be) | |
| 144 | { | |
| 145 | try | |
| 146 | { | |
| 147 | be = create_opencl_backend(); | |
| 148 | be->init(_splat, _sdev); | |
| 149 | } | |
| 150 | catch (const RuntimeException& e) | |
| 151 | { | |
| 152 | delete be; | |
| 153 | be = nullptr; | |
| 154 | if (_backend_hint == "opencl") | |
| 155 | throw; // User explicitly asked for OpenCL | |
| 156 | } | |
| 157 | } | |
| 158 | #endif | |
| 159 | ||
| 160 | if (nullptr == be) | |
| 161 | throw RuntimeException(TRACE_INFO, | |
| 162 | "GpuStorageNode: no GPU backend available for '%s'\n" | |
| 163 | "\tCompiled with:" | |
| 164 | #ifdef HAVE_CUDA | |
| 165 | " CUDA" | |
| 166 | #endif | |
| 167 | #ifdef HAVE_OPENCL | |
| 168 | " OpenCL" | |
| 169 | #endif | |
| 170 | "\n", | |
| 171 | _uri.c_str()); | |
| 172 | ||
| 173 | _backend.reset(be); | |
| 174 | _backend->alloc_pools(); | |
| 175 | _backend->init_pools(); | |
| 176 | ||
| 177 | _connected = true; | |
| 178 | _num_stores = 0; | |
| 179 | _num_fetches = 0; | |
| 180 | ||
| 181 | logger().info("GpuStorageNode: opened %s (backend: %s, device: %s)\n", | |
| 182 | _uri.c_str(), | |
| 183 | _backend->backend_name().c_str(), | |
| 184 | _backend->device_info().c_str()); | |
| 48 | _is_open = true; | |
| 185 | 49 | } |
| 186 | 50 | |
| 187 | 51 | void GpuStorageNode::close(void) |
| 188 | 52 | { |
| 189 | if (not _connected) return; | |
| 53 | if (!_is_open) return; | |
| 190 | 54 | |
| 191 | _backend->barrier(); | |
| 192 | _backend->shutdown(); | |
| 193 | _backend.reset(); | |
| 55 | gpu_table_barrier(&_gpu_table); | |
| 56 | gpu_table_free(&_gpu_table); | |
| 194 | 57 | |
| 195 | // Clear CPU-side maps | |
| 196 | { | |
| 197 | std::lock_guard<std::mutex> lk(_name_mtx); | |
| 198 | _name_to_hash.clear(); | |
| 199 | _hash_to_name.clear(); | |
| 200 | } | |
| 201 | { | |
| 202 | std::lock_guard<std::mutex> lk(_atom_mtx); | |
| 203 | _atom_map.clear(); | |
| 204 | } | |
| 58 | _tlb.clear(); | |
| 59 | _handle_to_slot.clear(); | |
| 60 | _slot_is_node.clear(); | |
| 205 | 61 | |
| 206 | _connected = false; | |
| 207 | logger().info("GpuStorageNode: closed %s\n", _uri.c_str()); | |
| 62 | _is_open = false; | |
| 208 | 63 | } |
| 209 | 64 | |
| 210 | 65 | bool GpuStorageNode::connected(void) |
| 211 | 66 | { |
| 212 | return _connected; | |
| 67 | return _is_open; | |
| 213 | 68 | } |
| 214 | 69 | |
| 215 | void GpuStorageNode::erase(void) | |
| 70 | void GpuStorageNode::destroy(void) | |
| 216 | 71 | { |
| 217 | if (not _connected) return; | |
| 218 | ||
| 219 | _backend->init_pools(); | |
| 220 | ||
| 221 | { | |
| 222 | std::lock_guard<std::mutex> lk(_name_mtx); | |
| 223 | _name_to_hash.clear(); | |
| 224 | _hash_to_name.clear(); | |
| 225 | } | |
| 226 | { | |
| 227 | std::lock_guard<std::mutex> lk(_atom_mtx); | |
| 228 | _atom_map.clear(); | |
| 229 | } | |
| 72 | if (!_is_open) return; | |
| 73 | gpu_table_clear(&_gpu_table); | |
| 74 | _gpu_table.atom_count = 0; | |
| 75 | _gpu_table.name_pool_used = 0; | |
| 76 | _gpu_table.out_pool_used = 0; | |
| 77 | _tlb.clear(); | |
| 78 | _handle_to_slot.clear(); | |
| 79 | _slot_is_node.clear(); | |
| 230 | 80 | } |
| 231 | 81 | |
| 232 | // ============================================================== | |
| 233 | // Name hashing | |
| 234 | ||
| 235 | uint64_t GpuStorageNode::name_hash(const std::string& name) | |
| 82 | void GpuStorageNode::erase(void) | |
| 236 | 83 | { |
| 237 | std::lock_guard<std::mutex> lk(_name_mtx); | |
| 238 | ||
| 239 | auto it = _name_to_hash.find(name); | |
| 240 | if (it != _name_to_hash.end()) | |
| 241 | return it->second; | |
| 242 | ||
| 243 | // splitmix64 on std::hash | |
| 244 | std::hash<std::string> hasher; | |
| 245 | uint64_t h = hasher(name); | |
| 246 | ||
| 247 | if (h == GPU_HT_EMPTY_KEY) h = 0; | |
| 248 | ||
| 249 | _name_to_hash[name] = h; | |
| 250 | _hash_to_name[h] = name; | |
| 251 | return h; | |
| 84 | destroy(); | |
| 252 | 85 | } |
| 253 | 86 | |
| 254 | // ============================================================== | |
| 255 | // GPU pool operations (delegated to backend) | |
| 256 | ||
| 257 | uint32_t GpuStorageNode::store_word(const std::string& name) | |
| 258 | { | |
| 259 | uint64_t nhash = name_hash(name); | |
| 260 | uint32_t idx = _backend->word_find_or_create(nhash); | |
| 261 | ||
| 262 | if (idx == GPU_NOT_FOUND) | |
| 263 | throw RuntimeException(TRACE_INFO, | |
| 264 | "GpuStorageNode: word pool full, cannot store '%s'\n", | |
| 265 | name.c_str()); | |
| 266 | ||
| 267 | return idx; | |
| 268 | } | |
| 87 | // ================================================================ | |
| 88 | // Slot management | |
| 89 | // | |
| 90 | // Each atom gets a sequential GPU slot (0, 1, 2, ...). | |
| 91 | // The TLB maps slot ↔ Handle (UUID = slot). | |
| 92 | // _handle_to_slot provides fast forward lookup. | |
| 269 | 93 | |
| 270 | uint32_t GpuStorageNode::lookup_word(const std::string& name) | |
| 94 | bool GpuStorageNode::has_slot(const Handle& h) const | |
| 271 | 95 | { |
| 272 | uint64_t nhash = name_hash(name); | |
| 273 | return _backend->word_lookup(nhash); | |
| 96 | return _handle_to_slot.count(h) > 0; | |
| 274 | 97 | } |
| 275 | 98 | |
| 276 | uint32_t GpuStorageNode::store_pair(uint32_t word_a, uint32_t word_b) | |
| 99 | uint32_t GpuStorageNode::assign_slot(const Handle& h) | |
| 277 | 100 | { |
| 278 | uint32_t idx = _backend->pair_find_or_create(word_a, word_b); | |
| 101 | auto it = _handle_to_slot.find(h); | |
| 102 | if (it != _handle_to_slot.end()) | |
| 103 | return it->second; | |
| 279 | 104 | |
| 280 | if (idx == GPU_NOT_FOUND) | |
| 281 | throw RuntimeException(TRACE_INFO, | |
| 282 | "GpuStorageNode: pair pool full\n"); | |
| 105 | uint32_t slot = _gpu_table.atom_count; | |
| 106 | _gpu_table.atom_count++; | |
| 283 | 107 | |
| 284 | return idx; | |
| 285 | } | |
| 108 | _handle_to_slot[h] = slot; | |
| 109 | _tlb.addAtom(h, (UUID)slot); | |
| 110 | _slot_is_node.push_back(h->is_node()); | |
| 286 | 111 | |
| 287 | uint32_t GpuStorageNode::lookup_pair(uint32_t word_a, uint32_t word_b) | |
| 288 | { | |
| 289 | return _backend->pair_lookup(word_a, word_b); | |
| 112 | return slot; | |
| 290 | 113 | } |
| 291 | 114 | |
| 292 | // ============================================================== | |
| 293 | // Value storage helpers | |
| 115 | // ================================================================ | |
| 116 | // Store | |
| 294 | 117 | |
| 295 | void GpuStorageNode::store_node_values(const Handle& h, uint32_t pool_idx) | |
| 118 | void GpuStorageNode::do_store_atom(const Handle& h) | |
| 296 | 119 | { |
| 297 | ValuePtr tv = h->getValue(truth_key()); | |
| 298 | if (nullptr == tv) return; | |
| 120 | // Already on GPU — nothing to update in Step 3 (no Values). | |
| 121 | if (has_slot(h)) return; | |
| 299 | 122 | |
| 300 | FloatValuePtr fv = FloatValueCast(tv); | |
| 301 | if (nullptr == fv) return; | |
| 302 | ||
| 303 | const std::vector<double>& vals = fv->value(); | |
| 304 | if (vals.size() > 0) | |
| 305 | _backend->word_write_count(pool_idx, vals[0]); | |
| 306 | if (vals.size() > 1) | |
| 307 | _backend->word_write_marginal(pool_idx, vals[1]); | |
| 308 | } | |
| 309 | ||
| 310 | void GpuStorageNode::load_node_values(const Handle& h, uint32_t pool_idx) | |
| 311 | { | |
| 312 | double count = 0.0, marginal = 0.0; | |
| 313 | _backend->word_read_values(pool_idx, count, marginal); | |
| 314 | ||
| 315 | if (count != 0.0 or marginal != 0.0) | |
| 123 | // Links: recursively store outgoing atoms first. | |
| 124 | if (h->is_link()) | |
| 316 | 125 | { |
| 317 | std::vector<double> vals = {count, marginal}; | |
| 318 | h->setValue(truth_key(), createFloatValue(vals)); | |
| 126 | for (const Handle& out : h->getOutgoingSet()) | |
| 127 | do_store_atom(out); | |
| 319 | 128 | } |
| 320 | } | |
| 321 | ||
| 322 | void GpuStorageNode::store_link_values(const Handle& h, uint32_t pool_idx) | |
| 323 | { | |
| 324 | ValuePtr tv = h->getValue(truth_key()); | |
| 325 | if (nullptr == tv) return; | |
| 326 | 129 | |
| 327 | FloatValuePtr fv = FloatValueCast(tv); | |
| 328 | if (nullptr == fv) return; | |
| 130 | uint32_t slot = assign_slot(h); | |
| 131 | int rc; | |
| 329 | 132 | |
| 330 | const std::vector<double>& vals = fv->value(); | |
| 331 | if (vals.size() > 0) | |
| 332 | _backend->pair_write_count(pool_idx, vals[0]); | |
| 333 | if (vals.size() > 1) | |
| 334 | _backend->pair_write_mi(pool_idx, vals[1]); | |
| 335 | } | |
| 336 | ||
| 337 | void GpuStorageNode::load_link_values(const Handle& h, uint32_t pool_idx) | |
| 338 | { | |
| 339 | double count = 0.0, mi = 0.0; | |
| 340 | _backend->pair_read_values(pool_idx, count, mi); | |
| 341 | ||
| 342 | if (count != 0.0 or mi != 0.0) | |
| 133 | if (h->is_node()) | |
| 343 | 134 | { |
| 344 | std::vector<double> vals = {count, mi}; | |
| 345 | h->setValue(truth_key(), createFloatValue(vals)); | |
| 135 | NodePtr np = NodeCast(h); | |
| 136 | const std::string& name = np->get_name(); | |
| 137 | rc = gpu_store_node(&_gpu_table, slot, | |
| 138 | h->get_type(), name.c_str(), (uint16_t)name.size()); | |
| 346 | 139 | } |
| 347 | } | |
| 348 | ||
| 349 | // ============================================================== | |
| 350 | // StorageNode API: storeAtom | |
| 351 | ||
| 352 | void GpuStorageNode::storeAtom(const Handle& h, bool synchronous) | |
| 353 | { | |
| 354 | if (not _connected) | |
| 355 | throw RuntimeException(TRACE_INFO, | |
| 356 | "GpuStorageNode: not connected!\n"); | |
| 357 | ||
| 358 | NodePtr np = NodeCast(h); | |
| 359 | if (np) | |
| 140 | else | |
| 360 | 141 | { |
| 361 | uint32_t idx = store_word(np->get_name()); | |
| 362 | store_node_values(h, idx); | |
| 363 | ||
| 364 | std::lock_guard<std::mutex> lk(_atom_mtx); | |
| 365 | _atom_map[h] = {idx, 0}; | |
| 366 | _num_stores++; | |
| 367 | ||
| 368 | if (synchronous) _backend->barrier(); | |
| 369 | return; | |
| 142 | const HandleSeq& outgoing = h->getOutgoingSet(); | |
| 143 | std::vector<uint32_t> out_slots; | |
| 144 | out_slots.reserve(outgoing.size()); | |
| 145 | for (const Handle& out : outgoing) | |
| 146 | out_slots.push_back(_handle_to_slot.at(out)); | |
| 147 | ||
| 148 | rc = gpu_store_link(&_gpu_table, slot, | |
| 149 | h->get_type(), out_slots.data(), (uint16_t)outgoing.size()); | |
| 370 | 150 | } |
| 371 | 151 | |
| 372 | LinkPtr lp = LinkCast(h); | |
| 373 | if (lp) | |
| 374 | { | |
| 375 | for (const Handle& oh : lp->getOutgoingSet()) | |
| 376 | storeAtom(oh, false); | |
| 377 | ||
| 378 | if (lp->get_arity() == 2) | |
| 379 | { | |
| 380 | const Handle& ha = lp->getOutgoingAtom(0); | |
| 381 | const Handle& hb = lp->getOutgoingAtom(1); | |
| 382 | ||
| 383 | NodePtr na = NodeCast(ha); | |
| 384 | NodePtr nb = NodeCast(hb); | |
| 385 | if (na and nb) | |
| 386 | { | |
| 387 | uint32_t ia = store_word(na->get_name()); | |
| 388 | uint32_t ib = store_word(nb->get_name()); | |
| 389 | uint32_t pidx = store_pair(ia, ib); | |
| 390 | store_link_values(h, pidx); | |
| 391 | ||
| 392 | std::lock_guard<std::mutex> lk(_atom_mtx); | |
| 393 | _atom_map[h] = {pidx, 1}; | |
| 394 | } | |
| 395 | } | |
| 396 | ||
| 397 | _num_stores++; | |
| 398 | if (synchronous) _backend->barrier(); | |
| 399 | return; | |
| 400 | } | |
| 152 | if (rc != 0) | |
| 153 | throw IOException(TRACE_INFO, | |
| 154 | "GPU: failed to store atom at slot %u", slot); | |
| 401 | 155 | } |
| 402 | 156 | |
| 403 | // ============================================================== | |
| 404 | // StorageNode API: getAtom | |
| 405 | ||
| 406 | void GpuStorageNode::getAtom(const Handle& h) | |
| 157 | void GpuStorageNode::storeAtom(const Handle& h, bool synchronous) | |
| 407 | 158 | { |
| 408 | if (not _connected) | |
| 409 | throw RuntimeException(TRACE_INFO, | |
| 410 | "GpuStorageNode: not connected!\n"); | |
| 411 | ||
| 412 | NodePtr np = NodeCast(h); | |
| 413 | if (np) | |
| 414 | { | |
| 415 | uint32_t idx = lookup_word(np->get_name()); | |
| 416 | if (idx == GPU_NOT_FOUND) return; | |
| 417 | ||
| 418 | load_node_values(h, idx); | |
| 419 | _num_fetches++; | |
| 420 | return; | |
| 421 | } | |
| 422 | ||
| 423 | LinkPtr lp = LinkCast(h); | |
| 424 | if (lp and lp->get_arity() == 2) | |
| 425 | { | |
| 426 | const Handle& ha = lp->getOutgoingAtom(0); | |
| 427 | const Handle& hb = lp->getOutgoingAtom(1); | |
| 428 | ||
| 429 | NodePtr na = NodeCast(ha); | |
| 430 | NodePtr nb = NodeCast(hb); | |
| 431 | if (na and nb) | |
| 432 | { | |
| 433 | uint32_t ia = lookup_word(na->get_name()); | |
| 434 | uint32_t ib = lookup_word(nb->get_name()); | |
| 435 | if (ia == GPU_NOT_FOUND or ib == GPU_NOT_FOUND) return; | |
| 436 | ||
| 437 | uint32_t pidx = lookup_pair(ia, ib); | |
| 438 | if (pidx == GPU_NOT_FOUND) return; | |
| 439 | ||
| 440 | load_link_values(h, pidx); | |
| 441 | _num_fetches++; | |
| 442 | } | |
| 443 | } | |
| 159 | std::lock_guard<std::mutex> lk(_mtx); | |
| 160 | do_store_atom(h); | |
| 161 | if (synchronous) | |
| 162 | gpu_table_barrier(&_gpu_table); | |
| 444 | 163 | } |
| 445 | 164 | |
| 446 | // ============================================================== | |
| 447 | // StorageNode API: storeValue / loadValue | |
| 165 | // ================================================================ | |
| 166 | // Load | |
| 167 | // | |
| 168 | // Reconstruct atoms from GPU data. Two-pass: nodes first (no | |
| 169 | // dependencies), then links (outgoing references resolved via | |
| 170 | // the slot→Handle vector built during pass 1). | |
| 448 | 171 | |
| 449 | void GpuStorageNode::storeValue(const Handle& atom, const Handle& key) | |
| 172 | void GpuStorageNode::loadAtomSpace(AtomSpace* as) | |
| 450 | 173 | { |
| 451 | if (not _connected) | |
| 452 | throw RuntimeException(TRACE_INFO, | |
| 453 | "GpuStorageNode: not connected!\n"); | |
| 174 | if (!_is_open) return; | |
| 454 | 175 | |
| 455 | if (key != truth_key()) return; | |
| 176 | gpu_table_barrier(&_gpu_table); | |
| 456 | 177 | |
| 457 | NodePtr np = NodeCast(atom); | |
| 458 | if (np) | |
| 459 | { | |
| 460 | uint32_t idx = store_word(np->get_name()); | |
| 461 | store_node_values(atom, idx); | |
| 462 | return; | |
| 463 | } | |
| 178 | uint32_t n = _gpu_table.atom_count; | |
| 179 | std::vector<Handle> loaded(n); | |
| 464 | 180 | |
| 465 | LinkPtr lp = LinkCast(atom); | |
| 466 | if (lp and lp->get_arity() == 2) | |
| 181 | // Pass 1: nodes | |
| 182 | for (uint32_t slot = 0; slot < n; slot++) | |
| 467 | 183 | { |
| 468 | NodePtr na = NodeCast(lp->getOutgoingAtom(0)); | |
| 469 | NodePtr nb = NodeCast(lp->getOutgoingAtom(1)); | |
| 470 | if (na and nb) | |
| 471 | { | |
| 472 | uint32_t ia = store_word(na->get_name()); | |
| 473 | uint32_t ib = store_word(nb->get_name()); | |
| 474 | uint32_t pidx = store_pair(ia, ib); | |
| 475 | store_link_values(atom, pidx); | |
| 476 | } | |
| 477 | } | |
| 478 | } | |
| 184 | if (!_slot_is_node[slot]) continue; | |
| 479 | 185 | |
| 480 | void GpuStorageNode::loadValue(const Handle& atom, const Handle& key) | |
| 481 | { | |
| 482 | if (not _connected) | |
| 483 | throw RuntimeException(TRACE_INFO, | |
| 484 | "GpuStorageNode: not connected!\n"); | |
| 186 | uint16_t type; | |
| 187 | char name_buf[4096]; | |
| 188 | uint16_t name_len = sizeof(name_buf); | |
| 485 | 189 | |
| 486 | if (key != truth_key()) return; | |
| 190 | int rc = gpu_fetch_node(&_gpu_table, slot, | |
| 191 | &type, name_buf, &name_len); | |
| 192 | if (rc != 0) continue; | |
| 487 | 193 | |
| 488 | NodePtr np = NodeCast(atom); | |
| 489 | if (np) | |
| 490 | { | |
| 491 | uint32_t idx = lookup_word(np->get_name()); | |
| 492 | if (idx != GPU_NOT_FOUND) | |
| 493 | load_node_values(atom, idx); | |
| 494 | return; | |
| 194 | std::string name(name_buf, name_len); | |
| 195 | loaded[slot] = as->add_node(type, std::move(name)); | |
| 495 | 196 | } |
| 496 | 197 | |
| 497 | LinkPtr lp = LinkCast(atom); | |
| 498 | if (lp and lp->get_arity() == 2) | |
| 198 | // Pass 2: links (outgoing slots are always < current slot | |
| 199 | // because do_store_atom stores outgoing first). | |
| 200 | for (uint32_t slot = 0; slot < n; slot++) | |
| 499 | 201 | { |
| 500 | NodePtr na = NodeCast(lp->getOutgoingAtom(0)); | |
| 501 | NodePtr nb = NodeCast(lp->getOutgoingAtom(1)); | |
| 502 | if (na and nb) | |
| 503 | { | |
| 504 | uint32_t ia = lookup_word(na->get_name()); | |
| 505 | uint32_t ib = lookup_word(nb->get_name()); | |
| 506 | if (ia != GPU_NOT_FOUND and ib != GPU_NOT_FOUND) | |
| 507 | { | |
| 508 | uint32_t pidx = lookup_pair(ia, ib); | |
| 509 | if (pidx != GPU_NOT_FOUND) | |
| 510 | load_link_values(atom, pidx); | |
| 511 | } | |
| 512 | } | |
| 513 | } | |
| 514 | } | |
| 202 | if (_slot_is_node[slot]) continue; | |
| 515 | 203 | |
| 516 | // ============================================================== | |
| 517 | // StorageNode API: removeAtom | |
| 204 | uint16_t type; | |
| 205 | uint32_t out_buf[256]; | |
| 206 | uint16_t arity = sizeof(out_buf) / sizeof(out_buf[0]); | |
| 518 | 207 | |
| 519 | void GpuStorageNode::removeAtom(AtomSpace*, const Handle& h, bool recursive) | |
| 520 | { | |
| 521 | if (not _connected) | |
| 522 | throw RuntimeException(TRACE_INFO, | |
| 523 | "GpuStorageNode: not connected!\n"); | |
| 208 | int rc = gpu_fetch_link(&_gpu_table, slot, | |
| 209 | &type, out_buf, &arity); | |
| 210 | if (rc != 0) continue; | |
| 524 | 211 | |
| 525 | NodePtr np = NodeCast(h); | |
| 526 | if (np) | |
| 527 | { | |
| 528 | uint64_t nhash = name_hash(np->get_name()); | |
| 529 | _backend->word_delete(nhash); | |
| 212 | HandleSeq outgoing; | |
| 213 | outgoing.reserve(arity); | |
| 214 | for (uint16_t i = 0; i < arity; i++) | |
| 215 | outgoing.push_back(loaded[out_buf[i]]); | |
| 530 | 216 | |
| 531 | std::lock_guard<std::mutex> lk(_atom_mtx); | |
| 532 | _atom_map.erase(h); | |
| 217 | loaded[slot] = as->add_link(type, std::move(outgoing)); | |
| 533 | 218 | } |
| 534 | 219 | } |
| 535 | 220 | |
| 536 | // ============================================================== | |
| 537 | // StorageNode API: bulk operations | |
| 538 | ||
| 539 | void GpuStorageNode::fetchIncomingSet(AtomSpace*, const Handle&) | |
| 221 | void GpuStorageNode::storeAtomSpace(const AtomSpace* as) | |
| 540 | 222 | { |
| 541 | } | |
| 223 | HandleSeq all; | |
| 224 | as->get_handles_by_type(all, ATOM, true); | |
| 542 | 225 | |
| 543 | void GpuStorageNode::fetchIncomingByType(AtomSpace*, const Handle&, Type) | |
| 544 | { | |
| 226 | std::lock_guard<std::mutex> lk(_mtx); | |
| 227 | for (const Handle& h : all) | |
| 228 | do_store_atom(h); | |
| 545 | 229 | } |
| 546 | 230 | |
| 547 | void GpuStorageNode::loadType(AtomSpace*, Type) | |
| 548 | { | |
| 549 | } | |
| 231 | // ================================================================ | |
| 232 | // Fetch (Step 3: no Values stored yet) | |
| 550 | 233 | |
| 551 | void GpuStorageNode::loadAtomSpace(AtomSpace* as) | |
| 234 | void GpuStorageNode::getAtom(const Handle&) | |
| 552 | 235 | { |
| 553 | if (not _connected) | |
| 554 | throw RuntimeException(TRACE_INFO, | |
| 555 | "GpuStorageNode: not connected!\n"); | |
| 556 | ||
| 557 | uint32_t nwords = _backend->word_pool_count(); | |
| 558 | if (0 == nwords) return; | |
| 559 | ||
| 560 | std::vector<uint64_t> hashes(nwords); | |
| 561 | std::vector<double> counts(nwords); | |
| 562 | std::vector<double> marginals(nwords); | |
| 563 | _backend->word_read_bulk(nwords, hashes.data(), | |
| 564 | counts.data(), marginals.data()); | |
| 565 | ||
| 566 | std::lock_guard<std::mutex> lk(_name_mtx); | |
| 567 | for (uint32_t i = 0; i < nwords; i++) | |
| 568 | { | |
| 569 | auto it = _hash_to_name.find(hashes[i]); | |
| 570 | if (it == _hash_to_name.end()) continue; | |
| 571 | ||
| 572 | std::string name = it->second; | |
| 573 | Handle h = as->add_node(SCHEMA_NODE, std::move(name)); | |
| 574 | if (counts[i] != 0.0 or marginals[i] != 0.0) | |
| 575 | { | |
| 576 | std::vector<double> vals = {counts[i], marginals[i]}; | |
| 577 | h->setValue(truth_key(), createFloatValue(vals)); | |
| 578 | } | |
| 579 | } | |
| 236 | // Step 3: no Values on GPU. Nothing to load. | |
| 580 | 237 | } |
| 581 | 238 | |
| 582 | void GpuStorageNode::storeAtomSpace(const AtomSpace* as) | |
| 583 | { | |
| 584 | if (not _connected) | |
| 585 | throw RuntimeException(TRACE_INFO, | |
| 586 | "GpuStorageNode: not connected!\n"); | |
| 239 | void GpuStorageNode::fetchIncomingSet(AtomSpace*, const Handle&) {} | |
| 240 | void GpuStorageNode::fetchIncomingByType(AtomSpace*, const Handle&, Type) {} | |
| 241 | void GpuStorageNode::removeAtom(AtomSpace*, const Handle&, bool) {} | |
| 242 | void GpuStorageNode::loadType(AtomSpace*, Type) {} | |
| 587 | 243 | |
| 588 | HandleSeq all_atoms; | |
| 589 | as->get_handles_by_type(all_atoms, ATOM, true); | |
| 590 | for (const Handle& h : all_atoms) | |
| 591 | storeAtom(h, false); | |
| 592 | ||
| 593 | _backend->barrier(); | |
| 594 | } | |
| 595 | ||
| 596 | // ============================================================== | |
| 597 | // Barrier | |
| 244 | // ================================================================ | |
| 245 | // Synchronization | |
| 598 | 246 | |
| 599 | 247 | void GpuStorageNode::barrier(AtomSpace*) |
| 600 | 248 | { |
| 601 | if (_connected) | |
| 602 | _backend->barrier(); | |
| 249 | if (_is_open) | |
| 250 | gpu_table_barrier(&_gpu_table); | |
| 603 | 251 | } |
| 604 | 252 | |
| 605 | // ============================================================== | |
| 606 | // Monitor / Stats | |
| 253 | // ================================================================ | |
| 254 | // Diagnostics | |
| 607 | 255 | |
| 608 | 256 | std::string GpuStorageNode::monitor(void) |
| 609 | 257 | { |
| 610 | std::ostringstream ss; | |
| 611 | ss << "GpuStorageNode: " << _uri << "\n"; | |
| 612 | ss << " Connected: " << (_connected ? "yes" : "no") << "\n"; | |
| 613 | ||
| 614 | if (_connected) | |
| 258 | std::string s; | |
| 259 | s += "GPU Storage Monitor\n"; | |
| 260 | s += "Connected: "; | |
| 261 | s += (_is_open ? "yes" : "no"); | |
| 262 | s += "\n"; | |
| 263 | if (_is_open) | |
| 615 | 264 | { |
| 616 | ss << " Backend: " << _backend->backend_name() << "\n"; | |
| 617 | ss << " Device: " << _backend->device_info() << "\n"; | |
| 618 | ss << " Words: " << _backend->word_pool_count() | |
| 619 | << " / " << GPU_WORD_CAPACITY << "\n"; | |
| 620 | ss << " Pairs: " << _backend->pair_pool_count() | |
| 621 | << " / " << GPU_PAIR_CAPACITY << "\n"; | |
| 622 | ss << " Sections: " << _backend->section_pool_count() | |
| 623 | << " / " << GPU_SECTION_CAPACITY << "\n"; | |
| 624 | ss << " Stores: " << _num_stores.load() << "\n"; | |
| 625 | ss << " Fetches: " << _num_fetches.load() << "\n"; | |
| 265 | s += "Backend: "; | |
| 266 | switch (_gpu_table.backend) { | |
| 267 | case GPU_BACKEND_CUDA: s += "CUDA\n"; break; | |
| 268 | case GPU_BACKEND_OPENCL: s += "OpenCL\n"; break; | |
| 269 | default: s += "None\n"; break; | |
| 270 | } | |
| 271 | s += "Atoms: " + std::to_string(_gpu_table.atom_count) + "\n"; | |
| 272 | s += "Name pool: " + std::to_string(_gpu_table.name_pool_used) + " bytes\n"; | |
| 273 | s += "Out pool: " + std::to_string(_gpu_table.out_pool_used) + " slots\n"; | |
| 626 | 274 | } |
| 627 | ||
| 628 | return ss.str(); | |
| 275 | return s; | |
| 629 | 276 | } |
| 630 | 277 | |
| 631 | // ============================================================== | |
| 632 | // Phase 2: runQuery (stub) | |
| 278 | // ================================================================ | |
| 279 | // Factory | |
| 633 | 280 | |
| 634 | void GpuStorageNode::runQuery(const Handle& query, const Handle& key, | |
| 635 | const Handle& metadata_key, bool fresh) | |
| 281 | Handle GpuStorageNode::factory(const Handle& base) | |
| 636 | 282 | { |
| 637 | throw RuntimeException(TRACE_INFO, | |
| 638 | "GpuStorageNode::runQuery() not yet implemented (Phase 2)\n"); | |
| 283 | Handle h(createGpuStorageNode(base->get_name())); | |
| 284 | return h; | |
| 639 | 285 | } |
| 640 | ||
| 641 | // ============================================================== | |
| 642 | // Factory | |
| 643 | ||
| 644 | DEFINE_NODE_FACTORY(GpuStorageNode, GPU_STORAGE_NODE); | |
| 645 | ||
| 646 | // ============================================================== |
| … | … | @@ -1,130 +1,83 @@ |
| 1 | 1 | /* |
| 2 | 2 | * opencog/persist/gpu/GpuStorageNode.h |
| 3 | 3 | * |
| 4 | * StorageNode implementation backed by GPU SoA pools. | |
| 5 | * Provides the standard StorageNode API (store/fetch/query) | |
| 6 | * using GPU-resident word, pair, and section pools. | |
| 7 | * Same pattern as RocksStorageNode (disk) and CogStorageNode | |
| 8 | * (network), but targeting GPU memory. | |
| 4 | * BackingStore wrapper for GpuAtomTable. Bridges the AtomSpace | |
| 5 | * persistence API to the GPU atom table (Step 2). | |
| 9 | 6 | * |
| 10 | * Supports CUDA (primary, NVIDIA) and OpenCL (fallback, AMD/Intel). | |
| 11 | * Backend is selected at open() time based on hardware availability | |
| 12 | * and URI hints. | |
| 13 | * | |
| 14 | * URI format: "gpu://[backend:]platform:device" | |
| 15 | * "gpu://:" first available device (CUDA preferred) | |
| 16 | * "gpu://NVIDIA:RTX" first NVIDIA device containing "RTX" | |
| 17 | * "gpu://cuda::" force CUDA backend | |
| 18 | * "gpu://opencl::" force OpenCL backend | |
| 19 | * "gpu://opencl:Intel:" OpenCL on first Intel device | |
| 20 | * | |
| 21 | * Copyright (C) 2025 OpenCog Foundation | |
| 7 | * Step 3 of Linas' plan: storeAtom/getAtom via BackingStore.h. | |
| 8 | * Uses TLB for Handle ↔ GPU-slot identity mapping. | |
| 22 | 9 | * |
| 10 | * Copyright (C) 2026 OpenCog Foundation | |
| 23 | 11 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 24 | 12 | */ |
| 25 | 13 | |
| 26 | 14 | #ifndef _OPENCOG_GPU_STORAGE_NODE_H |
| 27 | 15 | #define _OPENCOG_GPU_STORAGE_NODE_H |
| 28 | 16 | |
| 29 | #include <memory> | |
| 30 | 17 | #include <mutex> |
| 31 | 18 | #include <unordered_map> |
| 32 | #include <functional> | |
| 19 | #include <vector> | |
| 33 | 20 | |
| 21 | #include <opencog/atomspace/AtomSpace.h> | |
| 34 | 22 | #include <opencog/persist/api/StorageNode.h> |
| 35 | ||
| 36 | #include "gpu-pool-defs.h" | |
| 37 | #include "GpuBackend.h" | |
| 23 | #include <opencog/persist/tlb/TLB.h> | |
| 24 | #include <opencog/persist/gpu-types/atom_types.h> | |
| 25 | #include <opencog/persist/gpu/GpuAtomTable.h> | |
| 38 | 26 | |
| 39 | 27 | namespace opencog |
| 40 | 28 | { |
| 41 | 29 | |
| 42 | /** \addtogroup grp_persist | |
| 43 | * @{ | |
| 44 | */ | |
| 45 | ||
| 46 | 30 | class GpuStorageNode : public StorageNode |
| 47 | 31 | { |
| 48 | 32 | private: |
| 49 | std::string _uri; | |
| 50 | std::string _backend_hint; // "cuda", "opencl", or "" (auto) | |
| 51 | std::string _splat; // platform substring | |
| 52 | std::string _sdev; // device substring | |
| 53 | ||
| 54 | // GPU backend (CUDA or OpenCL, selected at open time) | |
| 55 | std::unique_ptr<GpuBackend> _backend; | |
| 56 | bool _connected; | |
| 57 | ||
| 58 | // CPU-side name->hash mapping (GPU stores hashes, CPU knows strings) | |
| 59 | std::mutex _name_mtx; | |
| 60 | std::unordered_map<std::string, uint64_t> _name_to_hash; | |
| 61 | std::unordered_map<uint64_t, std::string> _hash_to_name; | |
| 62 | ||
| 63 | // Atom->GPU-index mapping for Values | |
| 64 | struct ValueSlot { | |
| 65 | uint32_t pool_index; // index in word/pair/section pool | |
| 66 | int pool_type; // 0=word, 1=pair, 2=section | |
| 67 | }; | |
| 68 | std::mutex _atom_mtx; | |
| 69 | std::unordered_map<Handle, ValueSlot> _atom_map; | |
| 70 | ||
| 71 | // Internal helpers | |
| 72 | void parse_uri(void); | |
| 73 | ||
| 74 | uint64_t name_hash(const std::string& name); | |
| 75 | uint32_t store_word(const std::string& name); | |
| 76 | uint32_t store_pair(uint32_t word_a, uint32_t word_b); | |
| 77 | uint32_t lookup_word(const std::string& name); | |
| 78 | uint32_t lookup_pair(uint32_t word_a, uint32_t word_b); | |
| 79 | ||
| 80 | void store_node_values(const Handle&, uint32_t pool_idx); | |
| 81 | void load_node_values(const Handle&, uint32_t pool_idx); | |
| 82 | void store_link_values(const Handle&, uint32_t pool_idx); | |
| 83 | void load_link_values(const Handle&, uint32_t pool_idx); | |
| 84 | ||
| 85 | // Stats tracking | |
| 86 | std::atomic<size_t> _num_stores; | |
| 87 | std::atomic<size_t> _num_fetches; | |
| 88 | ||
| 89 | public: | |
| 90 | GpuStorageNode(Type t, const std::string&& uri); | |
| 91 | GpuStorageNode(const std::string&& uri); | |
| 92 | GpuStorageNode(const GpuStorageNode&) = delete; | |
| 93 | GpuStorageNode& operator=(const GpuStorageNode&) = delete; | |
| 94 | virtual ~GpuStorageNode(); | |
| 95 | ||
| 96 | // -- StorageNode interface -- | |
| 97 | void open(void) override; | |
| 98 | void close(void) override; | |
| 99 | bool connected(void) override; | |
| 100 | ||
| 101 | void create(void) override { erase(); } | |
| 102 | void destroy(void) override { erase(); } | |
| 103 | void erase(void) override; | |
| 33 | GpuAtomTable _gpu_table; | |
| 34 | TLB _tlb; | |
| 35 | bool _is_open; | |
| 36 | std::mutex _mtx; | |
| 104 | 37 | |
| 105 | // -- BackingStore interface -- | |
| 106 | void getAtom(const Handle&) override; | |
| 107 | void storeAtom(const Handle&, bool synchronous = false) override; | |
| 108 | void removeAtom(AtomSpace*, const Handle&, bool recursive) override; | |
| 38 | // Handle → GPU slot (forward lookup). | |
| 39 | // TLB provides the reverse (slot → Handle). | |
| 40 | std::unordered_map<Handle, uint32_t, | |
| 41 | std::hash<opencog::Handle>, | |
| 42 | std::equal_to<opencog::Handle>> _handle_to_slot; | |
| 109 | 43 | |
| 110 | void storeValue(const Handle& atom, const Handle& key) override; | |
| 111 | void loadValue(const Handle& atom, const Handle& key) override; | |
| 44 | // Per-slot node/link flag for two-pass loadAtomSpace. | |
| 45 | std::vector<bool> _slot_is_node; | |
| 112 | 46 | |
| 113 | void fetchIncomingSet(AtomSpace*, const Handle&) override; | |
| 114 | void fetchIncomingByType(AtomSpace*, const Handle&, Type) override; | |
| 115 | void loadType(AtomSpace*, Type) override; | |
| 116 | void loadAtomSpace(AtomSpace*) override; | |
| 117 | void storeAtomSpace(const AtomSpace*) override; | |
| 118 | void barrier(AtomSpace* = nullptr) override; | |
| 47 | bool has_slot(const Handle&) const; | |
| 48 | uint32_t assign_slot(const Handle&); | |
| 49 | void do_store_atom(const Handle&); | |
| 119 | 50 | |
| 120 | std::string monitor(void) override; | |
| 51 | public: | |
| 52 | GpuStorageNode(Type t, const std::string&& uri); | |
| 53 | GpuStorageNode(const std::string& uri); | |
| 54 | virtual ~GpuStorageNode(); | |
| 121 | 55 | |
| 122 | // -- Phase 2: Query execution on GPU -- | |
| 123 | void runQuery(const Handle&, const Handle&, | |
| 124 | const Handle& = Handle::UNDEFINED, | |
| 125 | bool = false) override; | |
| 56 | // StorageNode lifecycle | |
| 57 | void open(void); | |
| 58 | void close(void); | |
| 59 | bool connected(void); | |
| 60 | void create(void) {} | |
| 61 | void destroy(void); | |
| 62 | void erase(void); | |
| 63 | ||
| 64 | // BackingStore interface (implemented) | |
| 65 | void storeAtom(const Handle&, bool synchronous = false); | |
| 66 | void loadAtomSpace(AtomSpace*); | |
| 67 | void storeAtomSpace(const AtomSpace*); | |
| 68 | void barrier(AtomSpace* = nullptr); | |
| 69 | std::string monitor(void); | |
| 70 | ||
| 71 | // BackingStore interface (stubbed for Step 3) | |
| 72 | void getAtom(const Handle&); | |
| 73 | void fetchIncomingSet(AtomSpace*, const Handle&); | |
| 74 | void fetchIncomingByType(AtomSpace*, const Handle&, Type); | |
| 75 | void removeAtom(AtomSpace*, const Handle&, bool recursive); | |
| 76 | void loadType(AtomSpace*, Type); | |
| 77 | ||
| 78 | // Expose GPU table for verification in tests. | |
| 79 | const GpuAtomTable& gpu_table() const { return _gpu_table; } | |
| 126 | 80 | |
| 127 | // Atom factory | |
| 128 | 81 | void setAtomSpace(AtomSpace* as) |
| 129 | 82 | { |
| 130 | 83 | if (nullptr == as) close(); |
| … | … | @@ -136,7 +89,6 @@ public: |
| 136 | 89 | NODE_PTR_DECL(GpuStorageNode) |
| 137 | 90 | #define createGpuStorageNode CREATE_DECL(GpuStorageNode) |
| 138 | 91 | |
| 139 | /** @}*/ | |
| 140 | 92 | } // namespace opencog |
| 141 | 93 | |
| 142 | 94 | #endif // _OPENCOG_GPU_STORAGE_NODE_H |
| … | … | @@ -1,499 +0,0 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/OpenCLBackend.cc | |
| 3 | * | |
| 4 | * OpenCL implementation of GpuBackend. | |
| 5 | * Manages OpenCL context, command queue, kernel compilation, | |
| 6 | * and GPU buffer operations for SoA pools. | |
| 7 | * | |
| 8 | * Copyright (C) 2025 OpenCog Foundation | |
| 9 | * | |
| 10 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 11 | */ | |
| 12 | ||
| 13 | #include <fstream> | |
| 14 | #include <sstream> | |
| 15 | ||
| 16 | #include <opencog/util/exceptions.h> | |
| 17 | #include <opencog/util/Logger.h> | |
| 18 | ||
| 19 | #include "OpenCLBackend.h" | |
| 20 | ||
| 21 | using namespace opencog; | |
| 22 | ||
| 23 | // ============================================================== | |
| 24 | ||
| 25 | OpenCLBackend::~OpenCLBackend() | |
| 26 | { | |
| 27 | // OpenCL resources are RAII via cl::* wrappers | |
| 28 | } | |
| 29 | ||
| 30 | // ============================================================== | |
| 31 | // Device discovery | |
| 32 | ||
| 33 | void OpenCLBackend::find_device(const std::string& splat, | |
| 34 | const std::string& sdev) | |
| 35 | { | |
| 36 | std::vector<cl::Platform> platforms; | |
| 37 | cl::Platform::get(&platforms); | |
| 38 | ||
| 39 | for (const auto& plat : platforms) | |
| 40 | { | |
| 41 | std::string pname = plat.getInfo<CL_PLATFORM_NAME>(); | |
| 42 | if (0 < splat.size() and pname.find(splat) == std::string::npos) | |
| 43 | continue; | |
| 44 | ||
| 45 | std::vector<cl::Device> devices; | |
| 46 | plat.getDevices(CL_DEVICE_TYPE_ALL, &devices); | |
| 47 | for (const cl::Device& dev : devices) | |
| 48 | { | |
| 49 | std::string dname = dev.getInfo<CL_DEVICE_NAME>(); | |
| 50 | if (0 < sdev.size() and dname.find(sdev) == std::string::npos) | |
| 51 | continue; | |
| 52 | ||
| 53 | _platform = plat; | |
| 54 | _device = dev; | |
| 55 | _platform_name = pname; | |
| 56 | _device_name = dname; | |
| 57 | ||
| 58 | logger().info("OpenCLBackend: Using platform '%s' device '%s'\n", | |
| 59 | pname.c_str(), dname.c_str()); | |
| 60 | return; | |
| 61 | } | |
| 62 | } | |
| 63 | ||
| 64 | throw RuntimeException(TRACE_INFO, | |
| 65 | "OpenCLBackend: unable to find platform '%s' device '%s'\n", | |
| 66 | splat.c_str(), sdev.c_str()); | |
| 67 | } | |
| 68 | ||
| 69 | // ============================================================== | |
| 70 | // Kernel compilation | |
| 71 | ||
| 72 | void OpenCLBackend::compile_kernels() | |
| 73 | { | |
| 74 | std::ostringstream opts; | |
| 75 | opts << "-D WORD_CAPACITY=" << GPU_WORD_CAPACITY | |
| 76 | << " -D PAIR_CAPACITY=" << GPU_PAIR_CAPACITY | |
| 77 | << " -D SECTION_CAPACITY=" << GPU_SECTION_CAPACITY | |
| 78 | << " -D WORD_HT_CAPACITY=" << GPU_WORD_HT_CAPACITY | |
| 79 | << " -D PAIR_HT_CAPACITY=" << GPU_PAIR_HT_CAPACITY | |
| 80 | << " -D SECTION_HT_CAPACITY=" << GPU_SECTION_HT_CAPACITY; | |
| 81 | ||
| 82 | std::string ht_src, as_src; | |
| 83 | std::vector<std::string> search_paths = { | |
| 84 | "/usr/local/share/opencog/opencl/gpu/", | |
| 85 | "/usr/share/opencog/opencl/gpu/", | |
| 86 | }; | |
| 87 | ||
| 88 | search_paths.push_back("opencog/gpu/"); | |
| 89 | search_paths.push_back("../opencog/gpu/"); | |
| 90 | search_paths.push_back("../../opencog/gpu/"); | |
| 91 | ||
| 92 | for (const auto& prefix : search_paths) | |
| 93 | { | |
| 94 | if (not ht_src.empty() and not as_src.empty()) break; | |
| 95 | ||
| 96 | if (ht_src.empty()) | |
| 97 | { | |
| 98 | std::ifstream f(prefix + "gpu-hashtable.cl"); | |
| 99 | if (f.is_open()) | |
| 100 | ht_src.assign(std::istreambuf_iterator<char>(f), | |
| 101 | std::istreambuf_iterator<char>()); | |
| 102 | } | |
| 103 | if (as_src.empty()) | |
| 104 | { | |
| 105 | std::ifstream f(prefix + "gpu-atomspace.cl"); | |
| 106 | if (f.is_open()) | |
| 107 | as_src.assign(std::istreambuf_iterator<char>(f), | |
| 108 | std::istreambuf_iterator<char>()); | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | if (ht_src.empty() or as_src.empty()) | |
| 113 | throw RuntimeException(TRACE_INFO, | |
| 114 | "OpenCLBackend: cannot find gpu-hashtable.cl " | |
| 115 | "or gpu-atomspace.cl kernel source files\n"); | |
| 116 | ||
| 117 | std::string combined = ht_src + "\n" + as_src; | |
| 118 | ||
| 119 | cl::Program::Sources sources; | |
| 120 | sources.push_back(combined); | |
| 121 | _program = cl::Program(_context, sources); | |
| 122 | ||
| 123 | try | |
| 124 | { | |
| 125 | _program.build(opts.str().c_str()); | |
| 126 | } | |
| 127 | catch (const cl::Error& e) | |
| 128 | { | |
| 129 | std::string log = _program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(_device); | |
| 130 | logger().error("OpenCLBackend kernel build failed:\n%s\n", log.c_str()); | |
| 131 | throw RuntimeException(TRACE_INFO, | |
| 132 | "OpenCLBackend: kernel compilation failed\n%s\n", | |
| 133 | log.c_str()); | |
| 134 | } | |
| 135 | ||
| 136 | logger().info("OpenCLBackend: kernels compiled successfully\n"); | |
| 137 | } | |
| 138 | ||
| 139 | // ============================================================== | |
| 140 | // Lifecycle | |
| 141 | ||
| 142 | void OpenCLBackend::init(const std::string& platform_hint, | |
| 143 | const std::string& device_hint) | |
| 144 | { | |
| 145 | find_device(platform_hint, device_hint); | |
| 146 | _context = cl::Context(_device); | |
| 147 | _queue = cl::CommandQueue(_context, _device); | |
| 148 | compile_kernels(); | |
| 149 | } | |
| 150 | ||
| 151 | void OpenCLBackend::shutdown() | |
| 152 | { | |
| 153 | _queue.finish(); | |
| 154 | } | |
| 155 | ||
| 156 | std::string OpenCLBackend::device_info() | |
| 157 | { | |
| 158 | return _platform_name + " / " + _device_name; | |
| 159 | } | |
| 160 | ||
| 161 | // ============================================================== | |
| 162 | // Pool allocation | |
| 163 | ||
| 164 | void OpenCLBackend::alloc_pools() | |
| 165 | { | |
| 166 | // -- WordPool -- | |
| 167 | _word_name_hash = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 168 | sizeof(cl_ulong) * GPU_WORD_CAPACITY); | |
| 169 | _word_count = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 170 | sizeof(cl_double) * GPU_WORD_CAPACITY); | |
| 171 | _word_mi_marginal = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 172 | sizeof(cl_double) * GPU_WORD_CAPACITY); | |
| 173 | _word_class_id = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 174 | sizeof(cl_uint) * GPU_WORD_CAPACITY); | |
| 175 | _word_next_free = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 176 | sizeof(cl_uint)); | |
| 177 | ||
| 178 | // -- PairPool -- | |
| 179 | _pair_word_a = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 180 | sizeof(cl_uint) * GPU_PAIR_CAPACITY); | |
| 181 | _pair_word_b = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 182 | sizeof(cl_uint) * GPU_PAIR_CAPACITY); | |
| 183 | _pair_count = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 184 | sizeof(cl_double) * GPU_PAIR_CAPACITY); | |
| 185 | _pair_mi = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 186 | sizeof(cl_double) * GPU_PAIR_CAPACITY); | |
| 187 | _pair_flags = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 188 | sizeof(cl_uint) * GPU_PAIR_CAPACITY); | |
| 189 | _pair_next_free = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 190 | sizeof(cl_uint)); | |
| 191 | ||
| 192 | // -- SectionPool -- | |
| 193 | _sec_word = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 194 | sizeof(cl_uint) * GPU_SECTION_CAPACITY); | |
| 195 | _sec_djh = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 196 | sizeof(cl_ulong) * GPU_SECTION_CAPACITY); | |
| 197 | _sec_count = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 198 | sizeof(cl_double) * GPU_SECTION_CAPACITY); | |
| 199 | _sec_next_free = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 200 | sizeof(cl_uint)); | |
| 201 | ||
| 202 | // -- Hash tables -- | |
| 203 | _word_ht_keys = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 204 | sizeof(cl_ulong) * GPU_WORD_HT_CAPACITY); | |
| 205 | _word_ht_values = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 206 | sizeof(cl_uint) * GPU_WORD_HT_CAPACITY); | |
| 207 | _pair_ht_keys = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 208 | sizeof(cl_ulong) * GPU_PAIR_HT_CAPACITY); | |
| 209 | _pair_ht_values = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 210 | sizeof(cl_uint) * GPU_PAIR_HT_CAPACITY); | |
| 211 | _sec_ht_keys = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 212 | sizeof(cl_ulong) * GPU_SECTION_HT_CAPACITY); | |
| 213 | _sec_ht_values = cl::Buffer(_context, CL_MEM_READ_WRITE, | |
| 214 | sizeof(cl_uint) * GPU_SECTION_HT_CAPACITY); | |
| 215 | } | |
| 216 | ||
| 217 | void OpenCLBackend::init_pools() | |
| 218 | { | |
| 219 | cl_uint zero = 0; | |
| 220 | _queue.enqueueWriteBuffer(_word_next_free, CL_TRUE, 0, sizeof(cl_uint), &zero); | |
| 221 | _queue.enqueueWriteBuffer(_pair_next_free, CL_TRUE, 0, sizeof(cl_uint), &zero); | |
| 222 | _queue.enqueueWriteBuffer(_sec_next_free, CL_TRUE, 0, sizeof(cl_uint), &zero); | |
| 223 | ||
| 224 | // Initialize hash table keys to EMPTY, values to EMPTY. | |
| 225 | // Use CL_TRUE (blocking) because host vectors go out of scope. | |
| 226 | { | |
| 227 | std::vector<cl_ulong> ek(GPU_WORD_HT_CAPACITY, GPU_HT_EMPTY_KEY); | |
| 228 | _queue.enqueueWriteBuffer(_word_ht_keys, CL_TRUE, 0, | |
| 229 | sizeof(cl_ulong) * GPU_WORD_HT_CAPACITY, ek.data()); | |
| 230 | } | |
| 231 | { | |
| 232 | std::vector<cl_uint> ev(GPU_WORD_HT_CAPACITY, GPU_HT_EMPTY_VALUE); | |
| 233 | _queue.enqueueWriteBuffer(_word_ht_values, CL_TRUE, 0, | |
| 234 | sizeof(cl_uint) * GPU_WORD_HT_CAPACITY, ev.data()); | |
| 235 | } | |
| 236 | { | |
| 237 | std::vector<cl_ulong> ek(GPU_PAIR_HT_CAPACITY, GPU_HT_EMPTY_KEY); | |
| 238 | _queue.enqueueWriteBuffer(_pair_ht_keys, CL_TRUE, 0, | |
| 239 | sizeof(cl_ulong) * GPU_PAIR_HT_CAPACITY, ek.data()); | |
| 240 | } | |
| 241 | { | |
| 242 | std::vector<cl_uint> ev(GPU_PAIR_HT_CAPACITY, GPU_HT_EMPTY_VALUE); | |
| 243 | _queue.enqueueWriteBuffer(_pair_ht_values, CL_TRUE, 0, | |
| 244 | sizeof(cl_uint) * GPU_PAIR_HT_CAPACITY, ev.data()); | |
| 245 | } | |
| 246 | { | |
| 247 | std::vector<cl_ulong> ek(GPU_SECTION_HT_CAPACITY, GPU_HT_EMPTY_KEY); | |
| 248 | _queue.enqueueWriteBuffer(_sec_ht_keys, CL_TRUE, 0, | |
| 249 | sizeof(cl_ulong) * GPU_SECTION_HT_CAPACITY, ek.data()); | |
| 250 | } | |
| 251 | { | |
| 252 | std::vector<cl_uint> ev(GPU_SECTION_HT_CAPACITY, GPU_HT_EMPTY_VALUE); | |
| 253 | _queue.enqueueWriteBuffer(_sec_ht_values, CL_TRUE, 0, | |
| 254 | sizeof(cl_uint) * GPU_SECTION_HT_CAPACITY, ev.data()); | |
| 255 | } | |
| 256 | ||
| 257 | logger().info("OpenCLBackend: GPU pools initialized\n"); | |
| 258 | } | |
| 259 | ||
| 260 | // ============================================================== | |
| 261 | // Word pool operations | |
| 262 | ||
| 263 | uint32_t OpenCLBackend::word_find_or_create(uint64_t nhash) | |
| 264 | { | |
| 265 | cl::Kernel kern(_program, "word_find_or_create"); | |
| 266 | ||
| 267 | cl::Buffer in_hash(_context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 268 | sizeof(cl_ulong), &nhash); | |
| 269 | cl::Buffer out_buf(_context, CL_MEM_WRITE_ONLY, sizeof(cl_uint)); | |
| 270 | cl_uint num_items = 1; | |
| 271 | ||
| 272 | kern.setArg(0, _word_ht_keys); | |
| 273 | kern.setArg(1, _word_ht_values); | |
| 274 | kern.setArg(2, _word_name_hash); | |
| 275 | kern.setArg(3, _word_count); | |
| 276 | kern.setArg(4, _word_class_id); | |
| 277 | kern.setArg(5, _word_next_free); | |
| 278 | kern.setArg(6, in_hash); | |
| 279 | kern.setArg(7, out_buf); | |
| 280 | kern.setArg(8, num_items); | |
| 281 | ||
| 282 | _queue.enqueueNDRangeKernel(kern, cl::NullRange, cl::NDRange(1)); | |
| 283 | cl_uint out_idx = GPU_NOT_FOUND; | |
| 284 | _queue.enqueueReadBuffer(out_buf, CL_TRUE, 0, sizeof(cl_uint), &out_idx); | |
| 285 | ||
| 286 | return out_idx; | |
| 287 | } | |
| 288 | ||
| 289 | uint32_t OpenCLBackend::word_lookup(uint64_t nhash) | |
| 290 | { | |
| 291 | cl::Kernel kern(_program, "ht_lookup"); | |
| 292 | ||
| 293 | cl::Buffer query_key(_context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 294 | sizeof(cl_ulong), &nhash); | |
| 295 | cl::Buffer out_buf(_context, CL_MEM_WRITE_ONLY, sizeof(cl_uint)); | |
| 296 | cl_ulong capacity = GPU_WORD_HT_CAPACITY; | |
| 297 | cl_uint num_queries = 1; | |
| 298 | ||
| 299 | kern.setArg(0, _word_ht_keys); | |
| 300 | kern.setArg(1, _word_ht_values); | |
| 301 | kern.setArg(2, capacity); | |
| 302 | kern.setArg(3, query_key); | |
| 303 | kern.setArg(4, out_buf); | |
| 304 | kern.setArg(5, num_queries); | |
| 305 | ||
| 306 | _queue.enqueueNDRangeKernel(kern, cl::NullRange, cl::NDRange(1)); | |
| 307 | cl_uint result = GPU_NOT_FOUND; | |
| 308 | _queue.enqueueReadBuffer(out_buf, CL_TRUE, 0, sizeof(cl_uint), &result); | |
| 309 | ||
| 310 | return result; | |
| 311 | } | |
| 312 | ||
| 313 | void OpenCLBackend::word_write_count(uint32_t idx, double count) | |
| 314 | { | |
| 315 | cl_double v = count; | |
| 316 | _queue.enqueueWriteBuffer(_word_count, CL_FALSE, | |
| 317 | sizeof(cl_double) * idx, sizeof(cl_double), &v); | |
| 318 | } | |
| 319 | ||
| 320 | void OpenCLBackend::word_write_marginal(uint32_t idx, double marginal) | |
| 321 | { | |
| 322 | cl_double v = marginal; | |
| 323 | _queue.enqueueWriteBuffer(_word_mi_marginal, CL_FALSE, | |
| 324 | sizeof(cl_double) * idx, sizeof(cl_double), &v); | |
| 325 | } | |
| 326 | ||
| 327 | void OpenCLBackend::word_read_values(uint32_t idx, | |
| 328 | double& count, double& marginal) | |
| 329 | { | |
| 330 | cl_double c = 0.0, m = 0.0; | |
| 331 | _queue.enqueueReadBuffer(_word_count, CL_TRUE, | |
| 332 | sizeof(cl_double) * idx, sizeof(cl_double), &c); | |
| 333 | _queue.enqueueReadBuffer(_word_mi_marginal, CL_TRUE, | |
| 334 | sizeof(cl_double) * idx, sizeof(cl_double), &m); | |
| 335 | count = c; | |
| 336 | marginal = m; | |
| 337 | } | |
| 338 | ||
| 339 | void OpenCLBackend::word_delete(uint64_t nhash) | |
| 340 | { | |
| 341 | cl::Kernel kern(_program, "ht_delete"); | |
| 342 | cl::Buffer del_key(_context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 343 | sizeof(cl_ulong), &nhash); | |
| 344 | cl_ulong capacity = GPU_WORD_HT_CAPACITY; | |
| 345 | cl_uint num = 1; | |
| 346 | ||
| 347 | kern.setArg(0, _word_ht_keys); | |
| 348 | kern.setArg(1, _word_ht_values); | |
| 349 | kern.setArg(2, capacity); | |
| 350 | kern.setArg(3, del_key); | |
| 351 | kern.setArg(4, num); | |
| 352 | ||
| 353 | _queue.enqueueNDRangeKernel(kern, cl::NullRange, cl::NDRange(1)); | |
| 354 | _queue.finish(); | |
| 355 | } | |
| 356 | ||
| 357 | uint32_t OpenCLBackend::word_pool_count() | |
| 358 | { | |
| 359 | cl_uint cnt = 0; | |
| 360 | _queue.enqueueReadBuffer(_word_next_free, CL_TRUE, 0, sizeof(cl_uint), &cnt); | |
| 361 | return cnt; | |
| 362 | } | |
| 363 | ||
| 364 | void OpenCLBackend::word_read_bulk(uint32_t n, uint64_t* hashes, | |
| 365 | double* counts, double* marginals) | |
| 366 | { | |
| 367 | _queue.enqueueReadBuffer(_word_name_hash, CL_TRUE, 0, | |
| 368 | sizeof(cl_ulong) * n, hashes); | |
| 369 | _queue.enqueueReadBuffer(_word_count, CL_TRUE, 0, | |
| 370 | sizeof(cl_double) * n, counts); | |
| 371 | _queue.enqueueReadBuffer(_word_mi_marginal, CL_TRUE, 0, | |
| 372 | sizeof(cl_double) * n, marginals); | |
| 373 | } | |
| 374 | ||
| 375 | // ============================================================== | |
| 376 | // Pair pool operations | |
| 377 | ||
| 378 | uint32_t OpenCLBackend::pair_find_or_create(uint32_t word_a, uint32_t word_b) | |
| 379 | { | |
| 380 | cl::Kernel kern(_program, "pair_find_or_create"); | |
| 381 | ||
| 382 | cl::Buffer in_wa(_context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 383 | sizeof(cl_uint), &word_a); | |
| 384 | cl::Buffer in_wb(_context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 385 | sizeof(cl_uint), &word_b); | |
| 386 | cl::Buffer out_buf(_context, CL_MEM_WRITE_ONLY, sizeof(cl_uint)); | |
| 387 | cl_uint num_items = 1; | |
| 388 | ||
| 389 | kern.setArg(0, _pair_ht_keys); | |
| 390 | kern.setArg(1, _pair_ht_values); | |
| 391 | kern.setArg(2, _pair_word_a); | |
| 392 | kern.setArg(3, _pair_word_b); | |
| 393 | kern.setArg(4, _pair_count); | |
| 394 | kern.setArg(5, _pair_mi); | |
| 395 | kern.setArg(6, _pair_flags); | |
| 396 | kern.setArg(7, _pair_next_free); | |
| 397 | kern.setArg(8, in_wa); | |
| 398 | kern.setArg(9, in_wb); | |
| 399 | kern.setArg(10, out_buf); | |
| 400 | kern.setArg(11, num_items); | |
| 401 | ||
| 402 | _queue.enqueueNDRangeKernel(kern, cl::NullRange, cl::NDRange(1)); | |
| 403 | cl_uint out_idx = GPU_NOT_FOUND; | |
| 404 | _queue.enqueueReadBuffer(out_buf, CL_TRUE, 0, sizeof(cl_uint), &out_idx); | |
| 405 | ||
| 406 | return out_idx; | |
| 407 | } | |
| 408 | ||
| 409 | uint32_t OpenCLBackend::pair_lookup(uint32_t word_a, uint32_t word_b) | |
| 410 | { | |
| 411 | uint32_t lo = std::min(word_a, word_b); | |
| 412 | uint32_t hi = std::max(word_a, word_b); | |
| 413 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 414 | ||
| 415 | cl::Kernel kern(_program, "ht_lookup"); | |
| 416 | ||
| 417 | cl::Buffer query_key(_context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 418 | sizeof(cl_ulong), &key); | |
| 419 | cl::Buffer out_buf(_context, CL_MEM_WRITE_ONLY, sizeof(cl_uint)); | |
| 420 | cl_ulong capacity = GPU_PAIR_HT_CAPACITY; | |
| 421 | cl_uint num_queries = 1; | |
| 422 | ||
| 423 | kern.setArg(0, _pair_ht_keys); | |
| 424 | kern.setArg(1, _pair_ht_values); | |
| 425 | kern.setArg(2, capacity); | |
| 426 | kern.setArg(3, query_key); | |
| 427 | kern.setArg(4, out_buf); | |
| 428 | kern.setArg(5, num_queries); | |
| 429 | ||
| 430 | _queue.enqueueNDRangeKernel(kern, cl::NullRange, cl::NDRange(1)); | |
| 431 | cl_uint result = GPU_NOT_FOUND; | |
| 432 | _queue.enqueueReadBuffer(out_buf, CL_TRUE, 0, sizeof(cl_uint), &result); | |
| 433 | ||
| 434 | return result; | |
| 435 | } | |
| 436 | ||
| 437 | void OpenCLBackend::pair_write_count(uint32_t idx, double count) | |
| 438 | { | |
| 439 | cl_double v = count; | |
| 440 | _queue.enqueueWriteBuffer(_pair_count, CL_FALSE, | |
| 441 | sizeof(cl_double) * idx, sizeof(cl_double), &v); | |
| 442 | } | |
| 443 | ||
| 444 | void OpenCLBackend::pair_write_mi(uint32_t idx, double mi) | |
| 445 | { | |
| 446 | cl_double v = mi; | |
| 447 | _queue.enqueueWriteBuffer(_pair_mi, CL_FALSE, | |
| 448 | sizeof(cl_double) * idx, sizeof(cl_double), &v); | |
| 449 | } | |
| 450 | ||
| 451 | void OpenCLBackend::pair_read_values(uint32_t idx, | |
| 452 | double& count, double& mi) | |
| 453 | { | |
| 454 | cl_double c = 0.0, m = 0.0; | |
| 455 | _queue.enqueueReadBuffer(_pair_count, CL_TRUE, | |
| 456 | sizeof(cl_double) * idx, sizeof(cl_double), &c); | |
| 457 | _queue.enqueueReadBuffer(_pair_mi, CL_TRUE, | |
| 458 | sizeof(cl_double) * idx, sizeof(cl_double), &m); | |
| 459 | count = c; | |
| 460 | mi = m; | |
| 461 | } | |
| 462 | ||
| 463 | uint32_t OpenCLBackend::pair_pool_count() | |
| 464 | { | |
| 465 | cl_uint cnt = 0; | |
| 466 | _queue.enqueueReadBuffer(_pair_next_free, CL_TRUE, 0, sizeof(cl_uint), &cnt); | |
| 467 | return cnt; | |
| 468 | } | |
| 469 | ||
| 470 | // ============================================================== | |
| 471 | // Section pool operations | |
| 472 | ||
| 473 | uint32_t OpenCLBackend::section_pool_count() | |
| 474 | { | |
| 475 | cl_uint cnt = 0; | |
| 476 | _queue.enqueueReadBuffer(_sec_next_free, CL_TRUE, 0, sizeof(cl_uint), &cnt); | |
| 477 | return cnt; | |
| 478 | } | |
| 479 | ||
| 480 | // ============================================================== | |
| 481 | ||
| 482 | void OpenCLBackend::barrier() | |
| 483 | { | |
| 484 | _queue.finish(); | |
| 485 | } | |
| 486 | ||
| 487 | // ============================================================== | |
| 488 | // Factory function | |
| 489 | ||
| 490 | namespace opencog { | |
| 491 | ||
| 492 | GpuBackend* create_opencl_backend() | |
| 493 | { | |
| 494 | return new OpenCLBackend(); | |
| 495 | } | |
| 496 | ||
| 497 | } // namespace opencog | |
| 498 | ||
| 499 | // ============================================================== |
| … | … | @@ -1,106 +0,0 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/OpenCLBackend.h | |
| 3 | * | |
| 4 | * OpenCL implementation of the GpuBackend interface. | |
| 5 | * Fallback backend for AMD/Intel GPUs, also works on NVIDIA. | |
| 6 | * | |
| 7 | * Copyright (C) 2025 OpenCog Foundation | |
| 8 | * | |
| 9 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 10 | */ | |
| 11 | ||
| 12 | #ifndef _OPENCOG_OPENCL_BACKEND_H | |
| 13 | #define _OPENCOG_OPENCL_BACKEND_H | |
| 14 | ||
| 15 | #include "GpuBackend.h" | |
| 16 | #include "gpu-pool-defs.h" | |
| 17 | #include "opencl-headers.h" | |
| 18 | ||
| 19 | namespace opencog | |
| 20 | { | |
| 21 | ||
| 22 | class OpenCLBackend : public GpuBackend | |
| 23 | { | |
| 24 | private: | |
| 25 | cl::Platform _platform; | |
| 26 | cl::Device _device; | |
| 27 | cl::Context _context; | |
| 28 | cl::CommandQueue _queue; | |
| 29 | cl::Program _program; | |
| 30 | ||
| 31 | // GPU SoA pool buffers -- WordPool | |
| 32 | cl::Buffer _word_name_hash; // ulong[WORD_CAPACITY] | |
| 33 | cl::Buffer _word_count; // double[WORD_CAPACITY] | |
| 34 | cl::Buffer _word_mi_marginal; // double[WORD_CAPACITY] | |
| 35 | cl::Buffer _word_class_id; // uint[WORD_CAPACITY] | |
| 36 | cl::Buffer _word_next_free; // uint[1] | |
| 37 | ||
| 38 | // GPU SoA pool buffers -- PairPool | |
| 39 | cl::Buffer _pair_word_a; // uint[PAIR_CAPACITY] | |
| 40 | cl::Buffer _pair_word_b; // uint[PAIR_CAPACITY] | |
| 41 | cl::Buffer _pair_count; // double[PAIR_CAPACITY] | |
| 42 | cl::Buffer _pair_mi; // double[PAIR_CAPACITY] | |
| 43 | cl::Buffer _pair_flags; // uint[PAIR_CAPACITY] | |
| 44 | cl::Buffer _pair_next_free; // uint[1] | |
| 45 | ||
| 46 | // GPU SoA pool buffers -- SectionPool | |
| 47 | cl::Buffer _sec_word; // uint[SECTION_CAPACITY] | |
| 48 | cl::Buffer _sec_djh; // ulong[SECTION_CAPACITY] | |
| 49 | cl::Buffer _sec_count; // double[SECTION_CAPACITY] | |
| 50 | cl::Buffer _sec_next_free; // uint[1] | |
| 51 | ||
| 52 | // Hash tables (keys + values for each pool) | |
| 53 | cl::Buffer _word_ht_keys; // ulong[WORD_HT_CAPACITY] | |
| 54 | cl::Buffer _word_ht_values; // uint[WORD_HT_CAPACITY] | |
| 55 | cl::Buffer _pair_ht_keys; // ulong[PAIR_HT_CAPACITY] | |
| 56 | cl::Buffer _pair_ht_values; // uint[PAIR_HT_CAPACITY] | |
| 57 | cl::Buffer _sec_ht_keys; // ulong[SECTION_HT_CAPACITY] | |
| 58 | cl::Buffer _sec_ht_values; // uint[SECTION_HT_CAPACITY] | |
| 59 | ||
| 60 | std::string _device_name; | |
| 61 | std::string _platform_name; | |
| 62 | ||
| 63 | void find_device(const std::string& platform, const std::string& device); | |
| 64 | void compile_kernels(); | |
| 65 | ||
| 66 | public: | |
| 67 | OpenCLBackend() = default; | |
| 68 | ~OpenCLBackend() override; | |
| 69 | ||
| 70 | void init(const std::string& platform_hint, | |
| 71 | const std::string& device_hint) override; | |
| 72 | void shutdown() override; | |
| 73 | std::string device_info() override; | |
| 74 | std::string backend_name() override { return "OpenCL"; } | |
| 75 | ||
| 76 | void alloc_pools() override; | |
| 77 | void init_pools() override; | |
| 78 | ||
| 79 | uint32_t word_find_or_create(uint64_t name_hash) override; | |
| 80 | uint32_t word_lookup(uint64_t name_hash) override; | |
| 81 | void word_write_count(uint32_t idx, double count) override; | |
| 82 | void word_write_marginal(uint32_t idx, double marginal) override; | |
| 83 | void word_read_values(uint32_t idx, | |
| 84 | double& count, double& marginal) override; | |
| 85 | void word_delete(uint64_t name_hash) override; | |
| 86 | uint32_t word_pool_count() override; | |
| 87 | void word_read_bulk(uint32_t n, uint64_t* hashes, | |
| 88 | double* counts, double* marginals) override; | |
| 89 | ||
| 90 | uint32_t pair_find_or_create(uint32_t word_a, | |
| 91 | uint32_t word_b) override; | |
| 92 | uint32_t pair_lookup(uint32_t word_a, uint32_t word_b) override; | |
| 93 | void pair_write_count(uint32_t idx, double count) override; | |
| 94 | void pair_write_mi(uint32_t idx, double mi) override; | |
| 95 | void pair_read_values(uint32_t idx, | |
| 96 | double& count, double& mi) override; | |
| 97 | uint32_t pair_pool_count() override; | |
| 98 | ||
| 99 | uint32_t section_pool_count() override; | |
| 100 | ||
| 101 | void barrier() override; | |
| 102 | }; | |
| 103 | ||
| 104 | } // namespace opencog | |
| 105 | ||
| 106 | #endif // _OPENCOG_OPENCL_BACKEND_H |
| … | … | @@ -1,48 +0,0 @@ |
| 1 | /* | |
| 2 | * opencog/persist/gpu/gpu-pool-defs.h | |
| 3 | * | |
| 4 | * Shared pool capacity constants for GPU SoA pools. | |
| 5 | * Used by both CUDA and OpenCL backends. | |
| 6 | * | |
| 7 | * Copyright (C) 2025 OpenCog Foundation | |
| 8 | * | |
| 9 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 10 | */ | |
| 11 | ||
| 12 | #ifndef _OPENCOG_GPU_POOL_DEFS_H | |
| 13 | #define _OPENCOG_GPU_POOL_DEFS_H | |
| 14 | ||
| 15 | // Pool capacity defaults (can be overridden at compile time). | |
| 16 | // | |
| 17 | // GPU memory usage at default capacities: | |
| 18 | // WordPool 128K slots, 256K hash table ~ 4 MB | |
| 19 | // PairPool 4M slots, 8M hash table ~160 MB | |
| 20 | // SectionPool 1M slots, 2M hash table ~ 40 MB | |
| 21 | // Total ~204 MB | |
| 22 | #ifndef GPU_WORD_CAPACITY | |
| 23 | #define GPU_WORD_CAPACITY 131072 // 128K words | |
| 24 | #endif | |
| 25 | #ifndef GPU_PAIR_CAPACITY | |
| 26 | #define GPU_PAIR_CAPACITY 4194304 // 4M pairs | |
| 27 | #endif | |
| 28 | #ifndef GPU_SECTION_CAPACITY | |
| 29 | #define GPU_SECTION_CAPACITY 1048576 // 1M sections | |
| 30 | #endif | |
| 31 | ||
| 32 | // Hash tables: 2x the pool capacity (50% load factor) | |
| 33 | #ifndef GPU_WORD_HT_CAPACITY | |
| 34 | #define GPU_WORD_HT_CAPACITY 262144 | |
| 35 | #endif | |
| 36 | #ifndef GPU_PAIR_HT_CAPACITY | |
| 37 | #define GPU_PAIR_HT_CAPACITY 8388608 | |
| 38 | #endif | |
| 39 | #ifndef GPU_SECTION_HT_CAPACITY | |
| 40 | #define GPU_SECTION_HT_CAPACITY 2097152 | |
| 41 | #endif | |
| 42 | ||
| 43 | // Sentinel values (must match gpu-hashtable.cl and CUDA kernels) | |
| 44 | #define GPU_HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 45 | #define GPU_HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 46 | #define GPU_NOT_FOUND 0xFFFFFFFFU | |
| 47 | ||
| 48 | #endif // _OPENCOG_GPU_POOL_DEFS_H |
| … | … | @@ -1,15 +0,0 @@ |
| 1 | ; | |
| 2 | ; persist-gpu.scm -- OpenCL GPU storage backend for the AtomSpace | |
| 3 | ; | |
| 4 | ; Load the GPU persistence module. | |
| 5 | ; | |
| 6 | ||
| 7 | (define-module (opencog persist-gpu)) | |
| 8 | ||
| 9 | (use-modules (opencog) (opencog exec) (opencog persist)) | |
| 10 | (use-modules (opencog gpu-config)) | |
| 11 | ||
| 12 | ; Load the C++ library that implements the GPU storage node. | |
| 13 | (load-extension (string-append opencog-ext-path-persist-gpu "libpersist-gpu") "opencog_persist_gpu_init") | |
| 14 | ||
| 15 | (export GpuStorageNode) |
| … | … | @@ -4,10 +4,7 @@ INCLUDE(AddCxxtest) |
| 4 | 4 | # All tests should load the atomspace scm from the build dir |
| 5 | 5 | SET(GUILE_LOAD_PATH "${PROJECT_BINARY_DIR}/opencog/scm") |
| 6 | 6 | |
| 7 | # GPU AtomSpace kernel tests (standalone, no CxxTest needed) | |
| 8 | ADD_SUBDIRECTORY (gpu) | |
| 9 | ||
| 10 | # StorageNode tests (require CxxTest) | |
| 7 | # GpuAtomTable tests | |
| 11 | 8 | IF (CXXTEST_FOUND) |
| 12 | 9 | ADD_SUBDIRECTORY (persist) |
| 13 | 10 | ENDIF (CXXTEST_FOUND) |
| … | … | @@ -1,54 +0,0 @@ |
| 1 | # | |
| 2 | # GPU AtomSpace kernel tests. | |
| 3 | # OpenCL tests (.c) test the OpenCL kernels. | |
| 4 | # CUDA tests (.cu) test the CUDA kernels. | |
| 5 | # | |
| 6 | ||
| 7 | # -- OpenCL kernel tests -- | |
| 8 | IF (HAVE_OPENCL) | |
| 9 | SET(OPENCL_TESTS | |
| 10 | test-hashtable | |
| 11 | test-atomspace | |
| 12 | test-mi | |
| 13 | test-cosine | |
| 14 | test-counting | |
| 15 | test-sections | |
| 16 | test-substitute | |
| 17 | ) | |
| 18 | ||
| 19 | FOREACH(TEST_NAME ${OPENCL_TESTS}) | |
| 20 | ADD_EXECUTABLE(${TEST_NAME} ${TEST_NAME}.c) | |
| 21 | TARGET_LINK_LIBRARIES(${TEST_NAME} OpenCL::OpenCL m) | |
| 22 | TARGET_INCLUDE_DIRECTORIES(${TEST_NAME} PRIVATE | |
| 23 | ${PROJECT_SOURCE_DIR} | |
| 24 | ${PROJECT_SOURCE_DIR}/opencog/gpu) | |
| 25 | ADD_TEST(NAME ${TEST_NAME} COMMAND ${TEST_NAME} | |
| 26 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) | |
| 27 | ENDFOREACH() | |
| 28 | ENDIF (HAVE_OPENCL) | |
| 29 | ||
| 30 | # -- CUDA kernel tests -- | |
| 31 | IF (HAVE_CUDA) | |
| 32 | SET(CUDA_TESTS | |
| 33 | test-connected-components | |
| 34 | test-connector-rewrite | |
| 35 | test-spanning-forest | |
| 36 | test-divergence | |
| 37 | test-learning-loop | |
| 38 | test-compartment-kernel | |
| 39 | ) | |
| 40 | ||
| 41 | FOREACH(TEST_NAME ${CUDA_TESTS}) | |
| 42 | ADD_EXECUTABLE(${TEST_NAME} ${TEST_NAME}.cu) | |
| 43 | SET_TARGET_PROPERTIES(${TEST_NAME} PROPERTIES | |
| 44 | CUDA_SEPARABLE_COMPILATION ON | |
| 45 | CUDA_ARCHITECTURES "50;72;75;86;89" | |
| 46 | ) | |
| 47 | TARGET_INCLUDE_DIRECTORIES(${TEST_NAME} PRIVATE | |
| 48 | ${PROJECT_SOURCE_DIR} | |
| 49 | ${PROJECT_SOURCE_DIR}/opencog/gpu) | |
| 50 | TARGET_LINK_LIBRARIES(${TEST_NAME} gpu-kernels) | |
| 51 | ADD_TEST(NAME ${TEST_NAME} COMMAND ${TEST_NAME} | |
| 52 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}) | |
| 53 | ENDFOREACH() | |
| 54 | ENDIF (HAVE_CUDA) |
| … | … | @@ -1,739 +0,0 @@ |
| 1 | /* | |
| 2 | * test-atomspace.c -- Test GPU-resident AtomSpace pools | |
| 3 | * | |
| 4 | * Compile: gcc -O2 -o test-atomspace test-atomspace.c -lOpenCL -lm | |
| 5 | * Run: ./test-atomspace | |
| 6 | * | |
| 7 | * Tests: | |
| 8 | * 1. Create words (find-or-create with dedup) | |
| 9 | * 2. Create pairs (find-or-create from word indices) | |
| 10 | * 3. Create sections | |
| 11 | * 4. Count pairs (atomic double increment + marginals) | |
| 12 | * 5. Count sections | |
| 13 | * 6. Verify counts read back correctly | |
| 14 | * 7. Performance: create + count rates | |
| 15 | */ | |
| 16 | ||
| 17 | #include <stdio.h> | |
| 18 | #include <stdlib.h> | |
| 19 | #include <stdint.h> | |
| 20 | #include <string.h> | |
| 21 | #include <math.h> | |
| 22 | #include <time.h> | |
| 23 | #include <CL/cl.h> | |
| 24 | ||
| 25 | /* ─── Pool capacities ─── */ | |
| 26 | ||
| 27 | #define WORD_CAPACITY (128 * 1024) /* 128K words */ | |
| 28 | #define PAIR_CAPACITY (4 * 1024 * 1024) /* 4M pairs */ | |
| 29 | #define SECTION_CAPACITY (1024 * 1024) /* 1M sections */ | |
| 30 | ||
| 31 | /* Hash table sizes (2x pool capacity for 50% load) */ | |
| 32 | #define WORD_HT_CAPACITY (256 * 1024) | |
| 33 | #define PAIR_HT_CAPACITY (8 * 1024 * 1024) | |
| 34 | #define SECTION_HT_CAPACITY (2 * 1024 * 1024) | |
| 35 | ||
| 36 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 37 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 38 | ||
| 39 | /* ─── Error checking ─── */ | |
| 40 | ||
| 41 | #define CL_CHECK(err, msg) do { \ | |
| 42 | if ((err) != CL_SUCCESS) { \ | |
| 43 | fprintf(stderr, "OpenCL error %d at %s:%d: %s\n", \ | |
| 44 | (err), __FILE__, __LINE__, (msg)); \ | |
| 45 | exit(1); \ | |
| 46 | } \ | |
| 47 | } while(0) | |
| 48 | ||
| 49 | /* ─── Read file ─── */ | |
| 50 | ||
| 51 | char* read_file(const char* path, size_t* len) | |
| 52 | { | |
| 53 | FILE* f = fopen(path, "r"); | |
| 54 | if (!f) { fprintf(stderr, "Cannot open %s\n", path); exit(1); } | |
| 55 | fseek(f, 0, SEEK_END); | |
| 56 | *len = ftell(f); | |
| 57 | fseek(f, 0, SEEK_SET); | |
| 58 | char* buf = malloc(*len + 1); | |
| 59 | size_t n = fread(buf, 1, *len, f); | |
| 60 | buf[n] = '\0'; | |
| 61 | *len = n; | |
| 62 | fclose(f); | |
| 63 | return buf; | |
| 64 | } | |
| 65 | ||
| 66 | /* ─── Simple hash for word names (CPU-side) ─── */ | |
| 67 | ||
| 68 | uint64_t hash_word(const char* name) | |
| 69 | { | |
| 70 | uint64_t h = 0x12345678DEADBEEFULL; | |
| 71 | while (*name) { | |
| 72 | h ^= (uint64_t)*name++; | |
| 73 | h *= 0xBF58476D1CE4E5B9ULL; | |
| 74 | h ^= h >> 31; | |
| 75 | } | |
| 76 | return h; | |
| 77 | } | |
| 78 | ||
| 79 | /* ─── Timing ─── */ | |
| 80 | ||
| 81 | double now_ms(void) | |
| 82 | { | |
| 83 | struct timespec ts; | |
| 84 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 85 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; | |
| 86 | } | |
| 87 | ||
| 88 | /* ─── Main ─── */ | |
| 89 | ||
| 90 | int main(int argc, char** argv) | |
| 91 | { | |
| 92 | cl_int err; | |
| 93 | ||
| 94 | printf("=== GPU AtomSpace Pool Test ===\n\n"); | |
| 95 | ||
| 96 | /* ─── OpenCL setup ─── */ | |
| 97 | ||
| 98 | cl_platform_id platform; | |
| 99 | err = clGetPlatformIDs(1, &platform, NULL); | |
| 100 | CL_CHECK(err, "platform"); | |
| 101 | ||
| 102 | cl_device_id device; | |
| 103 | err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL); | |
| 104 | CL_CHECK(err, "device"); | |
| 105 | ||
| 106 | char dev_name[256]; | |
| 107 | clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(dev_name), dev_name, NULL); | |
| 108 | printf("GPU: %s\n", dev_name); | |
| 109 | ||
| 110 | cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err); | |
| 111 | CL_CHECK(err, "context"); | |
| 112 | ||
| 113 | cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err); | |
| 114 | CL_CHECK(err, "queue"); | |
| 115 | ||
| 116 | /* ─── Load and concatenate kernel sources ─── */ | |
| 117 | ||
| 118 | size_t ht_len, as_len; | |
| 119 | char* ht_src = read_file("opencog/gpu/gpu-hashtable.cl", &ht_len); | |
| 120 | char* as_src = read_file("opencog/gpu/gpu-atomspace.cl", &as_len); | |
| 121 | ||
| 122 | /* Concatenate: hashtable first, then atomspace */ | |
| 123 | size_t total_len = ht_len + 1 + as_len; | |
| 124 | char* combined = malloc(total_len + 1); | |
| 125 | memcpy(combined, ht_src, ht_len); | |
| 126 | combined[ht_len] = '\n'; | |
| 127 | memcpy(combined + ht_len + 1, as_src, as_len); | |
| 128 | combined[total_len] = '\0'; | |
| 129 | ||
| 130 | cl_program program = clCreateProgramWithSource(ctx, 1, | |
| 131 | (const char**)&combined, &total_len, &err); | |
| 132 | CL_CHECK(err, "create program"); | |
| 133 | ||
| 134 | /* Build with capacity defines */ | |
| 135 | char build_opts[512]; | |
| 136 | snprintf(build_opts, sizeof(build_opts), | |
| 137 | "-cl-std=CL1.2 " | |
| 138 | "-DWORD_CAPACITY=%d " | |
| 139 | "-DPAIR_CAPACITY=%d " | |
| 140 | "-DSECTION_CAPACITY=%d " | |
| 141 | "-DWORD_HT_CAPACITY=%d " | |
| 142 | "-DPAIR_HT_CAPACITY=%d " | |
| 143 | "-DSECTION_HT_CAPACITY=%d", | |
| 144 | WORD_CAPACITY, PAIR_CAPACITY, SECTION_CAPACITY, | |
| 145 | WORD_HT_CAPACITY, PAIR_HT_CAPACITY, SECTION_HT_CAPACITY); | |
| 146 | ||
| 147 | err = clBuildProgram(program, 1, &device, build_opts, NULL, NULL); | |
| 148 | if (err != CL_SUCCESS) { | |
| 149 | char log[16384]; | |
| 150 | clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, | |
| 151 | sizeof(log), log, NULL); | |
| 152 | fprintf(stderr, "Build error:\n%s\n", log); | |
| 153 | return 1; | |
| 154 | } | |
| 155 | printf("Kernels compiled successfully\n\n"); | |
| 156 | ||
| 157 | /* ─── Create kernels ─── */ | |
| 158 | ||
| 159 | cl_kernel k_word_foc = clCreateKernel(program, "word_find_or_create", &err); | |
| 160 | CL_CHECK(err, "kernel word_find_or_create"); | |
| 161 | cl_kernel k_pair_foc = clCreateKernel(program, "pair_find_or_create", &err); | |
| 162 | CL_CHECK(err, "kernel pair_find_or_create"); | |
| 163 | cl_kernel k_sec_foc = clCreateKernel(program, "section_find_or_create", &err); | |
| 164 | CL_CHECK(err, "kernel section_find_or_create"); | |
| 165 | cl_kernel k_count_pairs = clCreateKernel(program, "count_pairs", &err); | |
| 166 | CL_CHECK(err, "kernel count_pairs"); | |
| 167 | cl_kernel k_count_sec = clCreateKernel(program, "count_sections", &err); | |
| 168 | CL_CHECK(err, "kernel count_sections"); | |
| 169 | cl_kernel k_stats = clCreateKernel(program, "pool_stats", &err); | |
| 170 | CL_CHECK(err, "kernel pool_stats"); | |
| 171 | ||
| 172 | size_t local_size = 256; | |
| 173 | ||
| 174 | /* ═══ ALLOCATE GPU BUFFERS ═══ */ | |
| 175 | ||
| 176 | printf("Allocating GPU buffers...\n"); | |
| 177 | uint8_t pat_ff = 0xFF; | |
| 178 | uint8_t pat_00 = 0x00; | |
| 179 | ||
| 180 | /* Word hash table */ | |
| 181 | cl_mem wht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 182 | sizeof(uint64_t) * WORD_HT_CAPACITY, NULL, &err); | |
| 183 | CL_CHECK(err, "wht_keys"); | |
| 184 | cl_mem wht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 185 | sizeof(uint32_t) * WORD_HT_CAPACITY, NULL, &err); | |
| 186 | CL_CHECK(err, "wht_values"); | |
| 187 | ||
| 188 | /* Word pool SoA */ | |
| 189 | cl_mem word_name_hash = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 190 | sizeof(uint64_t) * WORD_CAPACITY, NULL, &err); | |
| 191 | cl_mem word_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 192 | sizeof(double) * WORD_CAPACITY, NULL, &err); | |
| 193 | cl_mem word_class_id = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 194 | sizeof(uint32_t) * WORD_CAPACITY, NULL, &err); | |
| 195 | ||
| 196 | /* Word bump allocator */ | |
| 197 | uint32_t zero = 0; | |
| 198 | cl_mem word_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 199 | sizeof(uint32_t), &zero, &err); | |
| 200 | ||
| 201 | /* Pair hash table */ | |
| 202 | cl_mem pht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 203 | sizeof(uint64_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 204 | cl_mem pht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 205 | sizeof(uint32_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 206 | ||
| 207 | /* Pair pool SoA */ | |
| 208 | cl_mem pair_word_a = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 209 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 210 | cl_mem pair_word_b = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 211 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 212 | cl_mem pair_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 213 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 214 | cl_mem pair_mi = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 215 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 216 | cl_mem pair_flags = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 217 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 218 | ||
| 219 | /* Pair bump allocator */ | |
| 220 | cl_mem pair_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 221 | sizeof(uint32_t), &zero, &err); | |
| 222 | ||
| 223 | /* Section hash table */ | |
| 224 | cl_mem sht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 225 | sizeof(uint64_t) * SECTION_HT_CAPACITY, NULL, &err); | |
| 226 | cl_mem sht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 227 | sizeof(uint32_t) * SECTION_HT_CAPACITY, NULL, &err); | |
| 228 | ||
| 229 | /* Section pool SoA */ | |
| 230 | cl_mem sec_word = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 231 | sizeof(uint32_t) * SECTION_CAPACITY, NULL, &err); | |
| 232 | cl_mem sec_disjunct_hash = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 233 | sizeof(uint64_t) * SECTION_CAPACITY, NULL, &err); | |
| 234 | cl_mem sec_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 235 | sizeof(double) * SECTION_CAPACITY, NULL, &err); | |
| 236 | ||
| 237 | /* Section bump allocator */ | |
| 238 | cl_mem sec_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 239 | sizeof(uint32_t), &zero, &err); | |
| 240 | ||
| 241 | /* Stats output */ | |
| 242 | cl_mem d_stats = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 243 | sizeof(uint32_t) * 3, NULL, &err); | |
| 244 | ||
| 245 | /* Initialize hash tables to empty */ | |
| 246 | clEnqueueFillBuffer(queue, wht_keys, &pat_ff, 1, 0, | |
| 247 | sizeof(uint64_t) * WORD_HT_CAPACITY, 0, NULL, NULL); | |
| 248 | clEnqueueFillBuffer(queue, wht_values, &pat_ff, 1, 0, | |
| 249 | sizeof(uint32_t) * WORD_HT_CAPACITY, 0, NULL, NULL); | |
| 250 | clEnqueueFillBuffer(queue, pht_keys, &pat_ff, 1, 0, | |
| 251 | sizeof(uint64_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 252 | clEnqueueFillBuffer(queue, pht_values, &pat_ff, 1, 0, | |
| 253 | sizeof(uint32_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 254 | clEnqueueFillBuffer(queue, sht_keys, &pat_ff, 1, 0, | |
| 255 | sizeof(uint64_t) * SECTION_HT_CAPACITY, 0, NULL, NULL); | |
| 256 | clEnqueueFillBuffer(queue, sht_values, &pat_ff, 1, 0, | |
| 257 | sizeof(uint32_t) * SECTION_HT_CAPACITY, 0, NULL, NULL); | |
| 258 | ||
| 259 | /* Initialize pool arrays to 0 */ | |
| 260 | clEnqueueFillBuffer(queue, word_count, &pat_00, 1, 0, | |
| 261 | sizeof(double) * WORD_CAPACITY, 0, NULL, NULL); | |
| 262 | clEnqueueFillBuffer(queue, word_class_id, &pat_00, 1, 0, | |
| 263 | sizeof(uint32_t) * WORD_CAPACITY, 0, NULL, NULL); | |
| 264 | clEnqueueFillBuffer(queue, pair_count, &pat_00, 1, 0, | |
| 265 | sizeof(double) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 266 | clEnqueueFillBuffer(queue, pair_mi, &pat_00, 1, 0, | |
| 267 | sizeof(double) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 268 | clEnqueueFillBuffer(queue, pair_flags, &pat_00, 1, 0, | |
| 269 | sizeof(uint32_t) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 270 | clEnqueueFillBuffer(queue, sec_count, &pat_00, 1, 0, | |
| 271 | sizeof(double) * SECTION_CAPACITY, 0, NULL, NULL); | |
| 272 | clFinish(queue); | |
| 273 | ||
| 274 | /* Calculate total GPU memory */ | |
| 275 | size_t total_mem = 0; | |
| 276 | total_mem += sizeof(uint64_t) * WORD_HT_CAPACITY; /* wht_keys */ | |
| 277 | total_mem += sizeof(uint32_t) * WORD_HT_CAPACITY; /* wht_values */ | |
| 278 | total_mem += sizeof(uint64_t) * WORD_CAPACITY; /* word_name_hash */ | |
| 279 | total_mem += sizeof(double) * WORD_CAPACITY; /* word_count */ | |
| 280 | total_mem += sizeof(uint32_t) * WORD_CAPACITY; /* word_class_id */ | |
| 281 | total_mem += sizeof(uint64_t) * PAIR_HT_CAPACITY; /* pht_keys */ | |
| 282 | total_mem += sizeof(uint32_t) * PAIR_HT_CAPACITY; /* pht_values */ | |
| 283 | total_mem += sizeof(uint32_t) * PAIR_CAPACITY; /* pair_word_a */ | |
| 284 | total_mem += sizeof(uint32_t) * PAIR_CAPACITY; /* pair_word_b */ | |
| 285 | total_mem += sizeof(double) * PAIR_CAPACITY; /* pair_count */ | |
| 286 | total_mem += sizeof(double) * PAIR_CAPACITY; /* pair_mi */ | |
| 287 | total_mem += sizeof(uint32_t) * PAIR_CAPACITY; /* pair_flags */ | |
| 288 | total_mem += sizeof(uint64_t) * SECTION_HT_CAPACITY;/* sht_keys */ | |
| 289 | total_mem += sizeof(uint32_t) * SECTION_HT_CAPACITY;/* sht_values */ | |
| 290 | total_mem += sizeof(uint32_t) * SECTION_CAPACITY; /* sec_word */ | |
| 291 | total_mem += sizeof(uint64_t) * SECTION_CAPACITY; /* sec_disjunct_hash */ | |
| 292 | total_mem += sizeof(double) * SECTION_CAPACITY; /* sec_count */ | |
| 293 | printf("Total GPU memory: %zu MB\n\n", total_mem / (1024*1024)); | |
| 294 | ||
| 295 | /* ═══ TEST 1: Create words ═══ */ | |
| 296 | ||
| 297 | printf("--- Test 1: Create words ---\n"); | |
| 298 | ||
| 299 | const char* test_words[] = { | |
| 300 | "the", "of", "and", "to", "a", "in", "was", "he", "she", "it", | |
| 301 | "that", "is", "for", "his", "with", "her", "had", "not", "at", "on", | |
| 302 | /* Duplicates to test dedup */ | |
| 303 | "the", "of", "and", "the", "he", "she" | |
| 304 | }; | |
| 305 | int num_words_in = 26; | |
| 306 | int num_unique_words = 20; | |
| 307 | ||
| 308 | uint64_t* h_word_hashes = malloc(sizeof(uint64_t) * num_words_in); | |
| 309 | for (int i = 0; i < num_words_in; i++) { | |
| 310 | h_word_hashes[i] = hash_word(test_words[i]); | |
| 311 | } | |
| 312 | ||
| 313 | cl_mem d_word_hashes = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 314 | sizeof(uint64_t) * num_words_in, h_word_hashes, &err); | |
| 315 | uint32_t* h_word_indices = malloc(sizeof(uint32_t) * num_words_in); | |
| 316 | cl_mem d_word_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 317 | sizeof(uint32_t) * num_words_in, NULL, &err); | |
| 318 | ||
| 319 | cl_uint nw = num_words_in; | |
| 320 | clSetKernelArg(k_word_foc, 0, sizeof(cl_mem), &wht_keys); | |
| 321 | clSetKernelArg(k_word_foc, 1, sizeof(cl_mem), &wht_values); | |
| 322 | clSetKernelArg(k_word_foc, 2, sizeof(cl_mem), &word_name_hash); | |
| 323 | clSetKernelArg(k_word_foc, 3, sizeof(cl_mem), &word_count); | |
| 324 | clSetKernelArg(k_word_foc, 4, sizeof(cl_mem), &word_class_id); | |
| 325 | clSetKernelArg(k_word_foc, 5, sizeof(cl_mem), &word_next_free); | |
| 326 | clSetKernelArg(k_word_foc, 6, sizeof(cl_mem), &d_word_hashes); | |
| 327 | clSetKernelArg(k_word_foc, 7, sizeof(cl_mem), &d_word_out); | |
| 328 | clSetKernelArg(k_word_foc, 8, sizeof(cl_uint), &nw); | |
| 329 | ||
| 330 | double t0 = now_ms(); | |
| 331 | size_t gs = ((num_words_in + local_size - 1) / local_size) * local_size; | |
| 332 | err = clEnqueueNDRangeKernel(queue, k_word_foc, 1, NULL, | |
| 333 | &gs, &local_size, 0, NULL, NULL); | |
| 334 | CL_CHECK(err, "enqueue word_foc"); | |
| 335 | clFinish(queue); | |
| 336 | double t1 = now_ms(); | |
| 337 | ||
| 338 | clEnqueueReadBuffer(queue, d_word_out, CL_TRUE, 0, | |
| 339 | sizeof(uint32_t) * num_words_in, h_word_indices, 0, NULL, NULL); | |
| 340 | ||
| 341 | printf(" Created %d words (with %d dups) in %.2f ms\n", | |
| 342 | num_words_in, num_words_in - num_unique_words, t1-t0); | |
| 343 | ||
| 344 | /* Check dedup: "the" appears at indices 0, 20, 23 — should all get same pool index */ | |
| 345 | int dedup_ok = (h_word_indices[0] == h_word_indices[20] && | |
| 346 | h_word_indices[0] == h_word_indices[23]); | |
| 347 | printf(" 'the' dedup: idx[0]=%u idx[20]=%u idx[23]=%u %s\n", | |
| 348 | h_word_indices[0], h_word_indices[20], h_word_indices[23], | |
| 349 | dedup_ok ? "PASS" : "FAIL"); | |
| 350 | ||
| 351 | /* "he"=7, "he"=24 */ | |
| 352 | int dedup2 = (h_word_indices[7] == h_word_indices[24]); | |
| 353 | printf(" 'he' dedup: idx[7]=%u idx[24]=%u %s\n", | |
| 354 | h_word_indices[7], h_word_indices[24], | |
| 355 | dedup2 ? "PASS" : "FAIL"); | |
| 356 | ||
| 357 | /* Check pool stats */ | |
| 358 | clSetKernelArg(k_stats, 0, sizeof(cl_mem), &word_next_free); | |
| 359 | clSetKernelArg(k_stats, 1, sizeof(cl_mem), &pair_next_free); | |
| 360 | clSetKernelArg(k_stats, 2, sizeof(cl_mem), &sec_next_free); | |
| 361 | clSetKernelArg(k_stats, 3, sizeof(cl_mem), &d_stats); | |
| 362 | ||
| 363 | size_t one = 1; | |
| 364 | clEnqueueNDRangeKernel(queue, k_stats, 1, NULL, &one, &one, 0, NULL, NULL); | |
| 365 | uint32_t stats[3]; | |
| 366 | clEnqueueReadBuffer(queue, d_stats, CL_TRUE, 0, sizeof(stats), stats, 0, NULL, NULL); | |
| 367 | ||
| 368 | printf(" Pool: %u words, %u pairs, %u sections\n", stats[0], stats[1], stats[2]); | |
| 369 | printf(" %s\n\n", (stats[0] == num_unique_words) ? "PASS" : "FAIL"); | |
| 370 | ||
| 371 | /* ═══ TEST 2: Create pairs ═══ */ | |
| 372 | ||
| 373 | printf("--- Test 2: Create pairs ---\n"); | |
| 374 | ||
| 375 | /* Create pairs for a sentence: "the cat was on the mat" | |
| 376 | * Words: the(0), cat(?), was(6), on(19), mat(?) | |
| 377 | * We need to create cat and mat first */ | |
| 378 | uint64_t extra_hashes[2] = { hash_word("cat"), hash_word("mat") }; | |
| 379 | cl_mem d_extra = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 380 | sizeof(uint64_t) * 2, extra_hashes, &err); | |
| 381 | cl_mem d_extra_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 382 | sizeof(uint32_t) * 2, NULL, &err); | |
| 383 | ||
| 384 | cl_uint n_extra = 2; | |
| 385 | clSetKernelArg(k_word_foc, 6, sizeof(cl_mem), &d_extra); | |
| 386 | clSetKernelArg(k_word_foc, 7, sizeof(cl_mem), &d_extra_out); | |
| 387 | clSetKernelArg(k_word_foc, 8, sizeof(cl_uint), &n_extra); | |
| 388 | gs = ((2 + local_size - 1) / local_size) * local_size; | |
| 389 | clEnqueueNDRangeKernel(queue, k_word_foc, 1, NULL, &gs, &local_size, 0, NULL, NULL); | |
| 390 | uint32_t extra_indices[2]; | |
| 391 | clEnqueueReadBuffer(queue, d_extra_out, CL_TRUE, 0, | |
| 392 | sizeof(uint32_t) * 2, extra_indices, 0, NULL, NULL); | |
| 393 | ||
| 394 | uint32_t idx_the = h_word_indices[0]; | |
| 395 | uint32_t idx_cat = extra_indices[0]; | |
| 396 | uint32_t idx_was = h_word_indices[6]; | |
| 397 | uint32_t idx_on = h_word_indices[19]; | |
| 398 | uint32_t idx_mat = extra_indices[1]; | |
| 399 | ||
| 400 | printf(" Word indices: the=%u cat=%u was=%u on=%u mat=%u\n", | |
| 401 | idx_the, idx_cat, idx_was, idx_on, idx_mat); | |
| 402 | ||
| 403 | /* Create pairs within window=2: (the,cat) (the,was) (cat,was) (cat,on) (was,on) (was,the_2) | |
| 404 | * Plus (on,the_2) (on,mat) (the_2,mat) */ | |
| 405 | int num_pairs_in = 9; | |
| 406 | uint32_t h_pair_a[] = {idx_the, idx_the, idx_cat, idx_cat, idx_was, idx_was, idx_on, idx_on, idx_the}; | |
| 407 | uint32_t h_pair_b[] = {idx_cat, idx_was, idx_was, idx_on, idx_on, idx_the, idx_the,idx_mat,idx_mat}; | |
| 408 | ||
| 409 | /* Add duplicates to test dedup */ | |
| 410 | int num_with_dups = 12; | |
| 411 | uint32_t h_pair_a2[12], h_pair_b2[12]; | |
| 412 | memcpy(h_pair_a2, h_pair_a, sizeof(uint32_t) * 9); | |
| 413 | memcpy(h_pair_b2, h_pair_b, sizeof(uint32_t) * 9); | |
| 414 | /* Duplicate first 3 pairs */ | |
| 415 | h_pair_a2[9] = idx_the; h_pair_b2[9] = idx_cat; | |
| 416 | h_pair_a2[10] = idx_the; h_pair_b2[10] = idx_was; | |
| 417 | h_pair_a2[11] = idx_cat; h_pair_b2[11] = idx_was; | |
| 418 | ||
| 419 | cl_mem d_pair_a = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 420 | sizeof(uint32_t) * num_with_dups, h_pair_a2, &err); | |
| 421 | cl_mem d_pair_b = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 422 | sizeof(uint32_t) * num_with_dups, h_pair_b2, &err); | |
| 423 | cl_mem d_pair_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 424 | sizeof(uint32_t) * num_with_dups, NULL, &err); | |
| 425 | ||
| 426 | cl_uint np = num_with_dups; | |
| 427 | clSetKernelArg(k_pair_foc, 0, sizeof(cl_mem), &pht_keys); | |
| 428 | clSetKernelArg(k_pair_foc, 1, sizeof(cl_mem), &pht_values); | |
| 429 | clSetKernelArg(k_pair_foc, 2, sizeof(cl_mem), &pair_word_a); | |
| 430 | clSetKernelArg(k_pair_foc, 3, sizeof(cl_mem), &pair_word_b); | |
| 431 | clSetKernelArg(k_pair_foc, 4, sizeof(cl_mem), &pair_count); | |
| 432 | clSetKernelArg(k_pair_foc, 5, sizeof(cl_mem), &pair_mi); | |
| 433 | clSetKernelArg(k_pair_foc, 6, sizeof(cl_mem), &pair_flags); | |
| 434 | clSetKernelArg(k_pair_foc, 7, sizeof(cl_mem), &pair_next_free); | |
| 435 | clSetKernelArg(k_pair_foc, 8, sizeof(cl_mem), &d_pair_a); | |
| 436 | clSetKernelArg(k_pair_foc, 9, sizeof(cl_mem), &d_pair_b); | |
| 437 | clSetKernelArg(k_pair_foc, 10, sizeof(cl_mem), &d_pair_out); | |
| 438 | clSetKernelArg(k_pair_foc, 11, sizeof(cl_uint), &np); | |
| 439 | ||
| 440 | t0 = now_ms(); | |
| 441 | gs = ((num_with_dups + local_size - 1) / local_size) * local_size; | |
| 442 | clEnqueueNDRangeKernel(queue, k_pair_foc, 1, NULL, &gs, &local_size, 0, NULL, NULL); | |
| 443 | clFinish(queue); | |
| 444 | t1 = now_ms(); | |
| 445 | ||
| 446 | uint32_t h_pair_indices[12]; | |
| 447 | clEnqueueReadBuffer(queue, d_pair_out, CL_TRUE, 0, | |
| 448 | sizeof(uint32_t) * num_with_dups, h_pair_indices, 0, NULL, NULL); | |
| 449 | ||
| 450 | /* Pair dedup: indices 0 and 9 should match (the,cat) */ | |
| 451 | int pair_dedup = (h_pair_indices[0] == h_pair_indices[9] && | |
| 452 | h_pair_indices[1] == h_pair_indices[10] && | |
| 453 | h_pair_indices[2] == h_pair_indices[11]); | |
| 454 | printf(" Created %d pairs (3 dups) in %.2f ms\n", num_with_dups, t1-t0); | |
| 455 | printf(" Pair dedup: %s\n", pair_dedup ? "PASS" : "FAIL"); | |
| 456 | ||
| 457 | /* Note: pair(the,was) and pair(was,the) should be the same (canonical order) */ | |
| 458 | int canon = (h_pair_indices[1] == h_pair_indices[5]); | |
| 459 | printf(" Canonical order (the,was)==(was,the): idx=%u==%u %s\n", | |
| 460 | h_pair_indices[1], h_pair_indices[5], canon ? "PASS" : "FAIL"); | |
| 461 | ||
| 462 | clEnqueueNDRangeKernel(queue, k_stats, 1, NULL, &one, &one, 0, NULL, NULL); | |
| 463 | clEnqueueReadBuffer(queue, d_stats, CL_TRUE, 0, sizeof(stats), stats, 0, NULL, NULL); | |
| 464 | /* 9 pairs but (the,was) and (was,the) deduplicate to same, and | |
| 465 | * (on,the) and (the,on) too. Let's count unique: */ | |
| 466 | printf(" Pool: %u words, %u pairs, %u sections\n", stats[0], stats[1], stats[2]); | |
| 467 | ||
| 468 | int num_unique_pairs = stats[1]; | |
| 469 | printf(" %s\n\n", (num_unique_pairs > 0 && num_unique_pairs <= num_pairs_in) ? "PASS" : "FAIL"); | |
| 470 | ||
| 471 | /* ═══ TEST 3: Create sections ═══ */ | |
| 472 | ||
| 473 | printf("--- Test 3: Create sections ---\n"); | |
| 474 | ||
| 475 | /* Simulate sections: word + disjunct hash */ | |
| 476 | int num_sections = 8; | |
| 477 | uint32_t h_sec_words[] = {idx_the, idx_the, idx_cat, idx_cat, idx_was, idx_was, idx_on, idx_mat}; | |
| 478 | uint64_t h_sec_dhash[] = { | |
| 479 | 0x1111111111111111ULL, /* the: cat+ */ | |
| 480 | 0x2222222222222222ULL, /* the: was+ */ | |
| 481 | 0x3333333333333333ULL, /* cat: the- was+ */ | |
| 482 | 0x4444444444444444ULL, /* cat: was- on+ */ | |
| 483 | 0x5555555555555555ULL, /* was: cat- on+ */ | |
| 484 | 0x2222222222222222ULL, /* was: same disjunct as "the: was+" — different word, different section */ | |
| 485 | 0x6666666666666666ULL, /* on: was- the+ */ | |
| 486 | 0x7777777777777777ULL, /* mat: on- the+ */ | |
| 487 | }; | |
| 488 | ||
| 489 | cl_mem d_sec_w = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 490 | sizeof(uint32_t) * num_sections, h_sec_words, &err); | |
| 491 | cl_mem d_sec_d = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 492 | sizeof(uint64_t) * num_sections, h_sec_dhash, &err); | |
| 493 | cl_mem d_sec_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 494 | sizeof(uint32_t) * num_sections, NULL, &err); | |
| 495 | ||
| 496 | cl_uint ns = num_sections; | |
| 497 | clSetKernelArg(k_sec_foc, 0, sizeof(cl_mem), &sht_keys); | |
| 498 | clSetKernelArg(k_sec_foc, 1, sizeof(cl_mem), &sht_values); | |
| 499 | clSetKernelArg(k_sec_foc, 2, sizeof(cl_mem), &sec_word); | |
| 500 | clSetKernelArg(k_sec_foc, 3, sizeof(cl_mem), &sec_disjunct_hash); | |
| 501 | clSetKernelArg(k_sec_foc, 4, sizeof(cl_mem), &sec_count); | |
| 502 | clSetKernelArg(k_sec_foc, 5, sizeof(cl_mem), &sec_next_free); | |
| 503 | clSetKernelArg(k_sec_foc, 6, sizeof(cl_mem), &d_sec_w); | |
| 504 | clSetKernelArg(k_sec_foc, 7, sizeof(cl_mem), &d_sec_d); | |
| 505 | clSetKernelArg(k_sec_foc, 8, sizeof(cl_mem), &d_sec_out); | |
| 506 | clSetKernelArg(k_sec_foc, 9, sizeof(cl_uint), &ns); | |
| 507 | ||
| 508 | t0 = now_ms(); | |
| 509 | gs = ((num_sections + local_size - 1) / local_size) * local_size; | |
| 510 | clEnqueueNDRangeKernel(queue, k_sec_foc, 1, NULL, &gs, &local_size, 0, NULL, NULL); | |
| 511 | clFinish(queue); | |
| 512 | t1 = now_ms(); | |
| 513 | ||
| 514 | uint32_t h_sec_indices[8]; | |
| 515 | clEnqueueReadBuffer(queue, d_sec_out, CL_TRUE, 0, | |
| 516 | sizeof(uint32_t) * num_sections, h_sec_indices, 0, NULL, NULL); | |
| 517 | ||
| 518 | printf(" Created %d sections in %.2f ms\n", num_sections, t1-t0); | |
| 519 | ||
| 520 | /* Section(the, 0x222..) != Section(was, 0x222..) — different word, different section */ | |
| 521 | int sec_diff = (h_sec_indices[1] != h_sec_indices[5]); | |
| 522 | printf(" Different word same disjunct = different section: %s\n", | |
| 523 | sec_diff ? "PASS" : "FAIL"); | |
| 524 | ||
| 525 | clEnqueueNDRangeKernel(queue, k_stats, 1, NULL, &one, &one, 0, NULL, NULL); | |
| 526 | clEnqueueReadBuffer(queue, d_stats, CL_TRUE, 0, sizeof(stats), stats, 0, NULL, NULL); | |
| 527 | printf(" Pool: %u words, %u pairs, %u sections\n", stats[0], stats[1], stats[2]); | |
| 528 | printf(" %s\n\n", (stats[2] == (uint32_t)num_sections) ? "PASS" : "FAIL"); | |
| 529 | ||
| 530 | /* ═══ TEST 4: Count pairs ═══ */ | |
| 531 | ||
| 532 | printf("--- Test 4: Count pairs (atomic double increment) ---\n"); | |
| 533 | ||
| 534 | /* Count pair 0 (the,cat) 100 times */ | |
| 535 | int count_n = 100; | |
| 536 | uint32_t* h_count_indices = malloc(sizeof(uint32_t) * count_n); | |
| 537 | for (int i = 0; i < count_n; i++) { | |
| 538 | h_count_indices[i] = h_pair_indices[0]; /* (the,cat) */ | |
| 539 | } | |
| 540 | ||
| 541 | cl_mem d_count_idx = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 542 | sizeof(uint32_t) * count_n, h_count_indices, &err); | |
| 543 | ||
| 544 | cl_uint cn = count_n; | |
| 545 | clSetKernelArg(k_count_pairs, 0, sizeof(cl_mem), &pair_count); | |
| 546 | clSetKernelArg(k_count_pairs, 1, sizeof(cl_mem), &pair_word_a); | |
| 547 | clSetKernelArg(k_count_pairs, 2, sizeof(cl_mem), &pair_word_b); | |
| 548 | clSetKernelArg(k_count_pairs, 3, sizeof(cl_mem), &word_count); | |
| 549 | clSetKernelArg(k_count_pairs, 4, sizeof(cl_mem), &pair_flags); | |
| 550 | clSetKernelArg(k_count_pairs, 5, sizeof(cl_mem), &d_count_idx); | |
| 551 | clSetKernelArg(k_count_pairs, 6, sizeof(cl_uint), &cn); | |
| 552 | ||
| 553 | t0 = now_ms(); | |
| 554 | gs = ((count_n + local_size - 1) / local_size) * local_size; | |
| 555 | clEnqueueNDRangeKernel(queue, k_count_pairs, 1, NULL, &gs, &local_size, 0, NULL, NULL); | |
| 556 | clFinish(queue); | |
| 557 | t1 = now_ms(); | |
| 558 | ||
| 559 | /* Read back the pair count and word marginals */ | |
| 560 | double h_pair_c; | |
| 561 | clEnqueueReadBuffer(queue, pair_count, CL_TRUE, | |
| 562 | sizeof(double) * h_pair_indices[0], sizeof(double), &h_pair_c, 0, NULL, NULL); | |
| 563 | ||
| 564 | double h_wc_the, h_wc_cat; | |
| 565 | clEnqueueReadBuffer(queue, word_count, CL_TRUE, | |
| 566 | sizeof(double) * idx_the, sizeof(double), &h_wc_the, 0, NULL, NULL); | |
| 567 | clEnqueueReadBuffer(queue, word_count, CL_TRUE, | |
| 568 | sizeof(double) * idx_cat, sizeof(double), &h_wc_cat, 0, NULL, NULL); | |
| 569 | ||
| 570 | printf(" Counted pair(the,cat) %d times in %.2f ms\n", count_n, t1-t0); | |
| 571 | printf(" pair_count = %.1f (expected %.1f) %s\n", | |
| 572 | h_pair_c, (double)count_n, | |
| 573 | (fabs(h_pair_c - count_n) < 0.5) ? "PASS" : "FAIL"); | |
| 574 | printf(" word_count[the] = %.1f word_count[cat] = %.1f (both expect %.1f) %s\n", | |
| 575 | h_wc_the, h_wc_cat, (double)count_n, | |
| 576 | (fabs(h_wc_the - count_n) < 0.5 && fabs(h_wc_cat - count_n) < 0.5) | |
| 577 | ? "PASS" : "FAIL"); | |
| 578 | ||
| 579 | /* Check dirty flag */ | |
| 580 | uint32_t h_flag; | |
| 581 | clEnqueueReadBuffer(queue, pair_flags, CL_TRUE, | |
| 582 | sizeof(uint32_t) * h_pair_indices[0], sizeof(uint32_t), &h_flag, 0, NULL, NULL); | |
| 583 | printf(" Dirty flag = %u (expected 1) %s\n\n", h_flag, (h_flag == 1) ? "PASS" : "FAIL"); | |
| 584 | ||
| 585 | /* ═══ TEST 5: Count sections ═══ */ | |
| 586 | ||
| 587 | printf("--- Test 5: Count sections ---\n"); | |
| 588 | ||
| 589 | /* Count section 0 (the: cat+) 50 times */ | |
| 590 | int sec_count_n = 50; | |
| 591 | uint32_t* h_sec_cnt_idx = malloc(sizeof(uint32_t) * sec_count_n); | |
| 592 | for (int i = 0; i < sec_count_n; i++) { | |
| 593 | h_sec_cnt_idx[i] = h_sec_indices[0]; | |
| 594 | } | |
| 595 | ||
| 596 | cl_mem d_sec_cnt_idx = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 597 | sizeof(uint32_t) * sec_count_n, h_sec_cnt_idx, &err); | |
| 598 | ||
| 599 | cl_uint scn = sec_count_n; | |
| 600 | clSetKernelArg(k_count_sec, 0, sizeof(cl_mem), &sec_count); | |
| 601 | clSetKernelArg(k_count_sec, 1, sizeof(cl_mem), &d_sec_cnt_idx); | |
| 602 | clSetKernelArg(k_count_sec, 2, sizeof(cl_uint), &scn); | |
| 603 | ||
| 604 | t0 = now_ms(); | |
| 605 | gs = ((sec_count_n + local_size - 1) / local_size) * local_size; | |
| 606 | clEnqueueNDRangeKernel(queue, k_count_sec, 1, NULL, &gs, &local_size, 0, NULL, NULL); | |
| 607 | clFinish(queue); | |
| 608 | t1 = now_ms(); | |
| 609 | ||
| 610 | double h_sec_c; | |
| 611 | clEnqueueReadBuffer(queue, sec_count, CL_TRUE, | |
| 612 | sizeof(double) * h_sec_indices[0], sizeof(double), &h_sec_c, 0, NULL, NULL); | |
| 613 | ||
| 614 | printf(" Counted section 0 %d times in %.2f ms\n", sec_count_n, t1-t0); | |
| 615 | printf(" sec_count = %.1f (expected %.1f) %s\n\n", | |
| 616 | h_sec_c, (double)sec_count_n, | |
| 617 | (fabs(h_sec_c - sec_count_n) < 0.5) ? "PASS" : "FAIL"); | |
| 618 | ||
| 619 | /* ═══ TEST 6: Bulk performance ═══ */ | |
| 620 | ||
| 621 | printf("--- Test 6: Bulk performance ---\n"); | |
| 622 | ||
| 623 | /* Create 100K words */ | |
| 624 | int bulk_words = 100000; | |
| 625 | uint64_t* h_bulk_hashes = malloc(sizeof(uint64_t) * bulk_words); | |
| 626 | uint64_t rng = 0xDEADBEEFCAFEBABEULL; | |
| 627 | for (int i = 0; i < bulk_words; i++) { | |
| 628 | rng += 0x9E3779B97F4A7C15ULL; | |
| 629 | uint64_t z = rng; | |
| 630 | z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; | |
| 631 | z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; | |
| 632 | z = z ^ (z >> 31); | |
| 633 | if (z == HT_EMPTY_KEY) z = 0; | |
| 634 | h_bulk_hashes[i] = z; | |
| 635 | } | |
| 636 | ||
| 637 | cl_mem d_bulk = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 638 | sizeof(uint64_t) * bulk_words, h_bulk_hashes, &err); | |
| 639 | cl_mem d_bulk_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 640 | sizeof(uint32_t) * bulk_words, NULL, &err); | |
| 641 | ||
| 642 | cl_uint bw = bulk_words; | |
| 643 | clSetKernelArg(k_word_foc, 6, sizeof(cl_mem), &d_bulk); | |
| 644 | clSetKernelArg(k_word_foc, 7, sizeof(cl_mem), &d_bulk_out); | |
| 645 | clSetKernelArg(k_word_foc, 8, sizeof(cl_uint), &bw); | |
| 646 | ||
| 647 | t0 = now_ms(); | |
| 648 | gs = ((bulk_words + local_size - 1) / local_size) * local_size; | |
| 649 | clEnqueueNDRangeKernel(queue, k_word_foc, 1, NULL, &gs, &local_size, 0, NULL, NULL); | |
| 650 | clFinish(queue); | |
| 651 | t1 = now_ms(); | |
| 652 | printf(" 100K words created in %.1f ms (%.1f M/sec)\n", | |
| 653 | t1-t0, bulk_words / ((t1-t0) / 1000.0) / 1e6); | |
| 654 | ||
| 655 | /* Create 1M pairs from random word indices */ | |
| 656 | int bulk_pairs = 1000000; | |
| 657 | uint32_t* h_bulk_pa = malloc(sizeof(uint32_t) * bulk_pairs); | |
| 658 | uint32_t* h_bulk_pb = malloc(sizeof(uint32_t) * bulk_pairs); | |
| 659 | for (int i = 0; i < bulk_pairs; i++) { | |
| 660 | rng += 0x9E3779B97F4A7C15ULL; | |
| 661 | h_bulk_pa[i] = (uint32_t)(rng >> 32) % bulk_words; | |
| 662 | h_bulk_pb[i] = (uint32_t)(rng & 0xFFFFFFFF) % bulk_words; | |
| 663 | } | |
| 664 | ||
| 665 | cl_mem d_bulk_pa = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 666 | sizeof(uint32_t) * bulk_pairs, h_bulk_pa, &err); | |
| 667 | cl_mem d_bulk_pb = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 668 | sizeof(uint32_t) * bulk_pairs, h_bulk_pb, &err); | |
| 669 | cl_mem d_bulk_po = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 670 | sizeof(uint32_t) * bulk_pairs, NULL, &err); | |
| 671 | ||
| 672 | cl_uint bp = bulk_pairs; | |
| 673 | clSetKernelArg(k_pair_foc, 8, sizeof(cl_mem), &d_bulk_pa); | |
| 674 | clSetKernelArg(k_pair_foc, 9, sizeof(cl_mem), &d_bulk_pb); | |
| 675 | clSetKernelArg(k_pair_foc, 10, sizeof(cl_mem), &d_bulk_po); | |
| 676 | clSetKernelArg(k_pair_foc, 11, sizeof(cl_uint), &bp); | |
| 677 | ||
| 678 | t0 = now_ms(); | |
| 679 | gs = ((bulk_pairs + local_size - 1) / local_size) * local_size; | |
| 680 | clEnqueueNDRangeKernel(queue, k_pair_foc, 1, NULL, &gs, &local_size, 0, NULL, NULL); | |
| 681 | clFinish(queue); | |
| 682 | t1 = now_ms(); | |
| 683 | printf(" 1M pairs created in %.1f ms (%.1f M/sec)\n", | |
| 684 | t1-t0, bulk_pairs / ((t1-t0) / 1000.0) / 1e6); | |
| 685 | ||
| 686 | /* Final stats */ | |
| 687 | clEnqueueNDRangeKernel(queue, k_stats, 1, NULL, &one, &one, 0, NULL, NULL); | |
| 688 | clEnqueueReadBuffer(queue, d_stats, CL_TRUE, 0, sizeof(stats), stats, 0, NULL, NULL); | |
| 689 | printf(" Final pool: %u words, %u pairs, %u sections\n\n", stats[0], stats[1], stats[2]); | |
| 690 | ||
| 691 | printf("=== All tests complete ===\n"); | |
| 692 | ||
| 693 | /* ─── Cleanup ─── */ | |
| 694 | ||
| 695 | clReleaseMemObject(wht_keys); | |
| 696 | clReleaseMemObject(wht_values); | |
| 697 | clReleaseMemObject(word_name_hash); | |
| 698 | clReleaseMemObject(word_count); | |
| 699 | clReleaseMemObject(word_class_id); | |
| 700 | clReleaseMemObject(word_next_free); | |
| 701 | clReleaseMemObject(pht_keys); | |
| 702 | clReleaseMemObject(pht_values); | |
| 703 | clReleaseMemObject(pair_word_a); | |
| 704 | clReleaseMemObject(pair_word_b); | |
| 705 | clReleaseMemObject(pair_count); | |
| 706 | clReleaseMemObject(pair_mi); | |
| 707 | clReleaseMemObject(pair_flags); | |
| 708 | clReleaseMemObject(pair_next_free); | |
| 709 | clReleaseMemObject(sht_keys); | |
| 710 | clReleaseMemObject(sht_values); | |
| 711 | clReleaseMemObject(sec_word); | |
| 712 | clReleaseMemObject(sec_disjunct_hash); | |
| 713 | clReleaseMemObject(sec_count); | |
| 714 | clReleaseMemObject(sec_next_free); | |
| 715 | clReleaseMemObject(d_stats); | |
| 716 | /* ... remaining temp buffers */ | |
| 717 | clReleaseKernel(k_word_foc); | |
| 718 | clReleaseKernel(k_pair_foc); | |
| 719 | clReleaseKernel(k_sec_foc); | |
| 720 | clReleaseKernel(k_count_pairs); | |
| 721 | clReleaseKernel(k_count_sec); | |
| 722 | clReleaseKernel(k_stats); | |
| 723 | clReleaseProgram(program); | |
| 724 | clReleaseCommandQueue(queue); | |
| 725 | clReleaseContext(ctx); | |
| 726 | ||
| 727 | free(h_word_hashes); | |
| 728 | free(h_word_indices); | |
| 729 | free(h_count_indices); | |
| 730 | free(h_sec_cnt_idx); | |
| 731 | free(h_bulk_hashes); | |
| 732 | free(h_bulk_pa); | |
| 733 | free(h_bulk_pb); | |
| 734 | free(combined); | |
| 735 | free(ht_src); | |
| 736 | free(as_src); | |
| 737 | ||
| 738 | return 0; | |
| 739 | } |
| … | … | @@ -1,639 +0,0 @@ |
| 1 | /* test-compartment-kernel.cu — Tests for hardware-native compartment kernel | |
| 2 | * | |
| 3 | * Build: | |
| 4 | * nvcc -O2 -arch=sm_75 -rdc=true -o test-compartment-kernel \ | |
| 5 | * test-compartment-kernel.cu gpu-compartment-kernel.cu -lcudadevrt -lm | |
| 6 | */ | |
| 7 | ||
| 8 | #include <cstdint> | |
| 9 | #include <cstdio> | |
| 10 | #include <cstdlib> | |
| 11 | #include <cstring> | |
| 12 | #include <cmath> | |
| 13 | #include <ctime> | |
| 14 | #include <unistd.h> | |
| 15 | #include <cuda_runtime.h> | |
| 16 | #include "gpu-learning-types.h" | |
| 17 | ||
| 18 | /* Forward declarations */ | |
| 19 | extern "C" { | |
| 20 | LearningState* ck_init(uint32_t num_words); | |
| 21 | CompartmentState* ck_init_compartments(uint32_t num_compartments, | |
| 22 | uint32_t num_words); | |
| 23 | SentenceRing* ck_init_ring(); | |
| 24 | void ck_feed_sentence(SentenceRing* ring, | |
| 25 | uint32_t* words, uint32_t length); | |
| 26 | int ck_launch(LearningState* state, | |
| 27 | CompartmentState* compartments, | |
| 28 | SentenceRing* ring, | |
| 29 | int* done_flag, int* pause_flag, | |
| 30 | uint32_t* stats_iteration, | |
| 31 | uint32_t* stats_pairs, | |
| 32 | uint32_t* stats_classes, | |
| 33 | double* stats_entropy); | |
| 34 | void ck_wait(); | |
| 35 | void ck_read_classes(LearningState* state, | |
| 36 | uint32_t* out, uint32_t n); | |
| 37 | void ck_shutdown(LearningState* state, | |
| 38 | CompartmentState* comps, | |
| 39 | SentenceRing* ring); | |
| 40 | int ck_get_num_sms(); | |
| 41 | void ck_memory_report(int num_compartments); | |
| 42 | } | |
| 43 | ||
| 44 | /* ─── Helpers ─── */ | |
| 45 | ||
| 46 | static double time_ms() { | |
| 47 | struct timespec ts; | |
| 48 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 49 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6; | |
| 50 | } | |
| 51 | ||
| 52 | static uint32_t xorshift32(uint32_t* state) { | |
| 53 | uint32_t x = *state; | |
| 54 | x ^= x << 13; | |
| 55 | x ^= x >> 17; | |
| 56 | x ^= x << 5; | |
| 57 | *state = x; | |
| 58 | return x; | |
| 59 | } | |
| 60 | ||
| 61 | /* Managed memory helpers for control flags */ | |
| 62 | struct ControlFlags { | |
| 63 | int* done_flag; | |
| 64 | int* pause_flag; | |
| 65 | uint32_t* stats_iteration; | |
| 66 | uint32_t* stats_pairs; | |
| 67 | uint32_t* stats_classes; | |
| 68 | double* stats_entropy; | |
| 69 | }; | |
| 70 | ||
| 71 | static ControlFlags alloc_flags() { | |
| 72 | ControlFlags f; | |
| 73 | cudaMallocManaged(&f.done_flag, sizeof(int)); | |
| 74 | cudaMallocManaged(&f.pause_flag, sizeof(int)); | |
| 75 | cudaMallocManaged(&f.stats_iteration, sizeof(uint32_t)); | |
| 76 | cudaMallocManaged(&f.stats_pairs, sizeof(uint32_t)); | |
| 77 | cudaMallocManaged(&f.stats_classes, sizeof(uint32_t)); | |
| 78 | cudaMallocManaged(&f.stats_entropy, sizeof(double)); | |
| 79 | *f.done_flag = 0; | |
| 80 | *f.pause_flag = 0; | |
| 81 | *f.stats_iteration = 0; | |
| 82 | *f.stats_pairs = 0; | |
| 83 | *f.stats_classes = 0; | |
| 84 | *f.stats_entropy = 0.0; | |
| 85 | return f; | |
| 86 | } | |
| 87 | ||
| 88 | static void free_flags(ControlFlags* f) { | |
| 89 | cudaFree(f->done_flag); | |
| 90 | cudaFree(f->pause_flag); | |
| 91 | cudaFree(f->stats_iteration); | |
| 92 | cudaFree(f->stats_pairs); | |
| 93 | cudaFree(f->stats_classes); | |
| 94 | cudaFree(f->stats_entropy); | |
| 95 | } | |
| 96 | ||
| 97 | /* Generate Zipf-distributed sentences */ | |
| 98 | static void gen_zipf_sentence(uint32_t* words, uint32_t len, | |
| 99 | uint32_t num_words, uint32_t* rng) { | |
| 100 | for (uint32_t i = 0; i < len; i++) { | |
| 101 | /* Approximate Zipf: lower word IDs more likely */ | |
| 102 | uint32_t r = xorshift32(rng) % 1000; | |
| 103 | uint32_t w; | |
| 104 | if (r < 200) w = xorshift32(rng) % 10; /* top 10 */ | |
| 105 | else if (r < 500) w = 10 + xorshift32(rng) % 90; /* top 100 */ | |
| 106 | else if (r < 800) w = 100 + xorshift32(rng) % 900; /* top 1000 */ | |
| 107 | else w = xorshift32(rng) % num_words; /* any */ | |
| 108 | words[i] = w % num_words; | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | /* ═══════════════════════════════════════════════════════════════ | |
| 113 | * TEST 1: Basic launch and memory | |
| 114 | * ═══════════════════════════════════════════════════════════════ */ | |
| 115 | ||
| 116 | static int test1_basic_launch() { | |
| 117 | printf("\n=== T1: Basic launch + memory report ===\n"); | |
| 118 | ||
| 119 | int num_sms = ck_get_num_sms(); | |
| 120 | printf("SMs detected: %d\n", num_sms); | |
| 121 | ||
| 122 | uint32_t num_words = 500; | |
| 123 | LearningState* state = ck_init(num_words); | |
| 124 | CompartmentState* comps = ck_init_compartments(num_sms, num_words); | |
| 125 | SentenceRing* ring = ck_init_ring(); | |
| 126 | ControlFlags flags = alloc_flags(); | |
| 127 | ||
| 128 | ck_memory_report(num_sms); | |
| 129 | ||
| 130 | /* Launch and immediately signal done */ | |
| 131 | *flags.done_flag = 0; | |
| 132 | ||
| 133 | int err = ck_launch(state, comps, ring, | |
| 134 | flags.done_flag, flags.pause_flag, | |
| 135 | flags.stats_iteration, flags.stats_pairs, | |
| 136 | flags.stats_classes, flags.stats_entropy); | |
| 137 | if (err != 0) { | |
| 138 | printf("FAIL: kernel launch failed\n"); | |
| 139 | ck_shutdown(state, comps, ring); | |
| 140 | free_flags(&flags); | |
| 141 | return 0; | |
| 142 | } | |
| 143 | ||
| 144 | /* Feed a few sentences, then stop */ | |
| 145 | uint32_t rng = 42; | |
| 146 | for (int s = 0; s < 20; s++) { | |
| 147 | uint32_t words[8]; | |
| 148 | gen_zipf_sentence(words, 8, num_words, &rng); | |
| 149 | ck_feed_sentence(ring, words, 8); | |
| 150 | } | |
| 151 | ||
| 152 | usleep(500000); /* 500ms */ | |
| 153 | *flags.done_flag = 1; | |
| 154 | ck_wait(); | |
| 155 | ||
| 156 | printf("After 20 sentences: pairs=%u, iter=%u\n", | |
| 157 | *flags.stats_pairs, *flags.stats_iteration); | |
| 158 | ||
| 159 | int pass = (*flags.stats_pairs > 0); | |
| 160 | printf("T1: %s (pairs discovered > 0)\n", pass ? "PASS" : "FAIL"); | |
| 161 | ||
| 162 | ck_shutdown(state, comps, ring); | |
| 163 | free_flags(&flags); | |
| 164 | return pass; | |
| 165 | } | |
| 166 | ||
| 167 | /* ═══════════════════════════════════════════════════════════════ | |
| 168 | * TEST 2: Sentence distribution across compartments | |
| 169 | * ═══════════════════════════════════════════════════════════════ */ | |
| 170 | ||
| 171 | static int test2_sentence_distribution() { | |
| 172 | printf("\n=== T2: Sentence distribution across SMs ===\n"); | |
| 173 | ||
| 174 | int num_sms = ck_get_num_sms(); | |
| 175 | uint32_t num_words = 500; | |
| 176 | LearningState* state = ck_init(num_words); | |
| 177 | CompartmentState* comps = ck_init_compartments(num_sms, num_words); | |
| 178 | SentenceRing* ring = ck_init_ring(); | |
| 179 | ControlFlags flags = alloc_flags(); | |
| 180 | ||
| 181 | /* Feed exactly num_sms * 10 sentences so each SM gets ~10 */ | |
| 182 | uint32_t total_sents = num_sms * 10; | |
| 183 | printf("Feeding %u sentences to %d SMs...\n", total_sents, num_sms); | |
| 184 | ||
| 185 | int err = ck_launch(state, comps, ring, | |
| 186 | flags.done_flag, flags.pause_flag, | |
| 187 | flags.stats_iteration, flags.stats_pairs, | |
| 188 | flags.stats_classes, flags.stats_entropy); | |
| 189 | if (err != 0) { | |
| 190 | printf("FAIL: kernel launch failed\n"); | |
| 191 | ck_shutdown(state, comps, ring); | |
| 192 | free_flags(&flags); | |
| 193 | return 0; | |
| 194 | } | |
| 195 | ||
| 196 | uint32_t rng = 123; | |
| 197 | for (uint32_t s = 0; s < total_sents; s++) { | |
| 198 | uint32_t words[10]; | |
| 199 | gen_zipf_sentence(words, 10, num_words, &rng); | |
| 200 | ck_feed_sentence(ring, words, 10); | |
| 201 | } | |
| 202 | ||
| 203 | /* Wait for processing */ | |
| 204 | usleep(2000000); /* 2s */ | |
| 205 | *flags.done_flag = 1; | |
| 206 | ck_wait(); | |
| 207 | ||
| 208 | printf("Total sentences tracked: %u (expected %u)\n", | |
| 209 | state->total_sentences, total_sents); | |
| 210 | printf("Pairs: %u, Iterations: %u\n", | |
| 211 | *flags.stats_pairs, *flags.stats_iteration); | |
| 212 | ||
| 213 | /* Check that total_sentences matches what we fed */ | |
| 214 | int pass = (state->total_sentences >= total_sents * 9 / 10); | |
| 215 | printf("T2: %s (total_sentences >= 90%% of fed)\n", | |
| 216 | pass ? "PASS" : "FAIL"); | |
| 217 | ||
| 218 | ck_shutdown(state, comps, ring); | |
| 219 | free_flags(&flags); | |
| 220 | return pass; | |
| 221 | } | |
| 222 | ||
| 223 | /* ═══════════════════════════════════════════════════════════════ | |
| 224 | * TEST 3: Phase-gated convergence (no false convergence) | |
| 225 | * ═══════════════════════════════════════════════════════════════ */ | |
| 226 | ||
| 227 | static int test3_phase_gated_convergence() { | |
| 228 | printf("\n=== T3: Phase-gated convergence ===\n"); | |
| 229 | ||
| 230 | int num_sms = ck_get_num_sms(); | |
| 231 | uint32_t num_words = 1000; | |
| 232 | LearningState* state = ck_init(num_words); | |
| 233 | CompartmentState* comps = ck_init_compartments(num_sms, num_words); | |
| 234 | SentenceRing* ring = ck_init_ring(); | |
| 235 | ControlFlags flags = alloc_flags(); | |
| 236 | ||
| 237 | int err = ck_launch(state, comps, ring, | |
| 238 | flags.done_flag, flags.pause_flag, | |
| 239 | flags.stats_iteration, flags.stats_pairs, | |
| 240 | flags.stats_classes, flags.stats_entropy); | |
| 241 | if (err != 0) { | |
| 242 | printf("FAIL: kernel launch failed\n"); | |
| 243 | ck_shutdown(state, comps, ring); | |
| 244 | free_flags(&flags); | |
| 245 | return 0; | |
| 246 | } | |
| 247 | ||
| 248 | /* Feed only 50 sentences — well below LL_MIN_SENTENCES (500). | |
| 249 | * The old kernel would false-converge here. | |
| 250 | * The new kernel should NOT converge. */ | |
| 251 | uint32_t rng = 456; | |
| 252 | for (int s = 0; s < 50; s++) { | |
| 253 | uint32_t words[8]; | |
| 254 | gen_zipf_sentence(words, 8, num_words, &rng); | |
| 255 | ck_feed_sentence(ring, words, 8); | |
| 256 | } | |
| 257 | ||
| 258 | /* Wait 3 seconds — old kernel converged in <1s */ | |
| 259 | usleep(3000000); | |
| 260 | ||
| 261 | int converged = *flags.done_flag; | |
| 262 | uint32_t iters = *flags.stats_iteration; | |
| 263 | ||
| 264 | printf("After 50 sentences, 3s wait: converged=%d, iters=%u\n", | |
| 265 | converged, iters); | |
| 266 | ||
| 267 | /* The kernel may converge via "data exhausted" path (idle_rounds > 50), | |
| 268 | * which is correct behavior. But it should NOT have converged via | |
| 269 | * entropy-plateau with phase gates (that would be false convergence). | |
| 270 | * Check: if converged, verify it's via data-exhausted, not phase gates. | |
| 271 | * The phase gate requires total_sentences >= 500, which 50 doesn't meet. */ | |
| 272 | *flags.done_flag = 1; | |
| 273 | ck_wait(); | |
| 274 | ||
| 275 | int pass; | |
| 276 | if (converged) { | |
| 277 | /* Verify it was data-exhausted, not phase-gated */ | |
| 278 | int was_phase_gated = (state->total_sentences >= LL_MIN_SENTENCES && | |
| 279 | state->cc_runs >= LL_MIN_CC_RUNS && | |
| 280 | state->num_classes >= LL_MIN_CLASSES); | |
| 281 | pass = !was_phase_gated; | |
| 282 | printf("T3: %s (converged=%d via %s)\n", | |
| 283 | pass ? "PASS" : "FAIL", converged, | |
| 284 | was_phase_gated ? "PHASE-GATE (BAD)" : "data-exhausted (OK)"); | |
| 285 | } else { | |
| 286 | pass = 1; | |
| 287 | printf("T3: PASS (did NOT converge with 50 sentences)\n"); | |
| 288 | } | |
| 289 | ||
| 290 | ck_shutdown(state, comps, ring); | |
| 291 | free_flags(&flags); | |
| 292 | return pass; | |
| 293 | } | |
| 294 | ||
| 295 | /* ═══════════════════════════════════════════════════════════════ | |
| 296 | * TEST 4: Real convergence with enough data | |
| 297 | * ═══════════════════════════════════════════════════════════════ */ | |
| 298 | ||
| 299 | static int test4_real_convergence() { | |
| 300 | printf("\n=== T4: Real convergence with 2000 sentences ===\n"); | |
| 301 | ||
| 302 | int num_sms = ck_get_num_sms(); | |
| 303 | uint32_t num_words = 500; | |
| 304 | LearningState* state = ck_init(num_words); | |
| 305 | CompartmentState* comps = ck_init_compartments(num_sms, num_words); | |
| 306 | SentenceRing* ring = ck_init_ring(); | |
| 307 | ControlFlags flags = alloc_flags(); | |
| 308 | ||
| 309 | int err = ck_launch(state, comps, ring, | |
| 310 | flags.done_flag, flags.pause_flag, | |
| 311 | flags.stats_iteration, flags.stats_pairs, | |
| 312 | flags.stats_classes, flags.stats_entropy); | |
| 313 | if (err != 0) { | |
| 314 | printf("FAIL: kernel launch failed\n"); | |
| 315 | ck_shutdown(state, comps, ring); | |
| 316 | free_flags(&flags); | |
| 317 | return 0; | |
| 318 | } | |
| 319 | ||
| 320 | /* Feed 2000 sentences — above LL_MIN_SENTENCES */ | |
| 321 | uint32_t rng = 789; | |
| 322 | double t_start = time_ms(); | |
| 323 | ||
| 324 | for (int s = 0; s < 2000; s++) { | |
| 325 | uint32_t len = 5 + (xorshift32(&rng) % 12); /* 5-16 words */ | |
| 326 | uint32_t words[LL_MAX_SENTENCE_LEN]; | |
| 327 | gen_zipf_sentence(words, len, num_words, &rng); | |
| 328 | ck_feed_sentence(ring, words, len); | |
| 329 | } | |
| 330 | ||
| 331 | double t_fed = time_ms(); | |
| 332 | printf("Fed 2000 sentences in %.1f ms\n", t_fed - t_start); | |
| 333 | ||
| 334 | /* Wait for convergence (timeout 60s) */ | |
| 335 | double timeout = 60000.0; | |
| 336 | uint32_t last_iter = 0; | |
| 337 | double last_report = time_ms(); | |
| 338 | ||
| 339 | while (!(*flags.done_flag)) { | |
| 340 | usleep(100000); /* 100ms */ | |
| 341 | if (time_ms() - t_start > timeout) { | |
| 342 | printf(" TIMEOUT after 60s\n"); | |
| 343 | *flags.done_flag = 1; | |
| 344 | break; | |
| 345 | } | |
| 346 | if (time_ms() - last_report > 5000.0 && | |
| 347 | *flags.stats_iteration > last_iter) { | |
| 348 | printf(" iter=%u pairs=%u classes=%u entropy=%.4f sents=%u cc_runs=%u\n", | |
| 349 | *flags.stats_iteration, *flags.stats_pairs, | |
| 350 | *flags.stats_classes, *flags.stats_entropy, | |
| 351 | state->total_sentences, state->cc_runs); | |
| 352 | last_iter = *flags.stats_iteration; | |
| 353 | last_report = time_ms(); | |
| 354 | } | |
| 355 | } | |
| 356 | ||
| 357 | ck_wait(); | |
| 358 | double t_end = time_ms(); | |
| 359 | ||
| 360 | printf("\nResults:\n"); | |
| 361 | printf(" Time: %.2f s\n", (t_end - t_start) / 1000.0); | |
| 362 | printf(" Iterations: %u\n", *flags.stats_iteration); | |
| 363 | printf(" Pairs: %u\n", *flags.stats_pairs); | |
| 364 | printf(" Classes: %u (num_classes=%u)\n", *flags.stats_classes, | |
| 365 | state->num_classes); | |
| 366 | printf(" GPU Entropy: %.4f\n", *flags.stats_entropy); | |
| 367 | printf(" Total sentences: %u\n", state->total_sentences); | |
| 368 | printf(" CC runs: %u\n", state->cc_runs); | |
| 369 | printf(" Converged: %s\n", | |
| 370 | state->iteration < LL_MAX_ITERATIONS ? "YES" : "NO (max iter)"); | |
| 371 | ||
| 372 | /* CPU-side entropy verification */ | |
| 373 | cudaDeviceSynchronize(); | |
| 374 | uint32_t nc = state->num_classes; | |
| 375 | uint32_t nw = state->word_count_u32; | |
| 376 | uint32_t* cpu_class_sizes = (uint32_t*)calloc(nc + 1, sizeof(uint32_t)); | |
| 377 | for (uint32_t w = 0; w < nw; w++) { | |
| 378 | uint32_t cid = state->word_class_id[w]; | |
| 379 | if (cid < nc) cpu_class_sizes[cid]++; | |
| 380 | } | |
| 381 | double cpu_entropy = 0.0; | |
| 382 | uint32_t total_assigned = 0; | |
| 383 | for (uint32_t c = 0; c < nc; c++) { | |
| 384 | total_assigned += cpu_class_sizes[c]; | |
| 385 | if (cpu_class_sizes[c] > 0) { | |
| 386 | double p = (double)cpu_class_sizes[c] / (double)nw; | |
| 387 | cpu_entropy -= p * log2(p); | |
| 388 | } | |
| 389 | } | |
| 390 | printf(" CPU Entropy: %.4f (nc=%u, nw=%u, assigned=%u)\n", | |
| 391 | cpu_entropy, nc, nw, total_assigned); | |
| 392 | ||
| 393 | /* Show entropy history */ | |
| 394 | printf(" Entropy history:"); | |
| 395 | for (int i = 0; i < LL_ENTROPY_WINDOW; i++) { | |
| 396 | printf(" [%d]=%.4f", i, state->entropy_history[i]); | |
| 397 | } | |
| 398 | printf("\n"); | |
| 399 | printf(" Entropy idx: %u\n", state->entropy_idx); | |
| 400 | free(cpu_class_sizes); | |
| 401 | ||
| 402 | /* Verify meaningful results */ | |
| 403 | int pass = 1; | |
| 404 | ||
| 405 | if (*flags.stats_pairs < 100) { | |
| 406 | printf(" FAIL: only %u pairs\n", *flags.stats_pairs); | |
| 407 | pass = 0; | |
| 408 | } | |
| 409 | if (*flags.stats_classes < 2) { | |
| 410 | printf(" FAIL: only %u classes\n", *flags.stats_classes); | |
| 411 | pass = 0; | |
| 412 | } | |
| 413 | if (state->total_sentences < 1800) { | |
| 414 | printf(" FAIL: only %u sentences processed\n", state->total_sentences); | |
| 415 | pass = 0; | |
| 416 | } | |
| 417 | ||
| 418 | printf("T4: %s\n", pass ? "PASS" : "FAIL"); | |
| 419 | ||
| 420 | ck_shutdown(state, comps, ring); | |
| 421 | free_flags(&flags); | |
| 422 | return pass; | |
| 423 | } | |
| 424 | ||
| 425 | /* ═══════════════════════════════════════════════════════════════ | |
| 426 | * TEST 5: MI correctness (compare with CPU reference) | |
| 427 | * ═══════════════════════════════════════════════════════════════ */ | |
| 428 | ||
| 429 | static int test5_mi_correctness() { | |
| 430 | printf("\n=== T5: MI correctness check ===\n"); | |
| 431 | ||
| 432 | int num_sms = ck_get_num_sms(); | |
| 433 | uint32_t num_words = 200; | |
| 434 | LearningState* state = ck_init(num_words); | |
| 435 | CompartmentState* comps = ck_init_compartments(num_sms, num_words); | |
| 436 | SentenceRing* ring = ck_init_ring(); | |
| 437 | ControlFlags flags = alloc_flags(); | |
| 438 | ||
| 439 | int err = ck_launch(state, comps, ring, | |
| 440 | flags.done_flag, flags.pause_flag, | |
| 441 | flags.stats_iteration, flags.stats_pairs, | |
| 442 | flags.stats_classes, flags.stats_entropy); | |
| 443 | if (err != 0) { | |
| 444 | printf("FAIL: kernel launch failed\n"); | |
| 445 | ck_shutdown(state, comps, ring); | |
| 446 | free_flags(&flags); | |
| 447 | return 0; | |
| 448 | } | |
| 449 | ||
| 450 | /* Feed 1000 sentences */ | |
| 451 | uint32_t rng = 999; | |
| 452 | for (int s = 0; s < 1000; s++) { | |
| 453 | uint32_t words[8]; | |
| 454 | gen_zipf_sentence(words, 8, num_words, &rng); | |
| 455 | ck_feed_sentence(ring, words, 8); | |
| 456 | } | |
| 457 | ||
| 458 | /* Wait for kernel to converge (data-exhausted) */ | |
| 459 | double t5_start = time_ms(); | |
| 460 | while (!(*flags.done_flag) && (time_ms() - t5_start) < 30000.0) { | |
| 461 | usleep(100000); | |
| 462 | } | |
| 463 | if (!(*flags.done_flag)) *flags.done_flag = 1; | |
| 464 | ck_wait(); | |
| 465 | cudaDeviceSynchronize(); | |
| 466 | ||
| 467 | /* Verify MI values are reasonable. | |
| 468 | * NOTE: MI was computed on intermediate counts (batch-by-batch), | |
| 469 | * so values won't exactly match CPU reference on final counts. | |
| 470 | * Instead check: no NaN/Inf, mostly positive for high-count pairs, | |
| 471 | * and MI is monotonic with count (higher count → higher MI generally). */ | |
| 472 | uint32_t num_pairs = state->pair_count_u32; | |
| 473 | printf("Checking %u pairs...\n", num_pairs); | |
| 474 | ||
| 475 | uint32_t nan_count = 0, inf_count = 0, positive = 0, negative = 0; | |
| 476 | ||
| 477 | for (uint32_t p = 0; p < num_pairs && p < LL_MAX_PAIRS; p++) { | |
| 478 | double mi = state->pair_mi[p]; | |
| 479 | if (isnan(mi)) { nan_count++; continue; } | |
| 480 | if (isinf(mi)) { inf_count++; continue; } | |
| 481 | if (mi > 0.0) positive++; | |
| 482 | else negative++; | |
| 483 | } | |
| 484 | ||
| 485 | printf("MI stats: %u positive, %u negative, %u NaN, %u Inf\n", | |
| 486 | positive, negative, nan_count, inf_count); | |
| 487 | printf("Total sentences: %u, pairs: %u\n", | |
| 488 | state->total_sentences, num_pairs); | |
| 489 | ||
| 490 | /* Checks: | |
| 491 | * 1. No NaN or Inf values | |
| 492 | * 2. More positive MI than negative (indicates real associations) | |
| 493 | * 3. At least some pairs computed */ | |
| 494 | int pass = (nan_count == 0 && inf_count == 0 && | |
| 495 | positive > negative / 2 && num_pairs > 100); | |
| 496 | printf("T5: %s\n", pass ? "PASS" : "FAIL"); | |
| 497 | ||
| 498 | ck_shutdown(state, comps, ring); | |
| 499 | free_flags(&flags); | |
| 500 | return pass; | |
| 501 | } | |
| 502 | ||
| 503 | /* ═══════════════════════════════════════════════════════════════ | |
| 504 | * TEST 6: Throughput benchmark | |
| 505 | * ═══════════════════════════════════════════════════════════════ */ | |
| 506 | ||
| 507 | static int test6_throughput() { | |
| 508 | printf("\n=== T6: Throughput benchmark ===\n"); | |
| 509 | ||
| 510 | int num_sms = ck_get_num_sms(); | |
| 511 | uint32_t num_words = 2000; | |
| 512 | LearningState* state = ck_init(num_words); | |
| 513 | CompartmentState* comps = ck_init_compartments(num_sms, num_words); | |
| 514 | SentenceRing* ring = ck_init_ring(); | |
| 515 | ControlFlags flags = alloc_flags(); | |
| 516 | ||
| 517 | ck_memory_report(num_sms); | |
| 518 | ||
| 519 | int err = ck_launch(state, comps, ring, | |
| 520 | flags.done_flag, flags.pause_flag, | |
| 521 | flags.stats_iteration, flags.stats_pairs, | |
| 522 | flags.stats_classes, flags.stats_entropy); | |
| 523 | if (err != 0) { | |
| 524 | printf("FAIL: kernel launch failed\n"); | |
| 525 | ck_shutdown(state, comps, ring); | |
| 526 | free_flags(&flags); | |
| 527 | return 0; | |
| 528 | } | |
| 529 | ||
| 530 | /* Feed 5000 sentences */ | |
| 531 | uint32_t rng = 5555; | |
| 532 | double t_start = time_ms(); | |
| 533 | ||
| 534 | for (int s = 0; s < 5000; s++) { | |
| 535 | uint32_t len = 5 + (xorshift32(&rng) % 12); | |
| 536 | uint32_t words[LL_MAX_SENTENCE_LEN]; | |
| 537 | gen_zipf_sentence(words, len, num_words, &rng); | |
| 538 | ck_feed_sentence(ring, words, len); | |
| 539 | } | |
| 540 | ||
| 541 | double t_fed = time_ms(); | |
| 542 | printf("Feed rate: %.0f sents/sec (%.1f ms)\n", | |
| 543 | 5000.0 * 1000.0 / (t_fed - t_start), t_fed - t_start); | |
| 544 | ||
| 545 | /* Wait for convergence (timeout 120s) */ | |
| 546 | double timeout = 120000.0; | |
| 547 | uint32_t last_iter = 0; | |
| 548 | double last_report = time_ms(); | |
| 549 | ||
| 550 | while (!(*flags.done_flag)) { | |
| 551 | usleep(100000); | |
| 552 | if (time_ms() - t_start > timeout) { | |
| 553 | printf(" TIMEOUT\n"); | |
| 554 | *flags.done_flag = 1; | |
| 555 | break; | |
| 556 | } | |
| 557 | if (time_ms() - last_report > 10000.0 && | |
| 558 | *flags.stats_iteration > last_iter) { | |
| 559 | printf(" iter=%u pairs=%u classes=%u entropy=%.4f sents=%u\n", | |
| 560 | *flags.stats_iteration, *flags.stats_pairs, | |
| 561 | *flags.stats_classes, *flags.stats_entropy, | |
| 562 | state->total_sentences); | |
| 563 | last_iter = *flags.stats_iteration; | |
| 564 | last_report = time_ms(); | |
| 565 | } | |
| 566 | } | |
| 567 | ||
| 568 | ck_wait(); | |
| 569 | double t_end = time_ms(); | |
| 570 | double total_s = (t_end - t_start) / 1000.0; | |
| 571 | ||
| 572 | printf("\nThroughput results:\n"); | |
| 573 | printf(" Total time: %.2f s\n", total_s); | |
| 574 | printf(" Pipeline throughput: %.1f sents/sec\n", | |
| 575 | 5000.0 / total_s); | |
| 576 | printf(" Iterations: %u (%.1f ms/iter)\n", | |
| 577 | *flags.stats_iteration, | |
| 578 | *flags.stats_iteration > 0 ? | |
| 579 | (t_end - t_start) / *flags.stats_iteration : 0.0); | |
| 580 | printf(" Pairs: %u\n", *flags.stats_pairs); | |
| 581 | printf(" Classes: %u\n", *flags.stats_classes); | |
| 582 | printf(" Total sentences: %u\n", state->total_sentences); | |
| 583 | printf(" Compartments: %u\n", state->num_compartments); | |
| 584 | ||
| 585 | /* With ring size 2048, max sentences in a burst is 2048. | |
| 586 | * The real metric is throughput and convergence. */ | |
| 587 | int pass = (state->total_sentences >= 1000 && | |
| 588 | *flags.stats_pairs > 100); | |
| 589 | printf("T6: %s\n", pass ? "PASS" : "FAIL"); | |
| 590 | ||
| 591 | ck_shutdown(state, comps, ring); | |
| 592 | free_flags(&flags); | |
| 593 | return pass; | |
| 594 | } | |
| 595 | ||
| 596 | /* ═══════════════════════════════════════════════════════════════ | |
| 597 | * MAIN | |
| 598 | * ═══════════════════════════════════════════════════════════════ */ | |
| 599 | ||
| 600 | int main() { | |
| 601 | printf("GPU Compartment Kernel — Test Suite\n"); | |
| 602 | printf("===================================\n"); | |
| 603 | ||
| 604 | cudaDeviceProp prop; | |
| 605 | cudaGetDeviceProperties(&prop, 0); | |
| 606 | printf("GPU: %s (SM %d.%d, %d SMs, %d MB)\n", | |
| 607 | prop.name, prop.major, prop.minor, | |
| 608 | prop.multiProcessorCount, | |
| 609 | (int)(prop.totalGlobalMem / (1024*1024))); | |
| 610 | ||
| 611 | int supports_coop = 0; | |
| 612 | cudaDeviceGetAttribute(&supports_coop, cudaDevAttrCooperativeLaunch, 0); | |
| 613 | if (!supports_coop) { | |
| 614 | printf("ERROR: Device does not support cooperative launch\n"); | |
| 615 | return 1; | |
| 616 | } | |
| 617 | ||
| 618 | printf("CompartmentState size: %.2f MB\n", | |
| 619 | sizeof(CompartmentState) / (1024.0 * 1024.0)); | |
| 620 | printf("LearningState size: %.2f MB\n", | |
| 621 | sizeof(LearningState) / (1024.0 * 1024.0)); | |
| 622 | printf("SentenceRing size: %.2f KB (%d slots)\n", | |
| 623 | sizeof(SentenceRing) / 1024.0, LL_RING_SIZE); | |
| 624 | ||
| 625 | int passed = 0, failed = 0; | |
| 626 | ||
| 627 | if (test1_basic_launch()) passed++; else failed++; | |
| 628 | if (test2_sentence_distribution()) passed++; else failed++; | |
| 629 | if (test3_phase_gated_convergence()) passed++; else failed++; | |
| 630 | if (test4_real_convergence()) passed++; else failed++; | |
| 631 | if (test5_mi_correctness()) passed++; else failed++; | |
| 632 | if (test6_throughput()) passed++; else failed++; | |
| 633 | ||
| 634 | printf("\n===================================\n"); | |
| 635 | printf("Results: %d/%d passed\n", passed, passed + failed); | |
| 636 | printf("===================================\n"); | |
| 637 | ||
| 638 | return failed > 0 ? 1 : 0; | |
| 639 | } |
| … | … | @@ -1,444 +0,0 @@ |
| 1 | /* test-connected-components.cu — Tests T1 and T2 for connected components | |
| 2 | * | |
| 3 | * T1: Basic CC (10 words, 2 clusters) | |
| 4 | * T2: Threshold sweep (20 words, 3 natural clusters at different similarities) | |
| 5 | * | |
| 6 | * Build: | |
| 7 | * nvcc -O2 -arch=sm_75 -rdc=true \ | |
| 8 | * -o test-connected-components \ | |
| 9 | * test-connected-components.cu gpu-connected-components.cu \ | |
| 10 | * -lcudadevrt -lm | |
| 11 | */ | |
| 12 | ||
| 13 | #include <cstdio> | |
| 14 | #include <cstdlib> | |
| 15 | #include <cstring> | |
| 16 | #include <cmath> | |
| 17 | #include <cstdint> | |
| 18 | ||
| 19 | /* Forward declarations from gpu-connected-components.cu */ | |
| 20 | extern "C" int cc_run( | |
| 21 | const uint32_t* d_cand_word_a, | |
| 22 | const uint32_t* d_cand_word_b, | |
| 23 | const double* d_cand_cosine, | |
| 24 | uint32_t num_candidates, | |
| 25 | uint32_t num_words, | |
| 26 | float threshold, | |
| 27 | uint32_t* d_edge_a, | |
| 28 | uint32_t* d_edge_b, | |
| 29 | float* d_edge_weight, | |
| 30 | uint32_t* d_edge_count, | |
| 31 | uint32_t edge_capacity, | |
| 32 | uint32_t* d_label, | |
| 33 | int* d_changed, | |
| 34 | uint32_t* d_component_flags, | |
| 35 | uint32_t* d_component_count, | |
| 36 | uint32_t* d_class_id, | |
| 37 | uint32_t* d_next_class_id, | |
| 38 | uint32_t* d_class_sizes, | |
| 39 | uint32_t* h_num_components, | |
| 40 | uint32_t* h_num_edges); | |
| 41 | ||
| 42 | extern "C" float cc_threshold_sweep( | |
| 43 | const uint32_t* d_cand_word_a, | |
| 44 | const uint32_t* d_cand_word_b, | |
| 45 | const double* d_cand_cosine, | |
| 46 | uint32_t num_candidates, | |
| 47 | uint32_t num_words, | |
| 48 | const float* thresholds, | |
| 49 | int num_thresholds, | |
| 50 | uint32_t* d_edge_a, | |
| 51 | uint32_t* d_edge_b, | |
| 52 | float* d_edge_weight, | |
| 53 | uint32_t* d_edge_count, | |
| 54 | uint32_t edge_capacity, | |
| 55 | uint32_t* d_label, | |
| 56 | int* d_changed, | |
| 57 | uint32_t* d_component_flags, | |
| 58 | uint32_t* d_component_count, | |
| 59 | uint32_t* d_class_id, | |
| 60 | uint32_t* d_next_class_id, | |
| 61 | uint32_t* d_class_sizes, | |
| 62 | uint32_t* h_component_counts, | |
| 63 | float* h_best_threshold); | |
| 64 | ||
| 65 | /* ─── Helper: Allocate work buffers ─── */ | |
| 66 | ||
| 67 | struct CCWorkBuffers { | |
| 68 | uint32_t* d_edge_a; | |
| 69 | uint32_t* d_edge_b; | |
| 70 | float* d_edge_weight; | |
| 71 | uint32_t* d_edge_count; | |
| 72 | uint32_t* d_label; | |
| 73 | int* d_changed; | |
| 74 | uint32_t* d_component_flags; | |
| 75 | uint32_t* d_component_count; | |
| 76 | uint32_t* d_class_id; | |
| 77 | uint32_t* d_next_class_id; | |
| 78 | uint32_t* d_class_sizes; | |
| 79 | uint32_t edge_capacity; | |
| 80 | uint32_t num_words; | |
| 81 | }; | |
| 82 | ||
| 83 | void alloc_work_buffers(CCWorkBuffers* wb, uint32_t num_words, uint32_t edge_capacity) { | |
| 84 | wb->edge_capacity = edge_capacity; | |
| 85 | wb->num_words = num_words; | |
| 86 | ||
| 87 | cudaMalloc(&wb->d_edge_a, edge_capacity * sizeof(uint32_t)); | |
| 88 | cudaMalloc(&wb->d_edge_b, edge_capacity * sizeof(uint32_t)); | |
| 89 | cudaMalloc(&wb->d_edge_weight, edge_capacity * sizeof(float)); | |
| 90 | cudaMalloc(&wb->d_edge_count, sizeof(uint32_t)); | |
| 91 | cudaMalloc(&wb->d_label, num_words * sizeof(uint32_t)); | |
| 92 | cudaMalloc(&wb->d_changed, sizeof(int)); | |
| 93 | cudaMalloc(&wb->d_component_flags, num_words * sizeof(uint32_t)); | |
| 94 | cudaMalloc(&wb->d_component_count, sizeof(uint32_t)); | |
| 95 | cudaMalloc(&wb->d_class_id, num_words * sizeof(uint32_t)); | |
| 96 | cudaMalloc(&wb->d_next_class_id, sizeof(uint32_t)); | |
| 97 | cudaMalloc(&wb->d_class_sizes, num_words * sizeof(uint32_t)); | |
| 98 | } | |
| 99 | ||
| 100 | void free_work_buffers(CCWorkBuffers* wb) { | |
| 101 | cudaFree(wb->d_edge_a); | |
| 102 | cudaFree(wb->d_edge_b); | |
| 103 | cudaFree(wb->d_edge_weight); | |
| 104 | cudaFree(wb->d_edge_count); | |
| 105 | cudaFree(wb->d_label); | |
| 106 | cudaFree(wb->d_changed); | |
| 107 | cudaFree(wb->d_component_flags); | |
| 108 | cudaFree(wb->d_component_count); | |
| 109 | cudaFree(wb->d_class_id); | |
| 110 | cudaFree(wb->d_next_class_id); | |
| 111 | cudaFree(wb->d_class_sizes); | |
| 112 | } | |
| 113 | ||
| 114 | /* ─── T1: Basic CC (10 words, 2 clusters) ─── */ | |
| 115 | ||
| 116 | int test_t1_basic_cc() { | |
| 117 | printf("T1: Basic CC (10 words, 2 clusters)\n"); | |
| 118 | ||
| 119 | /* Setup: 10 words forming 2 clear clusters: | |
| 120 | * Cluster A: words 0,1,2,3,4 (all pairs have cosine 0.8) | |
| 121 | * Cluster B: words 5,6,7,8,9 (all pairs have cosine 0.7) | |
| 122 | * Cross-cluster: cosine 0.1 (below any reasonable threshold) | |
| 123 | */ | |
| 124 | ||
| 125 | const uint32_t NUM_WORDS = 10; | |
| 126 | ||
| 127 | /* Build candidate pairs: all 10 choose 2 = 45 pairs */ | |
| 128 | uint32_t h_word_a[45], h_word_b[45]; | |
| 129 | double h_cosine[45]; | |
| 130 | int nc = 0; | |
| 131 | ||
| 132 | for (uint32_t i = 0; i < NUM_WORDS; i++) { | |
| 133 | for (uint32_t j = i + 1; j < NUM_WORDS; j++) { | |
| 134 | h_word_a[nc] = i; | |
| 135 | h_word_b[nc] = j; | |
| 136 | bool same_cluster = (i < 5 && j < 5) || (i >= 5 && j >= 5); | |
| 137 | h_cosine[nc] = same_cluster ? (i < 5 ? 0.8 : 0.7) : 0.1; | |
| 138 | nc++; | |
| 139 | } | |
| 140 | } | |
| 141 | ||
| 142 | /* Upload candidates to GPU */ | |
| 143 | uint32_t* d_cand_a; | |
| 144 | uint32_t* d_cand_b; | |
| 145 | double* d_cand_cos; | |
| 146 | cudaMalloc(&d_cand_a, nc * sizeof(uint32_t)); | |
| 147 | cudaMalloc(&d_cand_b, nc * sizeof(uint32_t)); | |
| 148 | cudaMalloc(&d_cand_cos, nc * sizeof(double)); | |
| 149 | cudaMemcpy(d_cand_a, h_word_a, nc * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 150 | cudaMemcpy(d_cand_b, h_word_b, nc * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 151 | cudaMemcpy(d_cand_cos, h_cosine, nc * sizeof(double), cudaMemcpyHostToDevice); | |
| 152 | ||
| 153 | /* Allocate work buffers */ | |
| 154 | CCWorkBuffers wb; | |
| 155 | alloc_work_buffers(&wb, NUM_WORDS, 256); | |
| 156 | ||
| 157 | /* Run CC at threshold 0.5 → should find 2 components */ | |
| 158 | uint32_t num_components = 0, num_edges = 0; | |
| 159 | cc_run(d_cand_a, d_cand_b, d_cand_cos, nc, NUM_WORDS, 0.5f, | |
| 160 | wb.d_edge_a, wb.d_edge_b, wb.d_edge_weight, | |
| 161 | wb.d_edge_count, wb.edge_capacity, | |
| 162 | wb.d_label, wb.d_changed, | |
| 163 | wb.d_component_flags, wb.d_component_count, | |
| 164 | wb.d_class_id, wb.d_next_class_id, wb.d_class_sizes, | |
| 165 | &num_components, &num_edges); | |
| 166 | ||
| 167 | printf(" Threshold 0.5: %u components, %u edges\n", num_components, num_edges); | |
| 168 | ||
| 169 | /* Verify: exactly 2 components */ | |
| 170 | int pass = 1; | |
| 171 | if (num_components != 2) { | |
| 172 | printf(" FAIL: expected 2 components, got %u\n", num_components); | |
| 173 | pass = 0; | |
| 174 | } | |
| 175 | ||
| 176 | /* Verify: within-cluster words have same class */ | |
| 177 | uint32_t h_class_id[10]; | |
| 178 | cudaMemcpy(h_class_id, wb.d_class_id, NUM_WORDS * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 179 | ||
| 180 | /* Words 0-4 should share one class, 5-9 another */ | |
| 181 | uint32_t class_a = h_class_id[0]; | |
| 182 | uint32_t class_b = h_class_id[5]; | |
| 183 | for (int i = 0; i < 5; i++) { | |
| 184 | if (h_class_id[i] != class_a) { | |
| 185 | printf(" FAIL: word %d class=%u != word 0 class=%u\n", i, h_class_id[i], class_a); | |
| 186 | pass = 0; | |
| 187 | } | |
| 188 | } | |
| 189 | for (int i = 5; i < 10; i++) { | |
| 190 | if (h_class_id[i] != class_b) { | |
| 191 | printf(" FAIL: word %d class=%u != word 5 class=%u\n", i, h_class_id[i], class_b); | |
| 192 | pass = 0; | |
| 193 | } | |
| 194 | } | |
| 195 | if (class_a == class_b) { | |
| 196 | printf(" FAIL: clusters A and B have same class_id=%u\n", class_a); | |
| 197 | pass = 0; | |
| 198 | } | |
| 199 | ||
| 200 | /* Verify class sizes */ | |
| 201 | uint32_t h_class_sizes[10]; | |
| 202 | cudaMemcpy(h_class_sizes, wb.d_class_sizes, NUM_WORDS * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 203 | if (h_class_sizes[class_a] != 5) { | |
| 204 | printf(" FAIL: class A size=%u, expected 5\n", h_class_sizes[class_a]); | |
| 205 | pass = 0; | |
| 206 | } | |
| 207 | if (h_class_sizes[class_b] != 5) { | |
| 208 | printf(" FAIL: class B size=%u, expected 5\n", h_class_sizes[class_b]); | |
| 209 | pass = 0; | |
| 210 | } | |
| 211 | ||
| 212 | /* Verify: at high threshold 0.75, cluster B splits (cosine=0.7 < 0.75) */ | |
| 213 | cc_run(d_cand_a, d_cand_b, d_cand_cos, nc, NUM_WORDS, 0.75f, | |
| 214 | wb.d_edge_a, wb.d_edge_b, wb.d_edge_weight, | |
| 215 | wb.d_edge_count, wb.edge_capacity, | |
| 216 | wb.d_label, wb.d_changed, | |
| 217 | wb.d_component_flags, wb.d_component_count, | |
| 218 | wb.d_class_id, wb.d_next_class_id, wb.d_class_sizes, | |
| 219 | &num_components, &num_edges); | |
| 220 | ||
| 221 | printf(" Threshold 0.75: %u components, %u edges\n", num_components, num_edges); | |
| 222 | /* At 0.75: cluster A (cos=0.8) stays together, cluster B (cos=0.7) breaks apart → 6 components */ | |
| 223 | if (num_components != 6) { | |
| 224 | printf(" FAIL: expected 6 components at threshold 0.75, got %u\n", num_components); | |
| 225 | pass = 0; | |
| 226 | } | |
| 227 | ||
| 228 | /* Verify: at threshold 0.05, all words form one component */ | |
| 229 | cc_run(d_cand_a, d_cand_b, d_cand_cos, nc, NUM_WORDS, 0.05f, | |
| 230 | wb.d_edge_a, wb.d_edge_b, wb.d_edge_weight, | |
| 231 | wb.d_edge_count, wb.edge_capacity, | |
| 232 | wb.d_label, wb.d_changed, | |
| 233 | wb.d_component_flags, wb.d_component_count, | |
| 234 | wb.d_class_id, wb.d_next_class_id, wb.d_class_sizes, | |
| 235 | &num_components, &num_edges); | |
| 236 | ||
| 237 | printf(" Threshold 0.05: %u components, %u edges\n", num_components, num_edges); | |
| 238 | if (num_components != 1) { | |
| 239 | printf(" FAIL: expected 1 component at threshold 0.05, got %u\n", num_components); | |
| 240 | pass = 0; | |
| 241 | } | |
| 242 | ||
| 243 | /* Cleanup */ | |
| 244 | cudaFree(d_cand_a); | |
| 245 | cudaFree(d_cand_b); | |
| 246 | cudaFree(d_cand_cos); | |
| 247 | free_work_buffers(&wb); | |
| 248 | ||
| 249 | printf(" T1: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 250 | return pass; | |
| 251 | } | |
| 252 | ||
| 253 | /* ─── T2: Threshold sweep (20 words, 3 natural clusters) ─── */ | |
| 254 | ||
| 255 | int test_t2_threshold_sweep() { | |
| 256 | printf("T2: Threshold sweep (20 words, 3 natural clusters)\n"); | |
| 257 | ||
| 258 | /* Setup: 20 words forming 3 clusters at different similarity levels: | |
| 259 | * Cluster A: words 0-6 (cos=0.9 within, very tight) | |
| 260 | * Cluster B: words 7-13 (cos=0.6 within, moderate) | |
| 261 | * Cluster C: words 14-19 (cos=0.4 within, loose) | |
| 262 | * Cross-cluster: cos=0.05 (noise floor) | |
| 263 | * | |
| 264 | * Expected knee detection: | |
| 265 | * Sweep from 0.9 → 0.1: | |
| 266 | * At 0.9: many components (A partial, B+C all singletons) | |
| 267 | * At 0.85: A merges fully → drop from ~18 to ~14 = drop of ~4 | |
| 268 | * At 0.55: B merges → drop from ~14 to ~8 = drop of ~6 ← BIGGEST DROP | |
| 269 | * At 0.35: C merges → drop from ~8 to ~3 = drop of ~5 | |
| 270 | * At 0.04: all merge → drop from 3 to 1 = drop of 2 | |
| 271 | * | |
| 272 | * Actually let's use exact cosines and work out the real numbers. | |
| 273 | */ | |
| 274 | ||
| 275 | const uint32_t NUM_WORDS = 20; | |
| 276 | ||
| 277 | /* Build all 20 choose 2 = 190 candidate pairs */ | |
| 278 | uint32_t h_word_a[190], h_word_b[190]; | |
| 279 | double h_cosine[190]; | |
| 280 | int nc = 0; | |
| 281 | ||
| 282 | for (uint32_t i = 0; i < NUM_WORDS; i++) { | |
| 283 | for (uint32_t j = i + 1; j < NUM_WORDS; j++) { | |
| 284 | h_word_a[nc] = i; | |
| 285 | h_word_b[nc] = j; | |
| 286 | ||
| 287 | /* Determine cluster membership */ | |
| 288 | int ci = (i < 7) ? 0 : (i < 14) ? 1 : 2; | |
| 289 | int cj = (j < 7) ? 0 : (j < 14) ? 1 : 2; | |
| 290 | ||
| 291 | if (ci == cj) { | |
| 292 | /* Within cluster */ | |
| 293 | double cos_vals[] = {0.9, 0.6, 0.4}; | |
| 294 | h_cosine[nc] = cos_vals[ci]; | |
| 295 | } else { | |
| 296 | h_cosine[nc] = 0.05; /* noise */ | |
| 297 | } | |
| 298 | nc++; | |
| 299 | } | |
| 300 | } | |
| 301 | printf(" Built %d candidate pairs\n", nc); | |
| 302 | ||
| 303 | /* Upload */ | |
| 304 | uint32_t* d_cand_a; | |
| 305 | uint32_t* d_cand_b; | |
| 306 | double* d_cand_cos; | |
| 307 | cudaMalloc(&d_cand_a, nc * sizeof(uint32_t)); | |
| 308 | cudaMalloc(&d_cand_b, nc * sizeof(uint32_t)); | |
| 309 | cudaMalloc(&d_cand_cos, nc * sizeof(double)); | |
| 310 | cudaMemcpy(d_cand_a, h_word_a, nc * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 311 | cudaMemcpy(d_cand_b, h_word_b, nc * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 312 | cudaMemcpy(d_cand_cos, h_cosine, nc * sizeof(double), cudaMemcpyHostToDevice); | |
| 313 | ||
| 314 | /* Work buffers */ | |
| 315 | CCWorkBuffers wb; | |
| 316 | alloc_work_buffers(&wb, NUM_WORDS, 1024); | |
| 317 | ||
| 318 | /* Sweep: high to low */ | |
| 319 | float thresholds[] = {0.85f, 0.55f, 0.35f, 0.10f, 0.04f}; | |
| 320 | int num_thresh = 5; | |
| 321 | uint32_t h_counts[5]; | |
| 322 | float h_best; | |
| 323 | ||
| 324 | cc_threshold_sweep( | |
| 325 | d_cand_a, d_cand_b, d_cand_cos, nc, NUM_WORDS, | |
| 326 | thresholds, num_thresh, | |
| 327 | wb.d_edge_a, wb.d_edge_b, wb.d_edge_weight, | |
| 328 | wb.d_edge_count, wb.edge_capacity, | |
| 329 | wb.d_label, wb.d_changed, | |
| 330 | wb.d_component_flags, wb.d_component_count, | |
| 331 | wb.d_class_id, wb.d_next_class_id, wb.d_class_sizes, | |
| 332 | h_counts, &h_best); | |
| 333 | ||
| 334 | printf(" Threshold sweep results:\n"); | |
| 335 | for (int i = 0; i < num_thresh; i++) { | |
| 336 | printf(" threshold=%.2f → %u components\n", thresholds[i], h_counts[i]); | |
| 337 | } | |
| 338 | printf(" Best threshold (knee): %.2f\n", h_best); | |
| 339 | ||
| 340 | int pass = 1; | |
| 341 | ||
| 342 | /* Verify expected component counts: | |
| 343 | * 0.85: cluster A (cos=0.9 > 0.85) stays → 1 + 7 + 6 = 14 components | |
| 344 | * 0.55: A + B (cos=0.6 > 0.55) stay → 1 + 1 + 6 = 8 components | |
| 345 | * 0.35: A + B + C (cos=0.4 > 0.35) all → 1 + 1 + 1 = 3 components | |
| 346 | * 0.10: same → 3 (cross at 0.05 < 0.10) | |
| 347 | * 0.04: all merge → 1 component | |
| 348 | */ | |
| 349 | uint32_t expected[] = {14, 8, 3, 3, 1}; | |
| 350 | for (int i = 0; i < num_thresh; i++) { | |
| 351 | if (h_counts[i] != expected[i]) { | |
| 352 | printf(" FAIL: threshold=%.2f: expected %u components, got %u\n", | |
| 353 | thresholds[i], expected[i], h_counts[i]); | |
| 354 | pass = 0; | |
| 355 | } | |
| 356 | } | |
| 357 | ||
| 358 | /* Knee should be at 0.55 (drop from 14 → 8 = 6, biggest) or 0.35 (drop from 8 → 3 = 5) | |
| 359 | * Actually drop at 0.85: 14→8=6, at 0.55: 8→3=5, at 0.35: 3→3=0, at 0.10: 3→1=2 | |
| 360 | * Biggest drop is threshold[0]=0.85 (14→8=6) | |
| 361 | */ | |
| 362 | /* The knee is the threshold BEFORE the biggest drop, so it should be 0.85 | |
| 363 | * where the drop to the next threshold (0.55) is 14-8=6 */ | |
| 364 | if (h_best != 0.85f) { | |
| 365 | printf(" NOTE: Expected knee at 0.85, got %.2f (largest drop: 14→8=6)\n", h_best); | |
| 366 | /* This is informational — the knee detection picks the largest drop, | |
| 367 | * which may vary. The important thing is it's reasonable. */ | |
| 368 | } | |
| 369 | ||
| 370 | /* Run CC at the final threshold 0.35 and verify 3 proper clusters */ | |
| 371 | uint32_t nc_final = 0, ne_final = 0; | |
| 372 | cc_run(d_cand_a, d_cand_b, d_cand_cos, nc, NUM_WORDS, 0.35f, | |
| 373 | wb.d_edge_a, wb.d_edge_b, wb.d_edge_weight, | |
| 374 | wb.d_edge_count, wb.edge_capacity, | |
| 375 | wb.d_label, wb.d_changed, | |
| 376 | wb.d_component_flags, wb.d_component_count, | |
| 377 | wb.d_class_id, wb.d_next_class_id, wb.d_class_sizes, | |
| 378 | &nc_final, &ne_final); | |
| 379 | ||
| 380 | printf(" Final run at 0.35: %u components, %u edges\n", nc_final, ne_final); | |
| 381 | ||
| 382 | /* Verify cluster membership */ | |
| 383 | uint32_t h_class_id[20]; | |
| 384 | cudaMemcpy(h_class_id, wb.d_class_id, NUM_WORDS * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 385 | ||
| 386 | /* Words 0-6 should share a class, 7-13 another, 14-19 another */ | |
| 387 | uint32_t cA = h_class_id[0], cB = h_class_id[7], cC = h_class_id[14]; | |
| 388 | for (int i = 0; i < 7; i++) { | |
| 389 | if (h_class_id[i] != cA) { | |
| 390 | printf(" FAIL: word %d class=%u != cluster A class=%u\n", i, h_class_id[i], cA); | |
| 391 | pass = 0; | |
| 392 | } | |
| 393 | } | |
| 394 | for (int i = 7; i < 14; i++) { | |
| 395 | if (h_class_id[i] != cB) { | |
| 396 | printf(" FAIL: word %d class=%u != cluster B class=%u\n", i, h_class_id[i], cB); | |
| 397 | pass = 0; | |
| 398 | } | |
| 399 | } | |
| 400 | for (int i = 14; i < 20; i++) { | |
| 401 | if (h_class_id[i] != cC) { | |
| 402 | printf(" FAIL: word %d class=%u != cluster C class=%u\n", i, h_class_id[i], cC); | |
| 403 | pass = 0; | |
| 404 | } | |
| 405 | } | |
| 406 | ||
| 407 | /* All three classes must be distinct */ | |
| 408 | if (cA == cB || cA == cC || cB == cC) { | |
| 409 | printf(" FAIL: classes not distinct: A=%u B=%u C=%u\n", cA, cB, cC); | |
| 410 | pass = 0; | |
| 411 | } | |
| 412 | ||
| 413 | /* Verify class sizes */ | |
| 414 | uint32_t h_sizes[20]; | |
| 415 | cudaMemcpy(h_sizes, wb.d_class_sizes, NUM_WORDS * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 416 | if (h_sizes[cA] != 7) { printf(" FAIL: class A size=%u, expected 7\n", h_sizes[cA]); pass = 0; } | |
| 417 | if (h_sizes[cB] != 7) { printf(" FAIL: class B size=%u, expected 7\n", h_sizes[cB]); pass = 0; } | |
| 418 | if (h_sizes[cC] != 6) { printf(" FAIL: class C size=%u, expected 6\n", h_sizes[cC]); pass = 0; } | |
| 419 | ||
| 420 | /* Cleanup */ | |
| 421 | cudaFree(d_cand_a); | |
| 422 | cudaFree(d_cand_b); | |
| 423 | cudaFree(d_cand_cos); | |
| 424 | free_work_buffers(&wb); | |
| 425 | ||
| 426 | printf(" T2: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 427 | return pass; | |
| 428 | } | |
| 429 | ||
| 430 | /* ─── Main ─── */ | |
| 431 | ||
| 432 | int main() { | |
| 433 | printf("=== Connected Components Tests ===\n\n"); | |
| 434 | ||
| 435 | int t1 = test_t1_basic_cc(); | |
| 436 | int t2 = test_t2_threshold_sweep(); | |
| 437 | ||
| 438 | printf("=== Results ===\n"); | |
| 439 | printf("T1 (Basic CC): %s\n", t1 ? "PASS" : "FAIL"); | |
| 440 | printf("T2 (Threshold sweep): %s\n", t2 ? "PASS" : "FAIL"); | |
| 441 | printf("Overall: %s\n", (t1 && t2) ? "ALL PASSED" : "SOME FAILED"); | |
| 442 | ||
| 443 | return (t1 && t2) ? 0 : 1; | |
| 444 | } |
| … | … | @@ -1,320 +0,0 @@ |
| 1 | /* test-connector-rewrite.cu — Tests T3 and T4 for grammar costs + class MI | |
| 2 | * | |
| 3 | * T3: Grammar costs (100 sections with varying counts) | |
| 4 | * T4: Class MI (5 words, 2 classes, MI aggregation correctness) | |
| 5 | * | |
| 6 | * Build: | |
| 7 | * nvcc -O2 -arch=sm_75 -rdc=true \ | |
| 8 | * -o test-connector-rewrite \ | |
| 9 | * test-connector-rewrite.cu gpu-connector-rewrite.cu \ | |
| 10 | * -lcudadevrt -lm | |
| 11 | */ | |
| 12 | ||
| 13 | #include <cstdio> | |
| 14 | #include <cstdlib> | |
| 15 | #include <cstring> | |
| 16 | #include <cmath> | |
| 17 | #include <cstdint> | |
| 18 | ||
| 19 | /* Forward declarations */ | |
| 20 | extern "C" void grammar_pipeline_run( | |
| 21 | const double* d_sec_count, double* d_sec_cost, uint32_t num_sections, | |
| 22 | const uint32_t* d_pair_word_a, const uint32_t* d_pair_word_b, | |
| 23 | const double* d_pair_mi, const double* d_pair_count, uint32_t num_pairs, | |
| 24 | const uint32_t* d_word_class_id, | |
| 25 | uint64_t* d_ht_keys, double* d_ht_mi_sum, uint32_t* d_ht_count, | |
| 26 | uint32_t ht_capacity, double* d_max_count); | |
| 27 | ||
| 28 | extern "C" double grammar_read_class_mi( | |
| 29 | uint32_t class_a, uint32_t class_b, | |
| 30 | uint64_t* d_ht_keys, double* d_ht_mi_sum, uint32_t ht_capacity); | |
| 31 | ||
| 32 | extern "C" uint32_t grammar_class_mi_count( | |
| 33 | uint64_t* d_ht_keys, uint32_t ht_capacity); | |
| 34 | ||
| 35 | /* ─── T3: Grammar costs (100 sections) ─── */ | |
| 36 | ||
| 37 | int test_t3_grammar_costs() { | |
| 38 | printf("T3: Grammar costs (100 sections)\n"); | |
| 39 | ||
| 40 | const uint32_t NUM_SECTIONS = 100; | |
| 41 | ||
| 42 | /* Create section counts: exponential distribution | |
| 43 | * Section i has count = 100 * exp(-i/20) | |
| 44 | * So section 0 has count 100, section 20 has ~37, section 60 has ~5, etc. | |
| 45 | */ | |
| 46 | double h_sec_count[100]; | |
| 47 | for (int i = 0; i < (int)NUM_SECTIONS; i++) { | |
| 48 | h_sec_count[i] = 100.0 * exp(-(double)i / 20.0); | |
| 49 | } | |
| 50 | /* Add a few zero-count sections */ | |
| 51 | h_sec_count[95] = 0.0; | |
| 52 | h_sec_count[96] = 0.0; | |
| 53 | h_sec_count[97] = 0.5; /* below threshold */ | |
| 54 | h_sec_count[98] = 0.0; | |
| 55 | h_sec_count[99] = 0.0; | |
| 56 | ||
| 57 | /* Upload */ | |
| 58 | double *d_sec_count, *d_sec_cost, *d_max_count; | |
| 59 | cudaMalloc(&d_sec_count, NUM_SECTIONS * sizeof(double)); | |
| 60 | cudaMalloc(&d_sec_cost, NUM_SECTIONS * sizeof(double)); | |
| 61 | cudaMalloc(&d_max_count, sizeof(double)); | |
| 62 | cudaMemcpy(d_sec_count, h_sec_count, NUM_SECTIONS * sizeof(double), cudaMemcpyHostToDevice); | |
| 63 | ||
| 64 | /* We need dummy pair/class data for the pipeline — but we can just | |
| 65 | * call the cost kernels directly. Let's use the pipeline with empty pairs. */ | |
| 66 | uint32_t *d_pair_wa, *d_pair_wb, *d_word_class; | |
| 67 | double *d_pair_mi, *d_pair_count; | |
| 68 | uint64_t *d_ht_keys; | |
| 69 | double *d_ht_mi_sum; | |
| 70 | uint32_t *d_ht_count; | |
| 71 | uint32_t ht_cap = 64; /* tiny, no pairs */ | |
| 72 | ||
| 73 | cudaMalloc(&d_pair_wa, sizeof(uint32_t)); | |
| 74 | cudaMalloc(&d_pair_wb, sizeof(uint32_t)); | |
| 75 | cudaMalloc(&d_pair_mi, sizeof(double)); | |
| 76 | cudaMalloc(&d_pair_count, sizeof(double)); | |
| 77 | cudaMalloc(&d_word_class, sizeof(uint32_t)); | |
| 78 | cudaMalloc(&d_ht_keys, ht_cap * sizeof(uint64_t)); | |
| 79 | cudaMalloc(&d_ht_mi_sum, ht_cap * sizeof(double)); | |
| 80 | cudaMalloc(&d_ht_count, ht_cap * sizeof(uint32_t)); | |
| 81 | ||
| 82 | grammar_pipeline_run( | |
| 83 | d_sec_count, d_sec_cost, NUM_SECTIONS, | |
| 84 | d_pair_wa, d_pair_wb, d_pair_mi, d_pair_count, 0, | |
| 85 | d_word_class, | |
| 86 | d_ht_keys, d_ht_mi_sum, d_ht_count, ht_cap, | |
| 87 | d_max_count); | |
| 88 | ||
| 89 | /* Read back costs */ | |
| 90 | double h_sec_cost[100]; | |
| 91 | cudaMemcpy(h_sec_cost, d_sec_cost, NUM_SECTIONS * sizeof(double), cudaMemcpyDeviceToHost); | |
| 92 | ||
| 93 | int pass = 1; | |
| 94 | double max_count = h_sec_count[0]; /* 100.0 */ | |
| 95 | ||
| 96 | printf(" Max section count: %.1f\n", max_count); | |
| 97 | printf(" Sample costs:\n"); | |
| 98 | ||
| 99 | /* Verify a few specific costs */ | |
| 100 | /* Section 0: count=100, cost = -0.5 * log2(100/100) + 0.1 = -0.5 * 0 + 0.1 = 0.1 */ | |
| 101 | double expected_0 = 0.1; | |
| 102 | printf(" sec[0]: count=%.1f cost=%.4f expected=%.4f\n", | |
| 103 | h_sec_count[0], h_sec_cost[0], expected_0); | |
| 104 | if (fabs(h_sec_cost[0] - expected_0) > 0.01) { | |
| 105 | printf(" FAIL: sec[0] cost mismatch\n"); | |
| 106 | pass = 0; | |
| 107 | } | |
| 108 | ||
| 109 | /* Section 20: count=100*exp(-1)≈36.79, cost = -0.5 * log2(36.79/100) + 0.1 | |
| 110 | * = -0.5 * log2(0.3679) + 0.1 = -0.5 * (-1.443) + 0.1 = 0.7215 + 0.1 = 0.8215 */ | |
| 111 | double ratio_20 = h_sec_count[20] / max_count; | |
| 112 | double expected_20 = -0.5 * log2(ratio_20) + 0.1; | |
| 113 | printf(" sec[20]: count=%.1f cost=%.4f expected=%.4f\n", | |
| 114 | h_sec_count[20], h_sec_cost[20], expected_20); | |
| 115 | if (fabs(h_sec_cost[20] - expected_20) > 0.01) { | |
| 116 | printf(" FAIL: sec[20] cost mismatch\n"); | |
| 117 | pass = 0; | |
| 118 | } | |
| 119 | ||
| 120 | /* Section 60: count≈4.98, cost should be higher */ | |
| 121 | double ratio_60 = h_sec_count[60] / max_count; | |
| 122 | double expected_60 = -0.5 * log2(ratio_60) + 0.1; | |
| 123 | expected_60 = fmax(0.1, fmin(expected_60, 10.0)); | |
| 124 | printf(" sec[60]: count=%.2f cost=%.4f expected=%.4f\n", | |
| 125 | h_sec_count[60], h_sec_cost[60], expected_60); | |
| 126 | if (fabs(h_sec_cost[60] - expected_60) > 0.05) { | |
| 127 | printf(" FAIL: sec[60] cost mismatch\n"); | |
| 128 | pass = 0; | |
| 129 | } | |
| 130 | ||
| 131 | /* Zero-count sections should have cost 99.0 */ | |
| 132 | if (h_sec_cost[95] != 99.0) { | |
| 133 | printf(" FAIL: sec[95] (count=0) cost=%.1f, expected 99.0\n", h_sec_cost[95]); | |
| 134 | pass = 0; | |
| 135 | } | |
| 136 | ||
| 137 | /* Verify monotonicity: cost should increase as count decreases */ | |
| 138 | int monotonic = 1; | |
| 139 | for (int i = 1; i < 95; i++) { /* skip zero-count tail */ | |
| 140 | if (h_sec_cost[i] < h_sec_cost[i-1] - 0.001) { | |
| 141 | printf(" FAIL: non-monotonic at sec[%d]: cost=%.4f < sec[%d]: cost=%.4f\n", | |
| 142 | i, h_sec_cost[i], i-1, h_sec_cost[i-1]); | |
| 143 | monotonic = 0; | |
| 144 | break; | |
| 145 | } | |
| 146 | } | |
| 147 | if (monotonic) printf(" Cost monotonicity: OK (cost increases as count decreases)\n"); | |
| 148 | else pass = 0; | |
| 149 | ||
| 150 | /* Verify cost range: all costs in [0.1, 10.0] for sections with count >= 1 */ | |
| 151 | for (int i = 0; i < (int)NUM_SECTIONS; i++) { | |
| 152 | if (h_sec_count[i] < 1.0) continue; /* skip pruned sections */ | |
| 153 | if (h_sec_cost[i] < 0.1 || h_sec_cost[i] > 10.0) { | |
| 154 | printf(" FAIL: sec[%d] cost=%.4f out of range [0.1, 10.0]\n", i, h_sec_cost[i]); | |
| 155 | pass = 0; | |
| 156 | break; | |
| 157 | } | |
| 158 | } | |
| 159 | /* Verify pruned sections get cost 99.0 */ | |
| 160 | for (int i = 0; i < (int)NUM_SECTIONS; i++) { | |
| 161 | if (h_sec_count[i] < 1.0 && h_sec_cost[i] != 99.0) { | |
| 162 | printf(" FAIL: sec[%d] (count=%.2f) cost=%.1f, expected 99.0\n", | |
| 163 | i, h_sec_count[i], h_sec_cost[i]); | |
| 164 | pass = 0; | |
| 165 | break; | |
| 166 | } | |
| 167 | } | |
| 168 | ||
| 169 | /* Cleanup */ | |
| 170 | cudaFree(d_sec_count); cudaFree(d_sec_cost); cudaFree(d_max_count); | |
| 171 | cudaFree(d_pair_wa); cudaFree(d_pair_wb); | |
| 172 | cudaFree(d_pair_mi); cudaFree(d_pair_count); | |
| 173 | cudaFree(d_word_class); | |
| 174 | cudaFree(d_ht_keys); cudaFree(d_ht_mi_sum); cudaFree(d_ht_count); | |
| 175 | ||
| 176 | printf(" T3: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 177 | return pass; | |
| 178 | } | |
| 179 | ||
| 180 | /* ─── T4: Class MI (5 words, 2 classes) ─── */ | |
| 181 | ||
| 182 | int test_t4_class_mi() { | |
| 183 | printf("T4: Class MI (5 words, 2 classes)\n"); | |
| 184 | ||
| 185 | /* Setup: | |
| 186 | * 5 words: w0, w1, w2 in class 0; w3, w4 in class 1 | |
| 187 | * 10 pairs (5 choose 2): | |
| 188 | * Within class 0: (0,1)=3.0 (0,2)=2.5 (1,2)=2.8 → same class, skipped | |
| 189 | * Within class 1: (3,4)=4.0 → same class, skipped | |
| 190 | * Cross-class: (0,3)=1.5 (0,4)=2.0 (1,3)=1.8 (1,4)=2.2 (2,3)=1.0 (2,4)=1.3 | |
| 191 | * | |
| 192 | * Class pair (0,1) should accumulate MI from cross-class pairs: | |
| 193 | * Sum = 1.5 + 2.0 + 1.8 + 2.2 + 1.0 + 1.3 = 9.8 | |
| 194 | * Count = 6 | |
| 195 | * Average = 9.8 / 6 ≈ 1.6333 | |
| 196 | */ | |
| 197 | ||
| 198 | const uint32_t NUM_WORDS = 5; | |
| 199 | const uint32_t NUM_PAIRS = 10; | |
| 200 | ||
| 201 | /* Word pairs and MI values */ | |
| 202 | uint32_t h_pair_wa[] = {0, 0, 0, 0, 0, 1, 1, 1, 2, 3}; | |
| 203 | uint32_t h_pair_wb[] = {1, 2, 3, 4, 4, 2, 3, 4, 3, 4}; | |
| 204 | /* Fix: canonical ordering (lo, hi) — already correct since wa < wb */ | |
| 205 | ||
| 206 | /* Wait — pair (0,4) appears twice? Let me recount: | |
| 207 | * (0,1), (0,2), (0,3), (0,4), (1,2), (1,3), (1,4), (2,3), (2,4), (3,4) | |
| 208 | * That's 10 pairs. Let me fix the arrays. */ | |
| 209 | uint32_t h_pair_wa_fixed[] = {0, 0, 0, 0, 1, 1, 1, 2, 2, 3}; | |
| 210 | uint32_t h_pair_wb_fixed[] = {1, 2, 3, 4, 2, 3, 4, 3, 4, 4}; | |
| 211 | ||
| 212 | double h_pair_mi[] = {3.0, 2.5, 1.5, 2.0, 2.8, 1.8, 2.2, 1.0, 1.3, 4.0}; | |
| 213 | double h_pair_count[10]; | |
| 214 | for (int i = 0; i < 10; i++) h_pair_count[i] = 10.0; /* all have count > 0 */ | |
| 215 | ||
| 216 | /* Word classes: w0,w1,w2 → class 0; w3,w4 → class 1 */ | |
| 217 | uint32_t h_word_class[] = {0, 0, 0, 1, 1}; | |
| 218 | ||
| 219 | /* Upload */ | |
| 220 | uint32_t *d_pair_wa, *d_pair_wb, *d_word_class; | |
| 221 | double *d_pair_mi, *d_pair_count; | |
| 222 | cudaMalloc(&d_pair_wa, NUM_PAIRS * sizeof(uint32_t)); | |
| 223 | cudaMalloc(&d_pair_wb, NUM_PAIRS * sizeof(uint32_t)); | |
| 224 | cudaMalloc(&d_pair_mi, NUM_PAIRS * sizeof(double)); | |
| 225 | cudaMalloc(&d_pair_count, NUM_PAIRS * sizeof(double)); | |
| 226 | cudaMalloc(&d_word_class, NUM_WORDS * sizeof(uint32_t)); | |
| 227 | cudaMemcpy(d_pair_wa, h_pair_wa_fixed, NUM_PAIRS * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 228 | cudaMemcpy(d_pair_wb, h_pair_wb_fixed, NUM_PAIRS * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 229 | cudaMemcpy(d_pair_mi, h_pair_mi, NUM_PAIRS * sizeof(double), cudaMemcpyHostToDevice); | |
| 230 | cudaMemcpy(d_pair_count, h_pair_count, NUM_PAIRS * sizeof(double), cudaMemcpyHostToDevice); | |
| 231 | cudaMemcpy(d_word_class, h_word_class, NUM_WORDS * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 232 | ||
| 233 | /* Dummy section data (not testing costs here) */ | |
| 234 | double *d_sec_count, *d_sec_cost, *d_max_count; | |
| 235 | cudaMalloc(&d_sec_count, sizeof(double)); | |
| 236 | cudaMalloc(&d_sec_cost, sizeof(double)); | |
| 237 | cudaMalloc(&d_max_count, sizeof(double)); | |
| 238 | ||
| 239 | /* Class MI hash table */ | |
| 240 | uint32_t ht_cap = 256; /* power of 2, plenty for 1 class pair */ | |
| 241 | uint64_t *d_ht_keys; | |
| 242 | double *d_ht_mi_sum; | |
| 243 | uint32_t *d_ht_count; | |
| 244 | cudaMalloc(&d_ht_keys, ht_cap * sizeof(uint64_t)); | |
| 245 | cudaMalloc(&d_ht_mi_sum, ht_cap * sizeof(double)); | |
| 246 | cudaMalloc(&d_ht_count, ht_cap * sizeof(uint32_t)); | |
| 247 | ||
| 248 | /* Run pipeline */ | |
| 249 | grammar_pipeline_run( | |
| 250 | d_sec_count, d_sec_cost, 0, /* no sections */ | |
| 251 | d_pair_wa, d_pair_wb, d_pair_mi, d_pair_count, NUM_PAIRS, | |
| 252 | d_word_class, | |
| 253 | d_ht_keys, d_ht_mi_sum, d_ht_count, ht_cap, | |
| 254 | d_max_count); | |
| 255 | ||
| 256 | int pass = 1; | |
| 257 | ||
| 258 | /* Check class MI count — should be exactly 1 class pair (0,1) */ | |
| 259 | uint32_t mi_count = grammar_class_mi_count(d_ht_keys, ht_cap); | |
| 260 | printf(" Class MI entries: %u (expected 1)\n", mi_count); | |
| 261 | if (mi_count != 1) { | |
| 262 | printf(" FAIL: expected 1 class pair, got %u\n", mi_count); | |
| 263 | pass = 0; | |
| 264 | } | |
| 265 | ||
| 266 | /* Read class MI for pair (0,1) */ | |
| 267 | double class_mi_01 = grammar_read_class_mi(0, 1, d_ht_keys, d_ht_mi_sum, ht_cap); | |
| 268 | ||
| 269 | /* Expected: avg of cross-class pairs | |
| 270 | * Cross pairs: (0,3)=1.5, (0,4)=2.0, (1,3)=1.8, (1,4)=2.2, (2,3)=1.0, (2,4)=1.3 | |
| 271 | * Sum = 9.8, count = 6, avg = 1.6333... */ | |
| 272 | double expected_mi = 9.8 / 6.0; | |
| 273 | printf(" Class MI (0,1): %.4f (expected %.4f)\n", class_mi_01, expected_mi); | |
| 274 | if (fabs(class_mi_01 - expected_mi) > 0.01) { | |
| 275 | printf(" FAIL: class MI mismatch\n"); | |
| 276 | pass = 0; | |
| 277 | } | |
| 278 | ||
| 279 | /* Verify symmetric lookup: (1,0) should give same result */ | |
| 280 | double class_mi_10 = grammar_read_class_mi(1, 0, d_ht_keys, d_ht_mi_sum, ht_cap); | |
| 281 | printf(" Class MI (1,0): %.4f (should equal (0,1))\n", class_mi_10); | |
| 282 | if (fabs(class_mi_10 - class_mi_01) > 0.001) { | |
| 283 | printf(" FAIL: asymmetric class MI lookup\n"); | |
| 284 | pass = 0; | |
| 285 | } | |
| 286 | ||
| 287 | /* Verify non-existent class pair returns 0 */ | |
| 288 | double class_mi_99 = grammar_read_class_mi(0, 99, d_ht_keys, d_ht_mi_sum, ht_cap); | |
| 289 | printf(" Class MI (0,99): %.4f (expected 0.0)\n", class_mi_99); | |
| 290 | if (class_mi_99 != 0.0) { | |
| 291 | printf(" FAIL: non-existent class pair returned non-zero\n"); | |
| 292 | pass = 0; | |
| 293 | } | |
| 294 | ||
| 295 | /* Cleanup */ | |
| 296 | cudaFree(d_pair_wa); cudaFree(d_pair_wb); | |
| 297 | cudaFree(d_pair_mi); cudaFree(d_pair_count); | |
| 298 | cudaFree(d_word_class); | |
| 299 | cudaFree(d_sec_count); cudaFree(d_sec_cost); cudaFree(d_max_count); | |
| 300 | cudaFree(d_ht_keys); cudaFree(d_ht_mi_sum); cudaFree(d_ht_count); | |
| 301 | ||
| 302 | printf(" T4: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 303 | return pass; | |
| 304 | } | |
| 305 | ||
| 306 | /* ─── Main ─── */ | |
| 307 | ||
| 308 | int main() { | |
| 309 | printf("=== Connector Rewrite Tests ===\n\n"); | |
| 310 | ||
| 311 | int t3 = test_t3_grammar_costs(); | |
| 312 | int t4 = test_t4_class_mi(); | |
| 313 | ||
| 314 | printf("=== Results ===\n"); | |
| 315 | printf("T3 (Grammar costs): %s\n", t3 ? "PASS" : "FAIL"); | |
| 316 | printf("T4 (Class MI): %s\n", t4 ? "PASS" : "FAIL"); | |
| 317 | printf("Overall: %s\n", (t3 && t4) ? "ALL PASSED" : "SOME FAILED"); | |
| 318 | ||
| 319 | return (t3 && t4) ? 0 : 1; | |
| 320 | } |
| … | … | @@ -1,954 +0,0 @@ |
| 1 | /* | |
| 2 | * test-cosine.c -- Test GPU cosine similarity and candidate generation | |
| 3 | * | |
| 4 | * Compile: gcc -O2 -o test-cosine test-cosine.c -lOpenCL -lm | |
| 5 | * Run: ./test-cosine | |
| 6 | * | |
| 7 | * Tests: | |
| 8 | * 1. Known cosine (2 words, 4 sections, exact verification) | |
| 9 | * 2. Three words — all pairwise cosines | |
| 10 | * 3. Identical vectors → cosine = 1.0 | |
| 11 | * 4. No shared disjuncts → 0 candidates | |
| 12 | * 5. Filter candidates above threshold | |
| 13 | * 6. Benchmark: 1000 sentences → sections → cosines (full pipeline) | |
| 14 | */ | |
| 15 | ||
| 16 | #include <stdio.h> | |
| 17 | #include <stdlib.h> | |
| 18 | #include <stdint.h> | |
| 19 | #include <string.h> | |
| 20 | #include <math.h> | |
| 21 | #include <time.h> | |
| 22 | #include <CL/cl.h> | |
| 23 | ||
| 24 | /* ─── Pool capacities ─── */ | |
| 25 | ||
| 26 | #define WORD_CAPACITY (128 * 1024) | |
| 27 | #define PAIR_CAPACITY (4 * 1024 * 1024) | |
| 28 | #define SECTION_CAPACITY (1024 * 1024) | |
| 29 | #define WORD_HT_CAPACITY (256 * 1024) | |
| 30 | #define PAIR_HT_CAPACITY (8 * 1024 * 1024) | |
| 31 | #define SECTION_HT_CAPACITY (2 * 1024 * 1024) | |
| 32 | ||
| 33 | /* Phase 5 capacities */ | |
| 34 | #define DJH_HT_CAPACITY (2 * 1024 * 1024) | |
| 35 | #define CANDIDATE_CAPACITY (512 * 1024) | |
| 36 | #define CANDIDATE_HT_CAPACITY (1024 * 1024) | |
| 37 | ||
| 38 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 39 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 40 | ||
| 41 | /* ─── Error checking ─── */ | |
| 42 | ||
| 43 | #define CL_CHECK(err, msg) do { \ | |
| 44 | if ((err) != CL_SUCCESS) { \ | |
| 45 | fprintf(stderr, "OpenCL error %d at %s:%d: %s\n", \ | |
| 46 | (err), __FILE__, __LINE__, (msg)); \ | |
| 47 | exit(1); \ | |
| 48 | } \ | |
| 49 | } while(0) | |
| 50 | ||
| 51 | /* ─── Read file ─── */ | |
| 52 | ||
| 53 | char* read_file(const char* path, size_t* len) | |
| 54 | { | |
| 55 | FILE* f = fopen(path, "r"); | |
| 56 | if (!f) { fprintf(stderr, "Cannot open %s\n", path); exit(1); } | |
| 57 | fseek(f, 0, SEEK_END); | |
| 58 | *len = ftell(f); | |
| 59 | fseek(f, 0, SEEK_SET); | |
| 60 | char* buf = malloc(*len + 1); | |
| 61 | size_t n = fread(buf, 1, *len, f); | |
| 62 | buf[n] = '\0'; | |
| 63 | *len = n; | |
| 64 | fclose(f); | |
| 65 | return buf; | |
| 66 | } | |
| 67 | ||
| 68 | /* ─── Timing ─── */ | |
| 69 | ||
| 70 | double now_ms(void) | |
| 71 | { | |
| 72 | struct timespec ts; | |
| 73 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 74 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; | |
| 75 | } | |
| 76 | ||
| 77 | /* ─── Helper: Concatenate sources ─── */ | |
| 78 | ||
| 79 | char* concat_sources(const char** srcs, size_t* lens, int n, size_t* total) | |
| 80 | { | |
| 81 | *total = 0; | |
| 82 | for (int i = 0; i < n; i++) *total += lens[i] + 1; | |
| 83 | char* buf = malloc(*total + 1); | |
| 84 | size_t pos = 0; | |
| 85 | for (int i = 0; i < n; i++) { | |
| 86 | memcpy(buf + pos, srcs[i], lens[i]); | |
| 87 | pos += lens[i]; | |
| 88 | buf[pos++] = '\n'; | |
| 89 | } | |
| 90 | buf[pos] = '\0'; | |
| 91 | *total = pos; | |
| 92 | return buf; | |
| 93 | } | |
| 94 | ||
| 95 | /* ─── Helper: reset cosine pipeline buffers ─── */ | |
| 96 | ||
| 97 | void reset_cosine_buffers(cl_command_queue queue, | |
| 98 | cl_mem djh_ht_keys, cl_mem djh_ht_values, | |
| 99 | cl_mem sec_chain_next, cl_mem word_norm_sq, | |
| 100 | cl_mem cand_ht_keys, cl_mem cand_ht_values, | |
| 101 | cl_mem cand_dot, cl_mem cand_cosine, | |
| 102 | cl_mem cand_next_free) | |
| 103 | { | |
| 104 | uint8_t pat_ff = 0xFF; | |
| 105 | uint8_t pat_00 = 0x00; | |
| 106 | uint32_t zero = 0; | |
| 107 | ||
| 108 | /* Disjunct reverse index HT */ | |
| 109 | clEnqueueFillBuffer(queue, djh_ht_keys, &pat_ff, 1, 0, | |
| 110 | sizeof(uint64_t) * DJH_HT_CAPACITY, 0, NULL, NULL); | |
| 111 | clEnqueueFillBuffer(queue, djh_ht_values, &pat_ff, 1, 0, | |
| 112 | sizeof(uint32_t) * DJH_HT_CAPACITY, 0, NULL, NULL); | |
| 113 | ||
| 114 | /* Section chain pointers */ | |
| 115 | clEnqueueFillBuffer(queue, sec_chain_next, &pat_ff, 1, 0, | |
| 116 | sizeof(uint32_t) * SECTION_CAPACITY, 0, NULL, NULL); | |
| 117 | ||
| 118 | /* Word norms */ | |
| 119 | clEnqueueFillBuffer(queue, word_norm_sq, &pat_00, 1, 0, | |
| 120 | sizeof(double) * WORD_CAPACITY, 0, NULL, NULL); | |
| 121 | ||
| 122 | /* Candidate HT */ | |
| 123 | clEnqueueFillBuffer(queue, cand_ht_keys, &pat_ff, 1, 0, | |
| 124 | sizeof(uint64_t) * CANDIDATE_HT_CAPACITY, 0, NULL, NULL); | |
| 125 | clEnqueueFillBuffer(queue, cand_ht_values, &pat_ff, 1, 0, | |
| 126 | sizeof(uint32_t) * CANDIDATE_HT_CAPACITY, 0, NULL, NULL); | |
| 127 | ||
| 128 | /* Candidate pool */ | |
| 129 | clEnqueueFillBuffer(queue, cand_dot, &pat_00, 1, 0, | |
| 130 | sizeof(double) * CANDIDATE_CAPACITY, 0, NULL, NULL); | |
| 131 | clEnqueueFillBuffer(queue, cand_cosine, &pat_00, 1, 0, | |
| 132 | sizeof(double) * CANDIDATE_CAPACITY, 0, NULL, NULL); | |
| 133 | clEnqueueWriteBuffer(queue, cand_next_free, CL_FALSE, 0, | |
| 134 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 135 | ||
| 136 | clFinish(queue); | |
| 137 | } | |
| 138 | ||
| 139 | /* ─── Helper: reset section pool ─── */ | |
| 140 | ||
| 141 | void reset_section_pool(cl_command_queue queue, | |
| 142 | cl_mem sht_keys, cl_mem sht_values, | |
| 143 | cl_mem sec_count, cl_mem sec_next_free) | |
| 144 | { | |
| 145 | uint8_t pat_ff = 0xFF; | |
| 146 | uint8_t pat_00 = 0x00; | |
| 147 | uint32_t zero = 0; | |
| 148 | ||
| 149 | clEnqueueFillBuffer(queue, sht_keys, &pat_ff, 1, 0, | |
| 150 | sizeof(uint64_t) * SECTION_HT_CAPACITY, 0, NULL, NULL); | |
| 151 | clEnqueueFillBuffer(queue, sht_values, &pat_ff, 1, 0, | |
| 152 | sizeof(uint32_t) * SECTION_HT_CAPACITY, 0, NULL, NULL); | |
| 153 | clEnqueueFillBuffer(queue, sec_count, &pat_00, 1, 0, | |
| 154 | sizeof(double) * SECTION_CAPACITY, 0, NULL, NULL); | |
| 155 | clEnqueueWriteBuffer(queue, sec_next_free, CL_FALSE, 0, | |
| 156 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 157 | clFinish(queue); | |
| 158 | } | |
| 159 | ||
| 160 | /* ─── Helper: manually populate sections ─── */ | |
| 161 | ||
| 162 | void upload_sections(cl_command_queue queue, | |
| 163 | cl_mem sec_word, cl_mem sec_disjunct_hash, cl_mem sec_count, | |
| 164 | cl_mem sec_next_free, | |
| 165 | uint32_t* words, uint64_t* djhs, double* counts, uint32_t n) | |
| 166 | { | |
| 167 | clEnqueueWriteBuffer(queue, sec_word, CL_FALSE, 0, | |
| 168 | sizeof(uint32_t) * n, words, 0, NULL, NULL); | |
| 169 | clEnqueueWriteBuffer(queue, sec_disjunct_hash, CL_FALSE, 0, | |
| 170 | sizeof(uint64_t) * n, djhs, 0, NULL, NULL); | |
| 171 | clEnqueueWriteBuffer(queue, sec_count, CL_FALSE, 0, | |
| 172 | sizeof(double) * n, counts, 0, NULL, NULL); | |
| 173 | clEnqueueWriteBuffer(queue, sec_next_free, CL_FALSE, 0, | |
| 174 | sizeof(uint32_t), &n, 0, NULL, NULL); | |
| 175 | clFinish(queue); | |
| 176 | } | |
| 177 | ||
| 178 | /* ─── Helper: run cosine pipeline ─── */ | |
| 179 | ||
| 180 | void run_cosine_pipeline(cl_command_queue queue, | |
| 181 | cl_kernel k_norms, cl_kernel k_chains, | |
| 182 | cl_kernel k_dots, cl_kernel k_cosines, | |
| 183 | uint32_t num_sections, | |
| 184 | cl_mem cand_next_free, uint32_t* out_num_candidates) | |
| 185 | { | |
| 186 | size_t local = 256; | |
| 187 | size_t gs; | |
| 188 | cl_int err; | |
| 189 | ||
| 190 | /* Step 1: Word norms */ | |
| 191 | gs = ((num_sections + local - 1) / local) * local; | |
| 192 | err = clEnqueueNDRangeKernel(queue, k_norms, 1, NULL, | |
| 193 | &gs, &local, 0, NULL, NULL); | |
| 194 | CL_CHECK(err, "enqueue compute_word_norms"); | |
| 195 | ||
| 196 | /* Step 2: Disjunct chains */ | |
| 197 | err = clEnqueueNDRangeKernel(queue, k_chains, 1, NULL, | |
| 198 | &gs, &local, 0, NULL, NULL); | |
| 199 | CL_CHECK(err, "enqueue build_disjunct_chains"); | |
| 200 | ||
| 201 | /* Step 3: Dot products */ | |
| 202 | err = clEnqueueNDRangeKernel(queue, k_dots, 1, NULL, | |
| 203 | &gs, &local, 0, NULL, NULL); | |
| 204 | CL_CHECK(err, "enqueue accumulate_dot_products"); | |
| 205 | ||
| 206 | clFinish(queue); | |
| 207 | ||
| 208 | /* Read candidate count */ | |
| 209 | clEnqueueReadBuffer(queue, cand_next_free, CL_TRUE, 0, | |
| 210 | sizeof(uint32_t), out_num_candidates, 0, NULL, NULL); | |
| 211 | ||
| 212 | if (*out_num_candidates == 0) return; | |
| 213 | ||
| 214 | /* Step 4: Cosines */ | |
| 215 | gs = ((*out_num_candidates + local - 1) / local) * local; | |
| 216 | cl_uint nc = *out_num_candidates; | |
| 217 | clSetKernelArg(k_cosines, 5, sizeof(cl_uint), &nc); | |
| 218 | err = clEnqueueNDRangeKernel(queue, k_cosines, 1, NULL, | |
| 219 | &gs, &local, 0, NULL, NULL); | |
| 220 | CL_CHECK(err, "enqueue compute_cosines"); | |
| 221 | ||
| 222 | clFinish(queue); | |
| 223 | } | |
| 224 | ||
| 225 | /* ─── Main ─── */ | |
| 226 | ||
| 227 | int main(int argc, char** argv) | |
| 228 | { | |
| 229 | cl_int err; | |
| 230 | int pass_count = 0, fail_count = 0; | |
| 231 | ||
| 232 | printf("=== GPU Cosine Similarity Test ===\n\n"); | |
| 233 | ||
| 234 | /* ─── OpenCL setup ─── */ | |
| 235 | ||
| 236 | cl_platform_id platform; | |
| 237 | err = clGetPlatformIDs(1, &platform, NULL); | |
| 238 | CL_CHECK(err, "platform"); | |
| 239 | ||
| 240 | cl_device_id device; | |
| 241 | err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL); | |
| 242 | CL_CHECK(err, "device"); | |
| 243 | ||
| 244 | char dev_name[256]; | |
| 245 | clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(dev_name), dev_name, NULL); | |
| 246 | printf("GPU: %s\n", dev_name); | |
| 247 | ||
| 248 | cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err); | |
| 249 | CL_CHECK(err, "context"); | |
| 250 | ||
| 251 | cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err); | |
| 252 | CL_CHECK(err, "queue"); | |
| 253 | ||
| 254 | /* ─── Load and concatenate kernel sources ─── */ | |
| 255 | ||
| 256 | size_t len_ht, len_as, len_sc, len_cos; | |
| 257 | char* src_ht = read_file("opencog/gpu/gpu-hashtable.cl", &len_ht); | |
| 258 | char* src_as = read_file("opencog/gpu/gpu-atomspace.cl", &len_as); | |
| 259 | char* src_sc = read_file("opencog/gpu/gpu-sections.cl", &len_sc); | |
| 260 | char* src_cos = read_file("opencog/gpu/gpu-cosine.cl", &len_cos); | |
| 261 | ||
| 262 | const char* srcs[] = {src_ht, src_as, src_sc, src_cos}; | |
| 263 | size_t lens[] = {len_ht, len_as, len_sc, len_cos}; | |
| 264 | size_t total_len; | |
| 265 | char* combined = concat_sources(srcs, lens, 4, &total_len); | |
| 266 | ||
| 267 | cl_program program = clCreateProgramWithSource(ctx, 1, | |
| 268 | (const char**)&combined, &total_len, &err); | |
| 269 | CL_CHECK(err, "create program"); | |
| 270 | ||
| 271 | char build_opts[1024]; | |
| 272 | snprintf(build_opts, sizeof(build_opts), | |
| 273 | "-cl-std=CL1.2 " | |
| 274 | "-DWORD_CAPACITY=%d " | |
| 275 | "-DPAIR_CAPACITY=%d " | |
| 276 | "-DSECTION_CAPACITY=%d " | |
| 277 | "-DWORD_HT_CAPACITY=%d " | |
| 278 | "-DPAIR_HT_CAPACITY=%d " | |
| 279 | "-DSECTION_HT_CAPACITY=%d " | |
| 280 | "-DDJH_HT_CAPACITY=%d " | |
| 281 | "-DCANDIDATE_CAPACITY=%d " | |
| 282 | "-DCANDIDATE_HT_CAPACITY=%d", | |
| 283 | WORD_CAPACITY, PAIR_CAPACITY, SECTION_CAPACITY, | |
| 284 | WORD_HT_CAPACITY, PAIR_HT_CAPACITY, SECTION_HT_CAPACITY, | |
| 285 | DJH_HT_CAPACITY, CANDIDATE_CAPACITY, CANDIDATE_HT_CAPACITY); | |
| 286 | ||
| 287 | err = clBuildProgram(program, 1, &device, build_opts, NULL, NULL); | |
| 288 | if (err != CL_SUCCESS) { | |
| 289 | char log[16384]; | |
| 290 | clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, | |
| 291 | sizeof(log), log, NULL); | |
| 292 | fprintf(stderr, "Build error:\n%s\n", log); | |
| 293 | return 1; | |
| 294 | } | |
| 295 | printf("Kernels compiled successfully\n\n"); | |
| 296 | ||
| 297 | /* ─── Create kernels ─── */ | |
| 298 | ||
| 299 | cl_kernel k_norms = clCreateKernel(program, "compute_word_norms", &err); | |
| 300 | CL_CHECK(err, "kernel compute_word_norms"); | |
| 301 | cl_kernel k_chains = clCreateKernel(program, "build_disjunct_chains", &err); | |
| 302 | CL_CHECK(err, "kernel build_disjunct_chains"); | |
| 303 | cl_kernel k_dots = clCreateKernel(program, "accumulate_dot_products", &err); | |
| 304 | CL_CHECK(err, "kernel accumulate_dot_products"); | |
| 305 | cl_kernel k_cosines = clCreateKernel(program, "compute_cosines", &err); | |
| 306 | CL_CHECK(err, "kernel compute_cosines"); | |
| 307 | cl_kernel k_filter = clCreateKernel(program, "filter_candidates", &err); | |
| 308 | CL_CHECK(err, "kernel filter_candidates"); | |
| 309 | cl_kernel k_extract = clCreateKernel(program, "extract_sections", &err); | |
| 310 | CL_CHECK(err, "kernel extract_sections"); | |
| 311 | ||
| 312 | /* ─── Allocate GPU buffers ─── */ | |
| 313 | ||
| 314 | printf("Allocating GPU buffers...\n"); | |
| 315 | uint32_t zero = 0; | |
| 316 | ||
| 317 | /* Section pool */ | |
| 318 | cl_mem sec_word = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 319 | sizeof(uint32_t) * SECTION_CAPACITY, NULL, &err); | |
| 320 | cl_mem sec_disjunct_hash = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 321 | sizeof(uint64_t) * SECTION_CAPACITY, NULL, &err); | |
| 322 | cl_mem sec_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 323 | sizeof(double) * SECTION_CAPACITY, NULL, &err); | |
| 324 | cl_mem sec_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 325 | sizeof(uint32_t), &zero, &err); | |
| 326 | ||
| 327 | /* Section hash table (for extract_sections) */ | |
| 328 | cl_mem sht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 329 | sizeof(uint64_t) * SECTION_HT_CAPACITY, NULL, &err); | |
| 330 | cl_mem sht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 331 | sizeof(uint32_t) * SECTION_HT_CAPACITY, NULL, &err); | |
| 332 | ||
| 333 | /* Total sections counter (for extract_sections) */ | |
| 334 | cl_mem total_sections_created = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 335 | sizeof(uint32_t), &zero, &err); | |
| 336 | ||
| 337 | /* Disjunct reverse index HT */ | |
| 338 | cl_mem djh_ht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 339 | sizeof(uint64_t) * DJH_HT_CAPACITY, NULL, &err); | |
| 340 | CL_CHECK(err, "djh_ht_keys"); | |
| 341 | cl_mem djh_ht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 342 | sizeof(uint32_t) * DJH_HT_CAPACITY, NULL, &err); | |
| 343 | CL_CHECK(err, "djh_ht_values"); | |
| 344 | ||
| 345 | /* Section chain pointers */ | |
| 346 | cl_mem sec_chain_next = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 347 | sizeof(uint32_t) * SECTION_CAPACITY, NULL, &err); | |
| 348 | CL_CHECK(err, "sec_chain_next"); | |
| 349 | ||
| 350 | /* Word norms */ | |
| 351 | cl_mem word_norm_sq = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 352 | sizeof(double) * WORD_CAPACITY, NULL, &err); | |
| 353 | CL_CHECK(err, "word_norm_sq"); | |
| 354 | ||
| 355 | /* Candidate HT */ | |
| 356 | cl_mem cand_ht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 357 | sizeof(uint64_t) * CANDIDATE_HT_CAPACITY, NULL, &err); | |
| 358 | cl_mem cand_ht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 359 | sizeof(uint32_t) * CANDIDATE_HT_CAPACITY, NULL, &err); | |
| 360 | ||
| 361 | /* Candidate pool */ | |
| 362 | cl_mem cand_word_a = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 363 | sizeof(uint32_t) * CANDIDATE_CAPACITY, NULL, &err); | |
| 364 | cl_mem cand_word_b = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 365 | sizeof(uint32_t) * CANDIDATE_CAPACITY, NULL, &err); | |
| 366 | cl_mem cand_dot = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 367 | sizeof(double) * CANDIDATE_CAPACITY, NULL, &err); | |
| 368 | cl_mem cand_cosine = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 369 | sizeof(double) * CANDIDATE_CAPACITY, NULL, &err); | |
| 370 | cl_mem cand_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 371 | sizeof(uint32_t), &zero, &err); | |
| 372 | ||
| 373 | /* ─── Set kernel args (persistent across tests) ─── */ | |
| 374 | ||
| 375 | /* compute_word_norms: (sec_word, sec_count, word_norm_sq, num_sections) */ | |
| 376 | clSetKernelArg(k_norms, 0, sizeof(cl_mem), &sec_word); | |
| 377 | clSetKernelArg(k_norms, 1, sizeof(cl_mem), &sec_count); | |
| 378 | clSetKernelArg(k_norms, 2, sizeof(cl_mem), &word_norm_sq); | |
| 379 | /* arg 3 = num_sections — set per test */ | |
| 380 | ||
| 381 | /* build_disjunct_chains: (sec_djh, sec_count, djh_ht_keys, djh_ht_values, | |
| 382 | * sec_chain_next, num_sections) */ | |
| 383 | clSetKernelArg(k_chains, 0, sizeof(cl_mem), &sec_disjunct_hash); | |
| 384 | clSetKernelArg(k_chains, 1, sizeof(cl_mem), &sec_count); | |
| 385 | clSetKernelArg(k_chains, 2, sizeof(cl_mem), &djh_ht_keys); | |
| 386 | clSetKernelArg(k_chains, 3, sizeof(cl_mem), &djh_ht_values); | |
| 387 | clSetKernelArg(k_chains, 4, sizeof(cl_mem), &sec_chain_next); | |
| 388 | /* arg 5 = num_sections — set per test */ | |
| 389 | ||
| 390 | /* accumulate_dot_products: 14 args */ | |
| 391 | clSetKernelArg(k_dots, 0, sizeof(cl_mem), &sec_word); | |
| 392 | clSetKernelArg(k_dots, 1, sizeof(cl_mem), &sec_disjunct_hash); | |
| 393 | clSetKernelArg(k_dots, 2, sizeof(cl_mem), &sec_count); | |
| 394 | clSetKernelArg(k_dots, 3, sizeof(cl_mem), &djh_ht_keys); | |
| 395 | clSetKernelArg(k_dots, 4, sizeof(cl_mem), &djh_ht_values); | |
| 396 | clSetKernelArg(k_dots, 5, sizeof(cl_mem), &sec_chain_next); | |
| 397 | clSetKernelArg(k_dots, 6, sizeof(cl_mem), &cand_ht_keys); | |
| 398 | clSetKernelArg(k_dots, 7, sizeof(cl_mem), &cand_ht_values); | |
| 399 | clSetKernelArg(k_dots, 8, sizeof(cl_mem), &cand_word_a); | |
| 400 | clSetKernelArg(k_dots, 9, sizeof(cl_mem), &cand_word_b); | |
| 401 | clSetKernelArg(k_dots, 10, sizeof(cl_mem), &cand_dot); | |
| 402 | clSetKernelArg(k_dots, 11, sizeof(cl_mem), &cand_next_free); | |
| 403 | /* arg 12 = num_sections — set per test */ | |
| 404 | ||
| 405 | /* compute_cosines: (cand_word_a, cand_word_b, cand_dot, cand_cosine, | |
| 406 | * word_norm_sq, num_candidates) */ | |
| 407 | clSetKernelArg(k_cosines, 0, sizeof(cl_mem), &cand_word_a); | |
| 408 | clSetKernelArg(k_cosines, 1, sizeof(cl_mem), &cand_word_b); | |
| 409 | clSetKernelArg(k_cosines, 2, sizeof(cl_mem), &cand_dot); | |
| 410 | clSetKernelArg(k_cosines, 3, sizeof(cl_mem), &cand_cosine); | |
| 411 | clSetKernelArg(k_cosines, 4, sizeof(cl_mem), &word_norm_sq); | |
| 412 | /* arg 5 = num_candidates — set in pipeline */ | |
| 413 | ||
| 414 | /* filter_candidates */ | |
| 415 | clSetKernelArg(k_filter, 0, sizeof(cl_mem), &cand_word_a); | |
| 416 | clSetKernelArg(k_filter, 1, sizeof(cl_mem), &cand_word_b); | |
| 417 | clSetKernelArg(k_filter, 2, sizeof(cl_mem), &cand_cosine); | |
| 418 | /* args 3-8 set per test */ | |
| 419 | ||
| 420 | printf("GPU buffers ready\n\n"); | |
| 421 | ||
| 422 | /* ═══════════════════════════════════════════════════════════════ | |
| 423 | * TEST 1: Known cosine (2 words, 4 sections) | |
| 424 | * | |
| 425 | * Section 0: word=0, djh=0x100, count=3.0 | |
| 426 | * Section 1: word=0, djh=0x200, count=4.0 | |
| 427 | * Section 2: word=1, djh=0x100, count=5.0 | |
| 428 | * Section 3: word=1, djh=0x300, count=2.0 | |
| 429 | * | |
| 430 | * word 0: {0x100: 3, 0x200: 4} → norm² = 25, norm = 5 | |
| 431 | * word 1: {0x100: 5, 0x300: 2} → norm² = 29, norm = √29 | |
| 432 | * Shared: 0x100 → dot = 3×5 = 15 | |
| 433 | * Cosine = 15 / (5 × √29) = 0.5571 | |
| 434 | * ═══════════════════════════════════════════════════════════════ */ | |
| 435 | ||
| 436 | printf("--- Test 1: Known cosine (2 words, 4 sections) ---\n"); | |
| 437 | ||
| 438 | reset_cosine_buffers(queue, djh_ht_keys, djh_ht_values, | |
| 439 | sec_chain_next, word_norm_sq, cand_ht_keys, cand_ht_values, | |
| 440 | cand_dot, cand_cosine, cand_next_free); | |
| 441 | ||
| 442 | { | |
| 443 | uint32_t sw[] = {0, 0, 1, 1}; | |
| 444 | uint64_t sd[] = {0x100, 0x200, 0x100, 0x300}; | |
| 445 | double sc[] = {3.0, 4.0, 5.0, 2.0}; | |
| 446 | cl_uint ns = 4; | |
| 447 | ||
| 448 | upload_sections(queue, sec_word, sec_disjunct_hash, sec_count, | |
| 449 | sec_next_free, sw, sd, sc, ns); | |
| 450 | ||
| 451 | /* Set num_sections for each kernel */ | |
| 452 | clSetKernelArg(k_norms, 3, sizeof(cl_uint), &ns); | |
| 453 | clSetKernelArg(k_chains, 5, sizeof(cl_uint), &ns); | |
| 454 | clSetKernelArg(k_dots, 12, sizeof(cl_uint), &ns); | |
| 455 | ||
| 456 | uint32_t num_cands = 0; | |
| 457 | double t0 = now_ms(); | |
| 458 | run_cosine_pipeline(queue, k_norms, k_chains, k_dots, k_cosines, | |
| 459 | ns, cand_next_free, &num_cands); | |
| 460 | double t1 = now_ms(); | |
| 461 | ||
| 462 | /* Read results */ | |
| 463 | uint32_t h_wa[4], h_wb[4]; | |
| 464 | double h_dot[4], h_cos[4]; | |
| 465 | if (num_cands > 0) { | |
| 466 | clEnqueueReadBuffer(queue, cand_word_a, CL_TRUE, 0, | |
| 467 | sizeof(uint32_t) * num_cands, h_wa, 0, NULL, NULL); | |
| 468 | clEnqueueReadBuffer(queue, cand_word_b, CL_TRUE, 0, | |
| 469 | sizeof(uint32_t) * num_cands, h_wb, 0, NULL, NULL); | |
| 470 | clEnqueueReadBuffer(queue, cand_dot, CL_TRUE, 0, | |
| 471 | sizeof(double) * num_cands, h_dot, 0, NULL, NULL); | |
| 472 | clEnqueueReadBuffer(queue, cand_cosine, CL_TRUE, 0, | |
| 473 | sizeof(double) * num_cands, h_cos, 0, NULL, NULL); | |
| 474 | } | |
| 475 | ||
| 476 | /* Expected: 1 candidate pair (0,1) with dot=15.0, cosine≈0.5571 */ | |
| 477 | double expected_cos = 15.0 / (5.0 * sqrt(29.0)); | |
| 478 | ||
| 479 | printf(" Candidates: %u (expected 1)\n", num_cands); | |
| 480 | if (num_cands > 0) { | |
| 481 | printf(" Pair: (%u, %u) dot=%.1f cosine=%.4f\n", | |
| 482 | h_wa[0], h_wb[0], h_dot[0], h_cos[0]); | |
| 483 | printf(" Expected: (0, 1) dot=15.0 cosine=%.4f\n", expected_cos); | |
| 484 | } | |
| 485 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 486 | ||
| 487 | int pass = (num_cands == 1) && | |
| 488 | (h_wa[0] == 0) && (h_wb[0] == 1) && | |
| 489 | (fabs(h_dot[0] - 15.0) < 0.01) && | |
| 490 | (fabs(h_cos[0] - expected_cos) < 0.001); | |
| 491 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 492 | if (pass) pass_count++; else fail_count++; | |
| 493 | } | |
| 494 | ||
| 495 | /* ═══════════════════════════════════════════════════════════════ | |
| 496 | * TEST 2: Three words — all pairwise cosines | |
| 497 | * | |
| 498 | * word 0: {X=0x10: 1, Y=0x20: 2} norm² = 5 | |
| 499 | * word 1: {X=0x10: 3, Z=0x30: 1} norm² = 10 | |
| 500 | * word 2: {Y=0x20: 2, Z=0x30: 4} norm² = 20 | |
| 501 | * | |
| 502 | * dot(0,1) = 1×3 = 3 cos = 3/√50 ≈ 0.4243 | |
| 503 | * dot(0,2) = 2×2 = 4 cos = 4/√100 = 0.4000 | |
| 504 | * dot(1,2) = 1×4 = 4 cos = 4/√200 ≈ 0.2828 | |
| 505 | * ═══════════════════════════════════════════════════════════════ */ | |
| 506 | ||
| 507 | printf("--- Test 2: Three words, all pairwise cosines ---\n"); | |
| 508 | ||
| 509 | reset_cosine_buffers(queue, djh_ht_keys, djh_ht_values, | |
| 510 | sec_chain_next, word_norm_sq, cand_ht_keys, cand_ht_values, | |
| 511 | cand_dot, cand_cosine, cand_next_free); | |
| 512 | ||
| 513 | { | |
| 514 | uint32_t sw[] = {0, 0, 1, 1, 2, 2}; | |
| 515 | uint64_t sd[] = {0x10, 0x20, 0x10, 0x30, 0x20, 0x30}; | |
| 516 | double sc[] = {1.0, 2.0, 3.0, 1.0, 2.0, 4.0}; | |
| 517 | cl_uint ns = 6; | |
| 518 | ||
| 519 | upload_sections(queue, sec_word, sec_disjunct_hash, sec_count, | |
| 520 | sec_next_free, sw, sd, sc, ns); | |
| 521 | ||
| 522 | clSetKernelArg(k_norms, 3, sizeof(cl_uint), &ns); | |
| 523 | clSetKernelArg(k_chains, 5, sizeof(cl_uint), &ns); | |
| 524 | clSetKernelArg(k_dots, 12, sizeof(cl_uint), &ns); | |
| 525 | ||
| 526 | uint32_t num_cands = 0; | |
| 527 | double t0 = now_ms(); | |
| 528 | run_cosine_pipeline(queue, k_norms, k_chains, k_dots, k_cosines, | |
| 529 | ns, cand_next_free, &num_cands); | |
| 530 | double t1 = now_ms(); | |
| 531 | ||
| 532 | uint32_t h_wa[8], h_wb[8]; | |
| 533 | double h_dot[8], h_cos[8]; | |
| 534 | if (num_cands > 0) { | |
| 535 | clEnqueueReadBuffer(queue, cand_word_a, CL_TRUE, 0, | |
| 536 | sizeof(uint32_t) * num_cands, h_wa, 0, NULL, NULL); | |
| 537 | clEnqueueReadBuffer(queue, cand_word_b, CL_TRUE, 0, | |
| 538 | sizeof(uint32_t) * num_cands, h_wb, 0, NULL, NULL); | |
| 539 | clEnqueueReadBuffer(queue, cand_dot, CL_TRUE, 0, | |
| 540 | sizeof(double) * num_cands, h_dot, 0, NULL, NULL); | |
| 541 | clEnqueueReadBuffer(queue, cand_cosine, CL_TRUE, 0, | |
| 542 | sizeof(double) * num_cands, h_cos, 0, NULL, NULL); | |
| 543 | } | |
| 544 | ||
| 545 | double exp_01 = 3.0 / sqrt(50.0); | |
| 546 | double exp_02 = 4.0 / sqrt(100.0); | |
| 547 | double exp_12 = 4.0 / sqrt(200.0); | |
| 548 | ||
| 549 | printf(" Candidates: %u (expected 3)\n", num_cands); | |
| 550 | ||
| 551 | /* Find each pair in results */ | |
| 552 | double got_01 = -1, got_02 = -1, got_12 = -1; | |
| 553 | double got_dot_01 = -1, got_dot_02 = -1, got_dot_12 = -1; | |
| 554 | for (uint32_t i = 0; i < num_cands; i++) { | |
| 555 | if (h_wa[i] == 0 && h_wb[i] == 1) | |
| 556 | { got_01 = h_cos[i]; got_dot_01 = h_dot[i]; } | |
| 557 | if (h_wa[i] == 0 && h_wb[i] == 2) | |
| 558 | { got_02 = h_cos[i]; got_dot_02 = h_dot[i]; } | |
| 559 | if (h_wa[i] == 1 && h_wb[i] == 2) | |
| 560 | { got_12 = h_cos[i]; got_dot_12 = h_dot[i]; } | |
| 561 | } | |
| 562 | ||
| 563 | printf(" (0,1): dot=%.1f cos=%.4f (exp dot=3.0 cos=%.4f)\n", | |
| 564 | got_dot_01, got_01, exp_01); | |
| 565 | printf(" (0,2): dot=%.1f cos=%.4f (exp dot=4.0 cos=%.4f)\n", | |
| 566 | got_dot_02, got_02, exp_02); | |
| 567 | printf(" (1,2): dot=%.1f cos=%.4f (exp dot=4.0 cos=%.4f)\n", | |
| 568 | got_dot_12, got_12, exp_12); | |
| 569 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 570 | ||
| 571 | int pass = (num_cands == 3) && | |
| 572 | (fabs(got_01 - exp_01) < 0.001) && | |
| 573 | (fabs(got_02 - exp_02) < 0.001) && | |
| 574 | (fabs(got_12 - exp_12) < 0.001); | |
| 575 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 576 | if (pass) pass_count++; else fail_count++; | |
| 577 | } | |
| 578 | ||
| 579 | /* ═══════════════════════════════════════════════════════════════ | |
| 580 | * TEST 3: Identical vectors → cosine = 1.0 | |
| 581 | * | |
| 582 | * word 0: {X=0x10: 3, Y=0x20: 4} | |
| 583 | * word 1: {X=0x10: 3, Y=0x20: 4} | |
| 584 | * | |
| 585 | * dot = 9+16 = 25, norms = 5 each, cosine = 25/25 = 1.0 | |
| 586 | * ═══════════════════════════════════════════════════════════════ */ | |
| 587 | ||
| 588 | printf("--- Test 3: Identical vectors → cosine = 1.0 ---\n"); | |
| 589 | ||
| 590 | reset_cosine_buffers(queue, djh_ht_keys, djh_ht_values, | |
| 591 | sec_chain_next, word_norm_sq, cand_ht_keys, cand_ht_values, | |
| 592 | cand_dot, cand_cosine, cand_next_free); | |
| 593 | ||
| 594 | { | |
| 595 | uint32_t sw[] = {0, 0, 1, 1}; | |
| 596 | uint64_t sd[] = {0x10, 0x20, 0x10, 0x20}; | |
| 597 | double sc[] = {3.0, 4.0, 3.0, 4.0}; | |
| 598 | cl_uint ns = 4; | |
| 599 | ||
| 600 | upload_sections(queue, sec_word, sec_disjunct_hash, sec_count, | |
| 601 | sec_next_free, sw, sd, sc, ns); | |
| 602 | ||
| 603 | clSetKernelArg(k_norms, 3, sizeof(cl_uint), &ns); | |
| 604 | clSetKernelArg(k_chains, 5, sizeof(cl_uint), &ns); | |
| 605 | clSetKernelArg(k_dots, 12, sizeof(cl_uint), &ns); | |
| 606 | ||
| 607 | uint32_t num_cands = 0; | |
| 608 | run_cosine_pipeline(queue, k_norms, k_chains, k_dots, k_cosines, | |
| 609 | ns, cand_next_free, &num_cands); | |
| 610 | ||
| 611 | double h_cos = 0; | |
| 612 | if (num_cands > 0) { | |
| 613 | clEnqueueReadBuffer(queue, cand_cosine, CL_TRUE, 0, | |
| 614 | sizeof(double), &h_cos, 0, NULL, NULL); | |
| 615 | } | |
| 616 | ||
| 617 | printf(" Candidates: %u (expected 1)\n", num_cands); | |
| 618 | printf(" Cosine: %.4f (expected 1.0000)\n", h_cos); | |
| 619 | ||
| 620 | int pass = (num_cands == 1) && (fabs(h_cos - 1.0) < 0.001); | |
| 621 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 622 | if (pass) pass_count++; else fail_count++; | |
| 623 | } | |
| 624 | ||
| 625 | /* ═══════════════════════════════════════════════════════════════ | |
| 626 | * TEST 4: No shared disjuncts → 0 candidates | |
| 627 | * | |
| 628 | * word 0: {X=0x10: 1} | |
| 629 | * word 1: {Y=0x20: 1} | |
| 630 | * | |
| 631 | * No shared disjuncts → no chain overlap → 0 candidates | |
| 632 | * ═══════════════════════════════════════════════════════════════ */ | |
| 633 | ||
| 634 | printf("--- Test 4: No shared disjuncts → 0 candidates ---\n"); | |
| 635 | ||
| 636 | reset_cosine_buffers(queue, djh_ht_keys, djh_ht_values, | |
| 637 | sec_chain_next, word_norm_sq, cand_ht_keys, cand_ht_values, | |
| 638 | cand_dot, cand_cosine, cand_next_free); | |
| 639 | ||
| 640 | { | |
| 641 | uint32_t sw[] = {0, 1}; | |
| 642 | uint64_t sd[] = {0x10, 0x20}; | |
| 643 | double sc[] = {1.0, 1.0}; | |
| 644 | cl_uint ns = 2; | |
| 645 | ||
| 646 | upload_sections(queue, sec_word, sec_disjunct_hash, sec_count, | |
| 647 | sec_next_free, sw, sd, sc, ns); | |
| 648 | ||
| 649 | clSetKernelArg(k_norms, 3, sizeof(cl_uint), &ns); | |
| 650 | clSetKernelArg(k_chains, 5, sizeof(cl_uint), &ns); | |
| 651 | clSetKernelArg(k_dots, 12, sizeof(cl_uint), &ns); | |
| 652 | ||
| 653 | uint32_t num_cands = 0; | |
| 654 | run_cosine_pipeline(queue, k_norms, k_chains, k_dots, k_cosines, | |
| 655 | ns, cand_next_free, &num_cands); | |
| 656 | ||
| 657 | printf(" Candidates: %u (expected 0)\n", num_cands); | |
| 658 | ||
| 659 | int pass = (num_cands == 0); | |
| 660 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 661 | if (pass) pass_count++; else fail_count++; | |
| 662 | } | |
| 663 | ||
| 664 | /* ═══════════════════════════════════════════════════════════════ | |
| 665 | * TEST 5: Filter candidates above threshold | |
| 666 | * | |
| 667 | * Reuse test 2's scenario (3 words): | |
| 668 | * cos(0,1) ≈ 0.4243 | |
| 669 | * cos(0,2) = 0.4000 | |
| 670 | * cos(1,2) ≈ 0.2828 | |
| 671 | * | |
| 672 | * Filter at 0.35 → should get 2 candidates (0,1) and (0,2) | |
| 673 | * ═══════════════════════════════════════════════════════════════ */ | |
| 674 | ||
| 675 | printf("--- Test 5: Filter candidates above threshold ---\n"); | |
| 676 | ||
| 677 | reset_cosine_buffers(queue, djh_ht_keys, djh_ht_values, | |
| 678 | sec_chain_next, word_norm_sq, cand_ht_keys, cand_ht_values, | |
| 679 | cand_dot, cand_cosine, cand_next_free); | |
| 680 | ||
| 681 | { | |
| 682 | uint32_t sw[] = {0, 0, 1, 1, 2, 2}; | |
| 683 | uint64_t sd[] = {0x10, 0x20, 0x10, 0x30, 0x20, 0x30}; | |
| 684 | double sc[] = {1.0, 2.0, 3.0, 1.0, 2.0, 4.0}; | |
| 685 | cl_uint ns = 6; | |
| 686 | ||
| 687 | upload_sections(queue, sec_word, sec_disjunct_hash, sec_count, | |
| 688 | sec_next_free, sw, sd, sc, ns); | |
| 689 | ||
| 690 | clSetKernelArg(k_norms, 3, sizeof(cl_uint), &ns); | |
| 691 | clSetKernelArg(k_chains, 5, sizeof(cl_uint), &ns); | |
| 692 | clSetKernelArg(k_dots, 12, sizeof(cl_uint), &ns); | |
| 693 | ||
| 694 | uint32_t num_cands = 0; | |
| 695 | run_cosine_pipeline(queue, k_norms, k_chains, k_dots, k_cosines, | |
| 696 | ns, cand_next_free, &num_cands); | |
| 697 | ||
| 698 | /* Now filter */ | |
| 699 | cl_double threshold = 0.35; | |
| 700 | cl_uint max_output = 64; | |
| 701 | cl_mem out_wa = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 702 | sizeof(uint32_t) * max_output, NULL, &err); | |
| 703 | cl_mem out_wb = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 704 | sizeof(uint32_t) * max_output, NULL, &err); | |
| 705 | cl_mem out_cos = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 706 | sizeof(double) * max_output, NULL, &err); | |
| 707 | cl_mem out_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 708 | sizeof(uint32_t), &zero, &err); | |
| 709 | ||
| 710 | clSetKernelArg(k_filter, 3, sizeof(cl_uint), &num_cands); | |
| 711 | clSetKernelArg(k_filter, 4, sizeof(cl_double), &threshold); | |
| 712 | clSetKernelArg(k_filter, 5, sizeof(cl_mem), &out_wa); | |
| 713 | clSetKernelArg(k_filter, 6, sizeof(cl_mem), &out_wb); | |
| 714 | clSetKernelArg(k_filter, 7, sizeof(cl_mem), &out_cos); | |
| 715 | clSetKernelArg(k_filter, 8, sizeof(cl_mem), &out_count); | |
| 716 | clSetKernelArg(k_filter, 9, sizeof(cl_uint), &max_output); | |
| 717 | ||
| 718 | size_t local = 256; | |
| 719 | size_t gs = ((num_cands + local - 1) / local) * local; | |
| 720 | err = clEnqueueNDRangeKernel(queue, k_filter, 1, NULL, | |
| 721 | &gs, &local, 0, NULL, NULL); | |
| 722 | CL_CHECK(err, "enqueue filter"); | |
| 723 | clFinish(queue); | |
| 724 | ||
| 725 | uint32_t n_filtered; | |
| 726 | clEnqueueReadBuffer(queue, out_count, CL_TRUE, 0, | |
| 727 | sizeof(uint32_t), &n_filtered, 0, NULL, NULL); | |
| 728 | ||
| 729 | uint32_t fwa[8], fwb[8]; | |
| 730 | double fcos[8]; | |
| 731 | if (n_filtered > 0) { | |
| 732 | clEnqueueReadBuffer(queue, out_wa, CL_TRUE, 0, | |
| 733 | sizeof(uint32_t) * n_filtered, fwa, 0, NULL, NULL); | |
| 734 | clEnqueueReadBuffer(queue, out_wb, CL_TRUE, 0, | |
| 735 | sizeof(uint32_t) * n_filtered, fwb, 0, NULL, NULL); | |
| 736 | clEnqueueReadBuffer(queue, out_cos, CL_TRUE, 0, | |
| 737 | sizeof(double) * n_filtered, fcos, 0, NULL, NULL); | |
| 738 | } | |
| 739 | ||
| 740 | printf(" Total candidates: %u, filtered (>0.35): %u (expected 2)\n", | |
| 741 | num_cands, n_filtered); | |
| 742 | for (uint32_t i = 0; i < n_filtered; i++) { | |
| 743 | printf(" (%u, %u) cos=%.4f\n", fwa[i], fwb[i], fcos[i]); | |
| 744 | } | |
| 745 | ||
| 746 | /* cos(1,2) ≈ 0.2828 should be filtered out */ | |
| 747 | int pass = (n_filtered == 2); | |
| 748 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 749 | if (pass) pass_count++; else fail_count++; | |
| 750 | ||
| 751 | clReleaseMemObject(out_wa); | |
| 752 | clReleaseMemObject(out_wb); | |
| 753 | clReleaseMemObject(out_cos); | |
| 754 | clReleaseMemObject(out_count); | |
| 755 | } | |
| 756 | ||
| 757 | /* ═══════════════════════════════════════════════════════════════ | |
| 758 | * TEST 6: Benchmark — 1000 sentences → sections → cosines | |
| 759 | * | |
| 760 | * Full pipeline: | |
| 761 | * extract_sections (Phase 4) → cosine pipeline (Phase 5) | |
| 762 | * | |
| 763 | * 1000 sentences, 10-20 words each, chain MST parse. | |
| 764 | * 500 word vocabulary for realistic disjunct sharing. | |
| 765 | * ═══════════════════════════════════════════════════════════════ */ | |
| 766 | ||
| 767 | printf("--- Test 6: Benchmark (1000 sentences, full pipeline) ---\n"); | |
| 768 | ||
| 769 | /* Reset everything */ | |
| 770 | reset_section_pool(queue, sht_keys, sht_values, sec_count, sec_next_free); | |
| 771 | reset_cosine_buffers(queue, djh_ht_keys, djh_ht_values, | |
| 772 | sec_chain_next, word_norm_sq, cand_ht_keys, cand_ht_values, | |
| 773 | cand_dot, cand_cosine, cand_next_free); | |
| 774 | { | |
| 775 | uint32_t tsec_zero = 0; | |
| 776 | clEnqueueWriteBuffer(queue, total_sections_created, CL_TRUE, 0, | |
| 777 | sizeof(uint32_t), &tsec_zero, 0, NULL, NULL); | |
| 778 | } | |
| 779 | ||
| 780 | srand(42); | |
| 781 | uint32_t bench_ns = 1000; | |
| 782 | uint32_t vocab_size = 500; | |
| 783 | ||
| 784 | uint32_t max_words = bench_ns * 25; | |
| 785 | uint32_t max_edges = bench_ns * 25; | |
| 786 | uint32_t* b_words = malloc(sizeof(uint32_t) * max_words); | |
| 787 | uint32_t* b_sent_offsets = malloc(sizeof(uint32_t) * bench_ns); | |
| 788 | uint32_t* b_sent_lengths = malloc(sizeof(uint32_t) * bench_ns); | |
| 789 | uint32_t* b_edge_p1 = malloc(sizeof(uint32_t) * max_edges); | |
| 790 | uint32_t* b_edge_p2 = malloc(sizeof(uint32_t) * max_edges); | |
| 791 | uint32_t* b_edge_offsets = malloc(sizeof(uint32_t) * bench_ns); | |
| 792 | uint32_t* b_edge_counts = malloc(sizeof(uint32_t) * bench_ns); | |
| 793 | ||
| 794 | uint32_t word_pos = 0, edge_pos = 0; | |
| 795 | for (uint32_t s = 0; s < bench_ns; s++) { | |
| 796 | uint32_t slen = 10 + (rand() % 11); | |
| 797 | b_sent_offsets[s] = word_pos; | |
| 798 | b_sent_lengths[s] = slen; | |
| 799 | b_edge_offsets[s] = edge_pos; | |
| 800 | b_edge_counts[s] = slen - 1; | |
| 801 | ||
| 802 | for (uint32_t w = 0; w < slen; w++) | |
| 803 | b_words[word_pos++] = rand() % vocab_size; | |
| 804 | for (uint32_t e = 0; e < slen - 1; e++) { | |
| 805 | b_edge_p1[edge_pos] = e; | |
| 806 | b_edge_p2[edge_pos] = e + 1; | |
| 807 | edge_pos++; | |
| 808 | } | |
| 809 | } | |
| 810 | ||
| 811 | printf(" Sentences: %u, words: %u, edges: %u\n", bench_ns, word_pos, edge_pos); | |
| 812 | ||
| 813 | cl_mem d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 814 | sizeof(uint32_t) * word_pos, b_words, &err); | |
| 815 | cl_mem d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 816 | sizeof(uint32_t) * bench_ns, b_sent_offsets, &err); | |
| 817 | cl_mem d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 818 | sizeof(uint32_t) * bench_ns, b_sent_lengths, &err); | |
| 819 | cl_mem d_edge_p1 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 820 | sizeof(uint32_t) * edge_pos, b_edge_p1, &err); | |
| 821 | cl_mem d_edge_p2 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 822 | sizeof(uint32_t) * edge_pos, b_edge_p2, &err); | |
| 823 | cl_mem d_edge_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 824 | sizeof(uint32_t) * bench_ns, b_edge_offsets, &err); | |
| 825 | cl_mem d_edge_counts = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 826 | sizeof(uint32_t) * bench_ns, b_edge_counts, &err); | |
| 827 | ||
| 828 | /* Set extract_sections args */ | |
| 829 | cl_uint tw = word_pos; | |
| 830 | clSetKernelArg(k_extract, 0, sizeof(cl_mem), &d_flat_words); | |
| 831 | clSetKernelArg(k_extract, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 832 | clSetKernelArg(k_extract, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 833 | clSetKernelArg(k_extract, 3, sizeof(cl_uint), &bench_ns); | |
| 834 | clSetKernelArg(k_extract, 4, sizeof(cl_uint), &tw); | |
| 835 | clSetKernelArg(k_extract, 5, sizeof(cl_mem), &d_edge_p1); | |
| 836 | clSetKernelArg(k_extract, 6, sizeof(cl_mem), &d_edge_p2); | |
| 837 | clSetKernelArg(k_extract, 7, sizeof(cl_mem), &d_edge_offsets); | |
| 838 | clSetKernelArg(k_extract, 8, sizeof(cl_mem), &d_edge_counts); | |
| 839 | clSetKernelArg(k_extract, 9, sizeof(cl_mem), &sht_keys); | |
| 840 | clSetKernelArg(k_extract, 10, sizeof(cl_mem), &sht_values); | |
| 841 | clSetKernelArg(k_extract, 11, sizeof(cl_mem), &sec_word); | |
| 842 | clSetKernelArg(k_extract, 12, sizeof(cl_mem), &sec_disjunct_hash); | |
| 843 | clSetKernelArg(k_extract, 13, sizeof(cl_mem), &sec_count); | |
| 844 | clSetKernelArg(k_extract, 14, sizeof(cl_mem), &sec_next_free); | |
| 845 | clSetKernelArg(k_extract, 15, sizeof(cl_mem), &total_sections_created); | |
| 846 | ||
| 847 | size_t local = 256; | |
| 848 | size_t gs; | |
| 849 | ||
| 850 | /* Phase 4: Extract sections */ | |
| 851 | double t_start = now_ms(); | |
| 852 | ||
| 853 | gs = ((tw + local - 1) / local) * local; | |
| 854 | err = clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 855 | &gs, &local, 0, NULL, NULL); | |
| 856 | CL_CHECK(err, "enqueue extract"); | |
| 857 | clFinish(queue); | |
| 858 | ||
| 859 | double t_extract = now_ms(); | |
| 860 | ||
| 861 | /* Read section count */ | |
| 862 | uint32_t h_num_sections; | |
| 863 | clEnqueueReadBuffer(queue, sec_next_free, CL_TRUE, 0, | |
| 864 | sizeof(uint32_t), &h_num_sections, 0, NULL, NULL); | |
| 865 | ||
| 866 | printf(" Sections extracted: %u (%.2f ms)\n", | |
| 867 | h_num_sections, t_extract - t_start); | |
| 868 | ||
| 869 | /* Phase 5: Cosine pipeline */ | |
| 870 | clSetKernelArg(k_norms, 3, sizeof(cl_uint), &h_num_sections); | |
| 871 | clSetKernelArg(k_chains, 5, sizeof(cl_uint), &h_num_sections); | |
| 872 | clSetKernelArg(k_dots, 12, sizeof(cl_uint), &h_num_sections); | |
| 873 | ||
| 874 | double t_cos_start = now_ms(); | |
| 875 | ||
| 876 | uint32_t num_cands = 0; | |
| 877 | run_cosine_pipeline(queue, k_norms, k_chains, k_dots, k_cosines, | |
| 878 | h_num_sections, cand_next_free, &num_cands); | |
| 879 | ||
| 880 | double t_cos_end = now_ms(); | |
| 881 | ||
| 882 | double total_time = t_cos_end - t_start; | |
| 883 | double cos_time = t_cos_end - t_cos_start; | |
| 884 | ||
| 885 | printf(" Candidate pairs: %u\n", num_cands); | |
| 886 | printf(" Cosine pipeline: %.2f ms\n", cos_time); | |
| 887 | printf(" Full pipeline (extract + cosine): %.2f ms\n", total_time); | |
| 888 | printf(" Throughput: %.0f sentences/sec\n", | |
| 889 | bench_ns / (total_time / 1000.0)); | |
| 890 | ||
| 891 | if (num_cands > 0) { | |
| 892 | /* Read a few top cosines to verify */ | |
| 893 | uint32_t peek = (num_cands < 8) ? num_cands : 8; | |
| 894 | double h_cos[8]; | |
| 895 | clEnqueueReadBuffer(queue, cand_cosine, CL_TRUE, 0, | |
| 896 | sizeof(double) * peek, h_cos, 0, NULL, NULL); | |
| 897 | ||
| 898 | /* Find max cosine */ | |
| 899 | double max_cos = 0; | |
| 900 | for (uint32_t i = 0; i < peek; i++) | |
| 901 | if (h_cos[i] > max_cos) max_cos = h_cos[i]; | |
| 902 | printf(" Max cosine (first %u): %.4f\n", peek, max_cos); | |
| 903 | } | |
| 904 | ||
| 905 | int t6_pass = (h_num_sections > 0) && (num_cands > 0) && (total_time < 5000.0); | |
| 906 | printf(" %s\n\n", t6_pass ? "PASS" : "FAIL"); | |
| 907 | if (t6_pass) pass_count++; else fail_count++; | |
| 908 | ||
| 909 | /* ─── Summary ─── */ | |
| 910 | ||
| 911 | printf("=== Results: %d PASS, %d FAIL ===\n", pass_count, fail_count); | |
| 912 | ||
| 913 | /* Cleanup */ | |
| 914 | free(b_words); free(b_sent_offsets); free(b_sent_lengths); | |
| 915 | free(b_edge_p1); free(b_edge_p2); free(b_edge_offsets); free(b_edge_counts); | |
| 916 | free(src_ht); free(src_as); free(src_sc); free(src_cos); free(combined); | |
| 917 | ||
| 918 | clReleaseMemObject(d_flat_words); | |
| 919 | clReleaseMemObject(d_sent_offsets); | |
| 920 | clReleaseMemObject(d_sent_lengths); | |
| 921 | clReleaseMemObject(d_edge_p1); | |
| 922 | clReleaseMemObject(d_edge_p2); | |
| 923 | clReleaseMemObject(d_edge_offsets); | |
| 924 | clReleaseMemObject(d_edge_counts); | |
| 925 | clReleaseMemObject(sec_word); | |
| 926 | clReleaseMemObject(sec_disjunct_hash); | |
| 927 | clReleaseMemObject(sec_count); | |
| 928 | clReleaseMemObject(sec_next_free); | |
| 929 | clReleaseMemObject(sht_keys); | |
| 930 | clReleaseMemObject(sht_values); | |
| 931 | clReleaseMemObject(total_sections_created); | |
| 932 | clReleaseMemObject(djh_ht_keys); | |
| 933 | clReleaseMemObject(djh_ht_values); | |
| 934 | clReleaseMemObject(sec_chain_next); | |
| 935 | clReleaseMemObject(word_norm_sq); | |
| 936 | clReleaseMemObject(cand_ht_keys); | |
| 937 | clReleaseMemObject(cand_ht_values); | |
| 938 | clReleaseMemObject(cand_word_a); | |
| 939 | clReleaseMemObject(cand_word_b); | |
| 940 | clReleaseMemObject(cand_dot); | |
| 941 | clReleaseMemObject(cand_cosine); | |
| 942 | clReleaseMemObject(cand_next_free); | |
| 943 | clReleaseKernel(k_norms); | |
| 944 | clReleaseKernel(k_chains); | |
| 945 | clReleaseKernel(k_dots); | |
| 946 | clReleaseKernel(k_cosines); | |
| 947 | clReleaseKernel(k_filter); | |
| 948 | clReleaseKernel(k_extract); | |
| 949 | clReleaseProgram(program); | |
| 950 | clReleaseCommandQueue(queue); | |
| 951 | clReleaseContext(ctx); | |
| 952 | ||
| 953 | return fail_count > 0 ? 1 : 0; | |
| 954 | } |
| … | … | @@ -1,720 +0,0 @@ |
| 1 | /* | |
| 2 | * test-counting.c -- Test GPU sentence counting kernel | |
| 3 | * | |
| 4 | * Compile: gcc -O2 -o test-counting test-counting.c -lOpenCL -lm | |
| 5 | * Run: ./test-counting | |
| 6 | * | |
| 7 | * Tests: | |
| 8 | * 1. Simple sentence pair counting (window=2, exact verification) | |
| 9 | * 2. Multi-sentence batch (verify no cross-boundary pairs) | |
| 10 | * 3. Read pairs kernel (readback verification) | |
| 11 | * 4. Binary search variant (count_sentence_pairs_large) | |
| 12 | * 5. Benchmark: 1000 sentences with window=6 | |
| 13 | */ | |
| 14 | ||
| 15 | #include <stdio.h> | |
| 16 | #include <stdlib.h> | |
| 17 | #include <stdint.h> | |
| 18 | #include <string.h> | |
| 19 | #include <math.h> | |
| 20 | #include <time.h> | |
| 21 | #include <CL/cl.h> | |
| 22 | ||
| 23 | /* ─── Pool capacities ─── */ | |
| 24 | ||
| 25 | #define WORD_CAPACITY (128 * 1024) | |
| 26 | #define PAIR_CAPACITY (4 * 1024 * 1024) | |
| 27 | #define SECTION_CAPACITY (1024 * 1024) | |
| 28 | #define WORD_HT_CAPACITY (256 * 1024) | |
| 29 | #define PAIR_HT_CAPACITY (8 * 1024 * 1024) | |
| 30 | #define SECTION_HT_CAPACITY (2 * 1024 * 1024) | |
| 31 | ||
| 32 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 33 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 34 | ||
| 35 | /* ─── Error checking ─── */ | |
| 36 | ||
| 37 | #define CL_CHECK(err, msg) do { \ | |
| 38 | if ((err) != CL_SUCCESS) { \ | |
| 39 | fprintf(stderr, "OpenCL error %d at %s:%d: %s\n", \ | |
| 40 | (err), __FILE__, __LINE__, (msg)); \ | |
| 41 | exit(1); \ | |
| 42 | } \ | |
| 43 | } while(0) | |
| 44 | ||
| 45 | /* ─── Read file ─── */ | |
| 46 | ||
| 47 | char* read_file(const char* path, size_t* len) | |
| 48 | { | |
| 49 | FILE* f = fopen(path, "r"); | |
| 50 | if (!f) { fprintf(stderr, "Cannot open %s\n", path); exit(1); } | |
| 51 | fseek(f, 0, SEEK_END); | |
| 52 | *len = ftell(f); | |
| 53 | fseek(f, 0, SEEK_SET); | |
| 54 | char* buf = malloc(*len + 1); | |
| 55 | size_t n = fread(buf, 1, *len, f); | |
| 56 | buf[n] = '\0'; | |
| 57 | *len = n; | |
| 58 | fclose(f); | |
| 59 | return buf; | |
| 60 | } | |
| 61 | ||
| 62 | /* ─── Timing ─── */ | |
| 63 | ||
| 64 | double now_ms(void) | |
| 65 | { | |
| 66 | struct timespec ts; | |
| 67 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 68 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; | |
| 69 | } | |
| 70 | ||
| 71 | /* ─── Helper: reset pair pool and counters ─── */ | |
| 72 | ||
| 73 | void reset_pools(cl_command_queue queue, | |
| 74 | cl_mem pht_keys, cl_mem pht_values, | |
| 75 | cl_mem pair_count, cl_mem pair_mi, cl_mem pair_flags, | |
| 76 | cl_mem word_count, | |
| 77 | cl_mem pair_next_free, cl_mem total_pair_count) | |
| 78 | { | |
| 79 | uint8_t pat_ff = 0xFF; | |
| 80 | uint8_t pat_00 = 0x00; | |
| 81 | uint32_t zero = 0; | |
| 82 | ||
| 83 | clEnqueueFillBuffer(queue, pht_keys, &pat_ff, 1, 0, | |
| 84 | sizeof(uint64_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 85 | clEnqueueFillBuffer(queue, pht_values, &pat_ff, 1, 0, | |
| 86 | sizeof(uint32_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 87 | clEnqueueFillBuffer(queue, pair_count, &pat_00, 1, 0, | |
| 88 | sizeof(double) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 89 | clEnqueueFillBuffer(queue, pair_mi, &pat_00, 1, 0, | |
| 90 | sizeof(double) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 91 | clEnqueueFillBuffer(queue, pair_flags, &pat_00, 1, 0, | |
| 92 | sizeof(uint32_t) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 93 | clEnqueueFillBuffer(queue, word_count, &pat_00, 1, 0, | |
| 94 | sizeof(double) * WORD_CAPACITY, 0, NULL, NULL); | |
| 95 | clEnqueueWriteBuffer(queue, pair_next_free, CL_FALSE, 0, | |
| 96 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 97 | clEnqueueWriteBuffer(queue, total_pair_count, CL_FALSE, 0, | |
| 98 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 99 | clFinish(queue); | |
| 100 | } | |
| 101 | ||
| 102 | /* ─── Main ─── */ | |
| 103 | ||
| 104 | int main(int argc, char** argv) | |
| 105 | { | |
| 106 | cl_int err; | |
| 107 | int pass_count = 0, fail_count = 0; | |
| 108 | ||
| 109 | printf("=== GPU Sentence Counting Test ===\n\n"); | |
| 110 | ||
| 111 | /* ─── OpenCL setup ─── */ | |
| 112 | ||
| 113 | cl_platform_id platform; | |
| 114 | err = clGetPlatformIDs(1, &platform, NULL); | |
| 115 | CL_CHECK(err, "platform"); | |
| 116 | ||
| 117 | cl_device_id device; | |
| 118 | err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL); | |
| 119 | CL_CHECK(err, "device"); | |
| 120 | ||
| 121 | char dev_name[256]; | |
| 122 | clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(dev_name), dev_name, NULL); | |
| 123 | printf("GPU: %s\n", dev_name); | |
| 124 | ||
| 125 | cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err); | |
| 126 | CL_CHECK(err, "context"); | |
| 127 | ||
| 128 | cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err); | |
| 129 | CL_CHECK(err, "queue"); | |
| 130 | ||
| 131 | /* ─── Load and concatenate kernel sources ─── */ | |
| 132 | ||
| 133 | size_t ht_len, as_len, ct_len; | |
| 134 | char* ht_src = read_file("opencog/gpu/gpu-hashtable.cl", &ht_len); | |
| 135 | char* as_src = read_file("opencog/gpu/gpu-atomspace.cl", &as_len); | |
| 136 | char* ct_src = read_file("opencog/gpu/gpu-counting.cl", &ct_len); | |
| 137 | ||
| 138 | size_t total_len = ht_len + 1 + as_len + 1 + ct_len; | |
| 139 | char* combined = malloc(total_len + 1); | |
| 140 | memcpy(combined, ht_src, ht_len); | |
| 141 | combined[ht_len] = '\n'; | |
| 142 | memcpy(combined + ht_len + 1, as_src, as_len); | |
| 143 | combined[ht_len + 1 + as_len] = '\n'; | |
| 144 | memcpy(combined + ht_len + 1 + as_len + 1, ct_src, ct_len); | |
| 145 | combined[total_len] = '\0'; | |
| 146 | ||
| 147 | cl_program program = clCreateProgramWithSource(ctx, 1, | |
| 148 | (const char**)&combined, &total_len, &err); | |
| 149 | CL_CHECK(err, "create program"); | |
| 150 | ||
| 151 | char build_opts[512]; | |
| 152 | snprintf(build_opts, sizeof(build_opts), | |
| 153 | "-cl-std=CL1.2 " | |
| 154 | "-DWORD_CAPACITY=%d " | |
| 155 | "-DPAIR_CAPACITY=%d " | |
| 156 | "-DSECTION_CAPACITY=%d " | |
| 157 | "-DWORD_HT_CAPACITY=%d " | |
| 158 | "-DPAIR_HT_CAPACITY=%d " | |
| 159 | "-DSECTION_HT_CAPACITY=%d", | |
| 160 | WORD_CAPACITY, PAIR_CAPACITY, SECTION_CAPACITY, | |
| 161 | WORD_HT_CAPACITY, PAIR_HT_CAPACITY, SECTION_HT_CAPACITY); | |
| 162 | ||
| 163 | err = clBuildProgram(program, 1, &device, build_opts, NULL, NULL); | |
| 164 | if (err != CL_SUCCESS) { | |
| 165 | char log[16384]; | |
| 166 | clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, | |
| 167 | sizeof(log), log, NULL); | |
| 168 | fprintf(stderr, "Build error:\n%s\n", log); | |
| 169 | return 1; | |
| 170 | } | |
| 171 | printf("Kernels compiled successfully\n\n"); | |
| 172 | ||
| 173 | /* ─── Create kernels ─── */ | |
| 174 | ||
| 175 | cl_kernel k_count = clCreateKernel(program, "count_sentence_pairs", &err); | |
| 176 | CL_CHECK(err, "kernel count_sentence_pairs"); | |
| 177 | cl_kernel k_count_large = clCreateKernel(program, "count_sentence_pairs_large", &err); | |
| 178 | CL_CHECK(err, "kernel count_sentence_pairs_large"); | |
| 179 | cl_kernel k_read = clCreateKernel(program, "read_pairs", &err); | |
| 180 | CL_CHECK(err, "kernel read_pairs"); | |
| 181 | ||
| 182 | size_t local_size = 256; | |
| 183 | ||
| 184 | /* ─── Allocate GPU buffers ─── */ | |
| 185 | ||
| 186 | printf("Allocating GPU buffers...\n"); | |
| 187 | uint32_t zero = 0; | |
| 188 | ||
| 189 | /* Pair hash table */ | |
| 190 | cl_mem pht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 191 | sizeof(uint64_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 192 | CL_CHECK(err, "pht_keys"); | |
| 193 | cl_mem pht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 194 | sizeof(uint32_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 195 | CL_CHECK(err, "pht_values"); | |
| 196 | ||
| 197 | /* Pair pool SoA */ | |
| 198 | cl_mem pair_word_a = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 199 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 200 | cl_mem pair_word_b = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 201 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 202 | cl_mem pair_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 203 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 204 | cl_mem pair_mi = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 205 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 206 | cl_mem pair_flags = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 207 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 208 | ||
| 209 | /* Pair bump allocator */ | |
| 210 | cl_mem pair_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 211 | sizeof(uint32_t), &zero, &err); | |
| 212 | ||
| 213 | /* Word count array (marginals) — used directly by counting kernel */ | |
| 214 | cl_mem word_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 215 | sizeof(double) * WORD_CAPACITY, NULL, &err); | |
| 216 | ||
| 217 | /* Total pair count (global atomic counter) */ | |
| 218 | cl_mem total_pair_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 219 | sizeof(uint32_t), &zero, &err); | |
| 220 | ||
| 221 | /* Initial reset */ | |
| 222 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 223 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 224 | ||
| 225 | printf("GPU buffers ready\n\n"); | |
| 226 | ||
| 227 | /* ═══════════════════════════════════════════════════════════════ | |
| 228 | * TEST 1: Simple sentence pair counting | |
| 229 | * | |
| 230 | * Sentence: word indices [0, 1, 2, 3] — 4 words | |
| 231 | * Window = 2 | |
| 232 | * | |
| 233 | * Expected pairs (one thread per word position): | |
| 234 | * pos 0: (0,1), (0,2) | |
| 235 | * pos 1: (1,2), (1,3) | |
| 236 | * pos 2: (2,3) | |
| 237 | * pos 3: — | |
| 238 | * | |
| 239 | * = 5 unique pairs, 5 count events | |
| 240 | * Word marginals: word0=2, word1=3, word2=3, word3=2 | |
| 241 | * ═══════════════════════════════════════════════════════════════ */ | |
| 242 | ||
| 243 | printf("--- Test 1: Simple sentence (window=2) ---\n"); | |
| 244 | ||
| 245 | uint32_t sent1_words[] = {0, 1, 2, 3}; | |
| 246 | uint32_t sent1_offset = 0; | |
| 247 | uint32_t sent1_length = 4; | |
| 248 | cl_uint num_sentences = 1; | |
| 249 | cl_uint tw = 4; | |
| 250 | cl_uint window_size = 2; | |
| 251 | ||
| 252 | cl_mem d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 253 | sizeof(uint32_t) * tw, sent1_words, &err); | |
| 254 | cl_mem d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 255 | sizeof(uint32_t), &sent1_offset, &err); | |
| 256 | cl_mem d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 257 | sizeof(uint32_t), &sent1_length, &err); | |
| 258 | ||
| 259 | /* Set all 16 kernel args for count_sentence_pairs */ | |
| 260 | clSetKernelArg(k_count, 0, sizeof(cl_mem), &d_flat_words); | |
| 261 | clSetKernelArg(k_count, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 262 | clSetKernelArg(k_count, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 263 | clSetKernelArg(k_count, 3, sizeof(cl_uint), &num_sentences); | |
| 264 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &tw); | |
| 265 | clSetKernelArg(k_count, 5, sizeof(cl_uint), &window_size); | |
| 266 | clSetKernelArg(k_count, 6, sizeof(cl_mem), &pht_keys); | |
| 267 | clSetKernelArg(k_count, 7, sizeof(cl_mem), &pht_values); | |
| 268 | clSetKernelArg(k_count, 8, sizeof(cl_mem), &pair_word_a); | |
| 269 | clSetKernelArg(k_count, 9, sizeof(cl_mem), &pair_word_b); | |
| 270 | clSetKernelArg(k_count, 10, sizeof(cl_mem), &pair_count); | |
| 271 | clSetKernelArg(k_count, 11, sizeof(cl_mem), &pair_mi); | |
| 272 | clSetKernelArg(k_count, 12, sizeof(cl_mem), &pair_flags); | |
| 273 | clSetKernelArg(k_count, 13, sizeof(cl_mem), &pair_next_free); | |
| 274 | clSetKernelArg(k_count, 14, sizeof(cl_mem), &word_count); | |
| 275 | clSetKernelArg(k_count, 15, sizeof(cl_mem), &total_pair_count); | |
| 276 | ||
| 277 | double t0 = now_ms(); | |
| 278 | size_t gs = ((tw + local_size - 1) / local_size) * local_size; | |
| 279 | err = clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 280 | &gs, &local_size, 0, NULL, NULL); | |
| 281 | CL_CHECK(err, "enqueue count"); | |
| 282 | clFinish(queue); | |
| 283 | double t1 = now_ms(); | |
| 284 | ||
| 285 | /* Read back results */ | |
| 286 | uint32_t h_num_pairs; | |
| 287 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 288 | sizeof(uint32_t), &h_num_pairs, 0, NULL, NULL); | |
| 289 | ||
| 290 | uint32_t h_total; | |
| 291 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 292 | sizeof(uint32_t), &h_total, 0, NULL, NULL); | |
| 293 | ||
| 294 | /* Read word marginals */ | |
| 295 | double wc[4]; | |
| 296 | clEnqueueReadBuffer(queue, word_count, CL_TRUE, 0, | |
| 297 | sizeof(double) * 4, wc, 0, NULL, NULL); | |
| 298 | ||
| 299 | printf(" Pairs created: %u (expected 5)\n", h_num_pairs); | |
| 300 | printf(" Total count events: %u (expected 5)\n", h_total); | |
| 301 | printf(" Word marginals: [0]=%.0f [1]=%.0f [2]=%.0f [3]=%.0f\n", | |
| 302 | wc[0], wc[1], wc[2], wc[3]); | |
| 303 | printf(" Expected: [0]=2 [1]=3 [2]=3 [3]=2\n"); | |
| 304 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 305 | ||
| 306 | int t1_pass = (h_num_pairs == 5) && (h_total == 5) && | |
| 307 | (fabs(wc[0] - 2.0) < 0.5) && (fabs(wc[1] - 3.0) < 0.5) && | |
| 308 | (fabs(wc[2] - 3.0) < 0.5) && (fabs(wc[3] - 2.0) < 0.5); | |
| 309 | printf(" %s\n\n", t1_pass ? "PASS" : "FAIL"); | |
| 310 | if (t1_pass) pass_count++; else fail_count++; | |
| 311 | ||
| 312 | /* ═══════════════════════════════════════════════════════════════ | |
| 313 | * TEST 2: Multi-sentence batch | |
| 314 | * | |
| 315 | * Sentence 1: [0, 1, 2, 3] — 4 words (same as test 1) | |
| 316 | * Sentence 2: [4, 5, 6] — 3 words (disjoint vocabulary) | |
| 317 | * | |
| 318 | * flat_words = [0, 1, 2, 3, 4, 5, 6] | |
| 319 | * sent_offsets = [0, 4] | |
| 320 | * sent_lengths = [4, 3] | |
| 321 | * | |
| 322 | * Window = 2: | |
| 323 | * Sentence 1: (0,1) (0,2) (1,2) (1,3) (2,3) = 5 pairs | |
| 324 | * Sentence 2: (4,5) (4,6) (5,6) = 3 pairs | |
| 325 | * Total: 8 unique pairs, 8 count events | |
| 326 | * | |
| 327 | * KEY: no cross-boundary pairs (e.g., no (3,4) pair) | |
| 328 | * ═══════════════════════════════════════════════════════════════ */ | |
| 329 | ||
| 330 | printf("--- Test 2: Multi-sentence batch (window=2) ---\n"); | |
| 331 | ||
| 332 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 333 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 334 | ||
| 335 | uint32_t multi_words[] = {0, 1, 2, 3, 4, 5, 6}; | |
| 336 | uint32_t multi_offsets[] = {0, 4}; | |
| 337 | uint32_t multi_lengths[] = {4, 3}; | |
| 338 | cl_uint multi_ns = 2; | |
| 339 | cl_uint multi_tw = 7; | |
| 340 | ||
| 341 | clReleaseMemObject(d_flat_words); | |
| 342 | clReleaseMemObject(d_sent_offsets); | |
| 343 | clReleaseMemObject(d_sent_lengths); | |
| 344 | ||
| 345 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 346 | sizeof(uint32_t) * multi_tw, multi_words, &err); | |
| 347 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 348 | sizeof(uint32_t) * multi_ns, multi_offsets, &err); | |
| 349 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 350 | sizeof(uint32_t) * multi_ns, multi_lengths, &err); | |
| 351 | ||
| 352 | /* Update sentence-specific args (6-15 stay same) */ | |
| 353 | clSetKernelArg(k_count, 0, sizeof(cl_mem), &d_flat_words); | |
| 354 | clSetKernelArg(k_count, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 355 | clSetKernelArg(k_count, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 356 | clSetKernelArg(k_count, 3, sizeof(cl_uint), &multi_ns); | |
| 357 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &multi_tw); | |
| 358 | clSetKernelArg(k_count, 5, sizeof(cl_uint), &window_size); | |
| 359 | ||
| 360 | t0 = now_ms(); | |
| 361 | gs = ((multi_tw + local_size - 1) / local_size) * local_size; | |
| 362 | err = clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 363 | &gs, &local_size, 0, NULL, NULL); | |
| 364 | CL_CHECK(err, "enqueue count multi"); | |
| 365 | clFinish(queue); | |
| 366 | t1 = now_ms(); | |
| 367 | ||
| 368 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 369 | sizeof(uint32_t), &h_num_pairs, 0, NULL, NULL); | |
| 370 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 371 | sizeof(uint32_t), &h_total, 0, NULL, NULL); | |
| 372 | ||
| 373 | printf(" Pairs created: %u (expected 8)\n", h_num_pairs); | |
| 374 | printf(" Total count events: %u (expected 8)\n", h_total); | |
| 375 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 376 | ||
| 377 | /* Verify no cross-boundary pairs exist. | |
| 378 | * Words 0-3 belong to sentence 1, words 4-6 to sentence 2. | |
| 379 | * No pair should have one word from each sentence. */ | |
| 380 | uint32_t* h_pa = malloc(sizeof(uint32_t) * h_num_pairs); | |
| 381 | uint32_t* h_pb = malloc(sizeof(uint32_t) * h_num_pairs); | |
| 382 | clEnqueueReadBuffer(queue, pair_word_a, CL_TRUE, 0, | |
| 383 | sizeof(uint32_t) * h_num_pairs, h_pa, 0, NULL, NULL); | |
| 384 | clEnqueueReadBuffer(queue, pair_word_b, CL_TRUE, 0, | |
| 385 | sizeof(uint32_t) * h_num_pairs, h_pb, 0, NULL, NULL); | |
| 386 | ||
| 387 | int cross_boundary = 0; | |
| 388 | for (uint32_t i = 0; i < h_num_pairs; i++) { | |
| 389 | int a_sent = (h_pa[i] <= 3) ? 1 : 2; | |
| 390 | int b_sent = (h_pb[i] <= 3) ? 1 : 2; | |
| 391 | if (a_sent != b_sent) { | |
| 392 | cross_boundary++; | |
| 393 | printf(" CROSS-BOUNDARY: pair(%u, %u)\n", h_pa[i], h_pb[i]); | |
| 394 | } | |
| 395 | } | |
| 396 | printf(" Cross-boundary pairs: %d (expected 0)\n", cross_boundary); | |
| 397 | ||
| 398 | int t2_pass = (h_num_pairs == 8) && (h_total == 8) && (cross_boundary == 0); | |
| 399 | printf(" %s\n\n", t2_pass ? "PASS" : "FAIL"); | |
| 400 | if (t2_pass) pass_count++; else fail_count++; | |
| 401 | ||
| 402 | free(h_pa); | |
| 403 | free(h_pb); | |
| 404 | ||
| 405 | /* ═══════════════════════════════════════════════════════════════ | |
| 406 | * TEST 3: Read pairs (readback kernel) | |
| 407 | * | |
| 408 | * Uses the read_pairs kernel to copy pair data from the pool | |
| 409 | * into output arrays. Verifies all pairs from Test 2 have | |
| 410 | * count=1 and dirty flag=1. | |
| 411 | * ═══════════════════════════════════════════════════════════════ */ | |
| 412 | ||
| 413 | printf("--- Test 3: Read pairs (readback verification) ---\n"); | |
| 414 | ||
| 415 | cl_mem d_out_wa = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 416 | sizeof(uint32_t) * h_num_pairs, NULL, &err); | |
| 417 | cl_mem d_out_wb = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 418 | sizeof(uint32_t) * h_num_pairs, NULL, &err); | |
| 419 | cl_mem d_out_cnt = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 420 | sizeof(double) * h_num_pairs, NULL, &err); | |
| 421 | cl_mem d_out_mi = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 422 | sizeof(double) * h_num_pairs, NULL, &err); | |
| 423 | cl_mem d_out_flags = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 424 | sizeof(uint32_t) * h_num_pairs, NULL, &err); | |
| 425 | ||
| 426 | cl_uint np = h_num_pairs; | |
| 427 | clSetKernelArg(k_read, 0, sizeof(cl_mem), &pair_word_a); | |
| 428 | clSetKernelArg(k_read, 1, sizeof(cl_mem), &pair_word_b); | |
| 429 | clSetKernelArg(k_read, 2, sizeof(cl_mem), &pair_count); | |
| 430 | clSetKernelArg(k_read, 3, sizeof(cl_mem), &pair_mi); | |
| 431 | clSetKernelArg(k_read, 4, sizeof(cl_mem), &pair_flags); | |
| 432 | clSetKernelArg(k_read, 5, sizeof(cl_mem), &d_out_wa); | |
| 433 | clSetKernelArg(k_read, 6, sizeof(cl_mem), &d_out_wb); | |
| 434 | clSetKernelArg(k_read, 7, sizeof(cl_mem), &d_out_cnt); | |
| 435 | clSetKernelArg(k_read, 8, sizeof(cl_mem), &d_out_mi); | |
| 436 | clSetKernelArg(k_read, 9, sizeof(cl_mem), &d_out_flags); | |
| 437 | clSetKernelArg(k_read, 10, sizeof(cl_uint), &np); | |
| 438 | ||
| 439 | gs = ((h_num_pairs + local_size - 1) / local_size) * local_size; | |
| 440 | err = clEnqueueNDRangeKernel(queue, k_read, 1, NULL, | |
| 441 | &gs, &local_size, 0, NULL, NULL); | |
| 442 | CL_CHECK(err, "enqueue read_pairs"); | |
| 443 | clFinish(queue); | |
| 444 | ||
| 445 | uint32_t* r_wa = malloc(sizeof(uint32_t) * h_num_pairs); | |
| 446 | uint32_t* r_wb = malloc(sizeof(uint32_t) * h_num_pairs); | |
| 447 | double* r_cnt = malloc(sizeof(double) * h_num_pairs); | |
| 448 | uint32_t* r_flags = malloc(sizeof(uint32_t) * h_num_pairs); | |
| 449 | ||
| 450 | clEnqueueReadBuffer(queue, d_out_wa, CL_TRUE, 0, | |
| 451 | sizeof(uint32_t) * h_num_pairs, r_wa, 0, NULL, NULL); | |
| 452 | clEnqueueReadBuffer(queue, d_out_wb, CL_TRUE, 0, | |
| 453 | sizeof(uint32_t) * h_num_pairs, r_wb, 0, NULL, NULL); | |
| 454 | clEnqueueReadBuffer(queue, d_out_cnt, CL_TRUE, 0, | |
| 455 | sizeof(double) * h_num_pairs, r_cnt, 0, NULL, NULL); | |
| 456 | clEnqueueReadBuffer(queue, d_out_flags, CL_TRUE, 0, | |
| 457 | sizeof(uint32_t) * h_num_pairs, r_flags, 0, NULL, NULL); | |
| 458 | ||
| 459 | int all_counted = 1; | |
| 460 | int all_dirty = 1; | |
| 461 | int all_canonical = 1; | |
| 462 | double sum_counts = 0; | |
| 463 | ||
| 464 | printf(" Pairs:\n"); | |
| 465 | for (uint32_t i = 0; i < h_num_pairs; i++) { | |
| 466 | printf(" [%u] (%u, %u) count=%.0f flags=%u\n", | |
| 467 | i, r_wa[i], r_wb[i], r_cnt[i], r_flags[i]); | |
| 468 | if (r_cnt[i] < 0.5) all_counted = 0; | |
| 469 | if (r_flags[i] != 1) all_dirty = 0; | |
| 470 | if (r_wa[i] > r_wb[i]) all_canonical = 0; | |
| 471 | sum_counts += r_cnt[i]; | |
| 472 | } | |
| 473 | ||
| 474 | printf(" Sum of counts: %.0f (expected %u)\n", sum_counts, h_total); | |
| 475 | printf(" All counts > 0: %s\n", all_counted ? "YES" : "NO"); | |
| 476 | printf(" All dirty flags: %s\n", all_dirty ? "YES" : "NO"); | |
| 477 | printf(" All canonical (a <= b): %s\n", all_canonical ? "YES" : "NO"); | |
| 478 | ||
| 479 | int t3_pass = all_counted && all_dirty && all_canonical && | |
| 480 | (fabs(sum_counts - h_total) < 0.5); | |
| 481 | printf(" %s\n\n", t3_pass ? "PASS" : "FAIL"); | |
| 482 | if (t3_pass) pass_count++; else fail_count++; | |
| 483 | ||
| 484 | free(r_wa); free(r_wb); free(r_cnt); free(r_flags); | |
| 485 | clReleaseMemObject(d_out_wa); | |
| 486 | clReleaseMemObject(d_out_wb); | |
| 487 | clReleaseMemObject(d_out_cnt); | |
| 488 | clReleaseMemObject(d_out_mi); | |
| 489 | clReleaseMemObject(d_out_flags); | |
| 490 | ||
| 491 | /* ═══════════════════════════════════════════════════════════════ | |
| 492 | * TEST 4: Binary search variant | |
| 493 | * | |
| 494 | * Same input as Test 2, using count_sentence_pairs_large. | |
| 495 | * Should produce identical results. | |
| 496 | * ═══════════════════════════════════════════════════════════════ */ | |
| 497 | ||
| 498 | printf("--- Test 4: Binary search variant ---\n"); | |
| 499 | ||
| 500 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 501 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 502 | ||
| 503 | /* Set all 16 kernel args for count_sentence_pairs_large */ | |
| 504 | clSetKernelArg(k_count_large, 0, sizeof(cl_mem), &d_flat_words); | |
| 505 | clSetKernelArg(k_count_large, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 506 | clSetKernelArg(k_count_large, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 507 | clSetKernelArg(k_count_large, 3, sizeof(cl_uint), &multi_ns); | |
| 508 | clSetKernelArg(k_count_large, 4, sizeof(cl_uint), &multi_tw); | |
| 509 | clSetKernelArg(k_count_large, 5, sizeof(cl_uint), &window_size); | |
| 510 | clSetKernelArg(k_count_large, 6, sizeof(cl_mem), &pht_keys); | |
| 511 | clSetKernelArg(k_count_large, 7, sizeof(cl_mem), &pht_values); | |
| 512 | clSetKernelArg(k_count_large, 8, sizeof(cl_mem), &pair_word_a); | |
| 513 | clSetKernelArg(k_count_large, 9, sizeof(cl_mem), &pair_word_b); | |
| 514 | clSetKernelArg(k_count_large, 10, sizeof(cl_mem), &pair_count); | |
| 515 | clSetKernelArg(k_count_large, 11, sizeof(cl_mem), &pair_mi); | |
| 516 | clSetKernelArg(k_count_large, 12, sizeof(cl_mem), &pair_flags); | |
| 517 | clSetKernelArg(k_count_large, 13, sizeof(cl_mem), &pair_next_free); | |
| 518 | clSetKernelArg(k_count_large, 14, sizeof(cl_mem), &word_count); | |
| 519 | clSetKernelArg(k_count_large, 15, sizeof(cl_mem), &total_pair_count); | |
| 520 | ||
| 521 | t0 = now_ms(); | |
| 522 | gs = ((multi_tw + local_size - 1) / local_size) * local_size; | |
| 523 | err = clEnqueueNDRangeKernel(queue, k_count_large, 1, NULL, | |
| 524 | &gs, &local_size, 0, NULL, NULL); | |
| 525 | CL_CHECK(err, "enqueue count_large"); | |
| 526 | clFinish(queue); | |
| 527 | t1 = now_ms(); | |
| 528 | ||
| 529 | uint32_t h_pairs_large, h_total_large; | |
| 530 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 531 | sizeof(uint32_t), &h_pairs_large, 0, NULL, NULL); | |
| 532 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 533 | sizeof(uint32_t), &h_total_large, 0, NULL, NULL); | |
| 534 | ||
| 535 | printf(" Pairs created: %u (expected 8)\n", h_pairs_large); | |
| 536 | printf(" Total count events: %u (expected 8)\n", h_total_large); | |
| 537 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 538 | ||
| 539 | /* Verify word marginals match Test 2's expected values */ | |
| 540 | double wc4[7]; | |
| 541 | clEnqueueReadBuffer(queue, word_count, CL_TRUE, 0, | |
| 542 | sizeof(double) * 7, wc4, 0, NULL, NULL); | |
| 543 | printf(" Word marginals: [0]=%.0f [1]=%.0f [2]=%.0f [3]=%.0f [4]=%.0f [5]=%.0f [6]=%.0f\n", | |
| 544 | wc4[0], wc4[1], wc4[2], wc4[3], wc4[4], wc4[5], wc4[6]); | |
| 545 | printf(" Expected: [0]=2 [1]=3 [2]=3 [3]=2 [4]=2 [5]=2 [6]=2\n"); | |
| 546 | ||
| 547 | int t4_pass = (h_pairs_large == 8) && (h_total_large == 8) && | |
| 548 | (fabs(wc4[0] - 2.0) < 0.5) && (fabs(wc4[1] - 3.0) < 0.5) && | |
| 549 | (fabs(wc4[2] - 3.0) < 0.5) && (fabs(wc4[3] - 2.0) < 0.5) && | |
| 550 | (fabs(wc4[4] - 2.0) < 0.5) && (fabs(wc4[5] - 2.0) < 0.5) && | |
| 551 | (fabs(wc4[6] - 2.0) < 0.5); | |
| 552 | printf(" %s\n\n", t4_pass ? "PASS" : "FAIL"); | |
| 553 | if (t4_pass) pass_count++; else fail_count++; | |
| 554 | ||
| 555 | /* ═══════════════════════════════════════════════════════════════ | |
| 556 | * TEST 5: Benchmark — 1000 sentences, window=6 | |
| 557 | * | |
| 558 | * 500-word vocabulary, Zipf-like distribution, 5-20 words per | |
| 559 | * sentence. Compares linear scan vs binary search variant. | |
| 560 | * ═══════════════════════════════════════════════════════════════ */ | |
| 561 | ||
| 562 | printf("--- Test 5: Benchmark (1000 sentences, window=6) ---\n"); | |
| 563 | ||
| 564 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 565 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 566 | ||
| 567 | /* Generate 1000 sentences with Zipf-like word distribution */ | |
| 568 | int bench_ns = 1000; | |
| 569 | uint32_t* bench_offsets = malloc(sizeof(uint32_t) * bench_ns); | |
| 570 | uint32_t* bench_lengths = malloc(sizeof(uint32_t) * bench_ns); | |
| 571 | ||
| 572 | uint64_t rng = 0xCAFEBABEDEADBEEFULL; | |
| 573 | uint32_t bench_tw = 0; | |
| 574 | for (int s = 0; s < bench_ns; s++) { | |
| 575 | bench_offsets[s] = bench_tw; | |
| 576 | rng += 0x9E3779B97F4A7C15ULL; | |
| 577 | uint32_t slen = 5 + (uint32_t)((rng >> 32) % 16); | |
| 578 | bench_lengths[s] = slen; | |
| 579 | bench_tw += slen; | |
| 580 | } | |
| 581 | ||
| 582 | uint32_t* bench_words = malloc(sizeof(uint32_t) * bench_tw); | |
| 583 | rng = 0xFEEDFACE12345678ULL; | |
| 584 | for (uint32_t i = 0; i < bench_tw; i++) { | |
| 585 | rng += 0x9E3779B97F4A7C15ULL; | |
| 586 | double u = (double)(rng >> 32) / (double)0xFFFFFFFFU; | |
| 587 | bench_words[i] = (uint32_t)(u * u * 499.0); | |
| 588 | } | |
| 589 | ||
| 590 | printf(" Sentences: %d, Total words: %u, Avg len: %.1f\n", | |
| 591 | bench_ns, bench_tw, (float)bench_tw / bench_ns); | |
| 592 | ||
| 593 | clReleaseMemObject(d_flat_words); | |
| 594 | clReleaseMemObject(d_sent_offsets); | |
| 595 | clReleaseMemObject(d_sent_lengths); | |
| 596 | ||
| 597 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 598 | sizeof(uint32_t) * bench_tw, bench_words, &err); | |
| 599 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 600 | sizeof(uint32_t) * bench_ns, bench_offsets, &err); | |
| 601 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 602 | sizeof(uint32_t) * bench_ns, bench_lengths, &err); | |
| 603 | ||
| 604 | cl_uint bench_window = 6; | |
| 605 | cl_uint bns = bench_ns; | |
| 606 | ||
| 607 | /* Run linear scan variant */ | |
| 608 | clSetKernelArg(k_count, 0, sizeof(cl_mem), &d_flat_words); | |
| 609 | clSetKernelArg(k_count, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 610 | clSetKernelArg(k_count, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 611 | clSetKernelArg(k_count, 3, sizeof(cl_uint), &bns); | |
| 612 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &bench_tw); | |
| 613 | clSetKernelArg(k_count, 5, sizeof(cl_uint), &bench_window); | |
| 614 | ||
| 615 | t0 = now_ms(); | |
| 616 | gs = ((bench_tw + local_size - 1) / local_size) * local_size; | |
| 617 | err = clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 618 | &gs, &local_size, 0, NULL, NULL); | |
| 619 | CL_CHECK(err, "enqueue bench count"); | |
| 620 | clFinish(queue); | |
| 621 | t1 = now_ms(); | |
| 622 | ||
| 623 | uint32_t bench_pairs, bench_total; | |
| 624 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 625 | sizeof(uint32_t), &bench_pairs, 0, NULL, NULL); | |
| 626 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 627 | sizeof(uint32_t), &bench_total, 0, NULL, NULL); | |
| 628 | ||
| 629 | double linear_ms = t1 - t0; | |
| 630 | printf(" Linear scan: %.2f ms\n", linear_ms); | |
| 631 | printf(" Unique pairs: %u\n", bench_pairs); | |
| 632 | printf(" Total count events: %u\n", bench_total); | |
| 633 | printf(" Throughput: %.0f K sentences/sec, %.0f K pair-events/sec\n", | |
| 634 | bench_ns / (linear_ms / 1000.0) / 1000.0, | |
| 635 | bench_total / (linear_ms / 1000.0) / 1000.0); | |
| 636 | ||
| 637 | /* Run binary search variant with same data */ | |
| 638 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 639 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 640 | ||
| 641 | clSetKernelArg(k_count_large, 0, sizeof(cl_mem), &d_flat_words); | |
| 642 | clSetKernelArg(k_count_large, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 643 | clSetKernelArg(k_count_large, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 644 | clSetKernelArg(k_count_large, 3, sizeof(cl_uint), &bns); | |
| 645 | clSetKernelArg(k_count_large, 4, sizeof(cl_uint), &bench_tw); | |
| 646 | clSetKernelArg(k_count_large, 5, sizeof(cl_uint), &bench_window); | |
| 647 | clSetKernelArg(k_count_large, 6, sizeof(cl_mem), &pht_keys); | |
| 648 | clSetKernelArg(k_count_large, 7, sizeof(cl_mem), &pht_values); | |
| 649 | clSetKernelArg(k_count_large, 8, sizeof(cl_mem), &pair_word_a); | |
| 650 | clSetKernelArg(k_count_large, 9, sizeof(cl_mem), &pair_word_b); | |
| 651 | clSetKernelArg(k_count_large, 10, sizeof(cl_mem), &pair_count); | |
| 652 | clSetKernelArg(k_count_large, 11, sizeof(cl_mem), &pair_mi); | |
| 653 | clSetKernelArg(k_count_large, 12, sizeof(cl_mem), &pair_flags); | |
| 654 | clSetKernelArg(k_count_large, 13, sizeof(cl_mem), &pair_next_free); | |
| 655 | clSetKernelArg(k_count_large, 14, sizeof(cl_mem), &word_count); | |
| 656 | clSetKernelArg(k_count_large, 15, sizeof(cl_mem), &total_pair_count); | |
| 657 | ||
| 658 | t0 = now_ms(); | |
| 659 | err = clEnqueueNDRangeKernel(queue, k_count_large, 1, NULL, | |
| 660 | &gs, &local_size, 0, NULL, NULL); | |
| 661 | CL_CHECK(err, "enqueue bench count_large"); | |
| 662 | clFinish(queue); | |
| 663 | t1 = now_ms(); | |
| 664 | ||
| 665 | uint32_t bench_pairs_l, bench_total_l; | |
| 666 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 667 | sizeof(uint32_t), &bench_pairs_l, 0, NULL, NULL); | |
| 668 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 669 | sizeof(uint32_t), &bench_total_l, 0, NULL, NULL); | |
| 670 | ||
| 671 | double binary_ms = t1 - t0; | |
| 672 | printf(" Binary search: %.2f ms\n", binary_ms); | |
| 673 | printf(" Unique pairs: %u (expected %u)\n", bench_pairs_l, bench_pairs); | |
| 674 | printf(" Total count events: %u (expected %u)\n", bench_total_l, bench_total); | |
| 675 | printf(" Throughput: %.0f K sentences/sec, %.0f K pair-events/sec\n", | |
| 676 | bench_ns / (binary_ms / 1000.0) / 1000.0, | |
| 677 | bench_total_l / (binary_ms / 1000.0) / 1000.0); | |
| 678 | ||
| 679 | int t5_pass = (bench_pairs_l == bench_pairs) && (bench_total_l == bench_total); | |
| 680 | printf(" Linear vs Binary match: %s\n", t5_pass ? "PASS" : "FAIL"); | |
| 681 | printf(" %s\n\n", t5_pass ? "PASS" : "FAIL"); | |
| 682 | if (t5_pass) pass_count++; else fail_count++; | |
| 683 | ||
| 684 | /* ═══ Summary ═══ */ | |
| 685 | ||
| 686 | printf("=== Results: %d PASS, %d FAIL ===\n", pass_count, fail_count); | |
| 687 | ||
| 688 | /* ─── Cleanup ─── */ | |
| 689 | ||
| 690 | clReleaseMemObject(pht_keys); | |
| 691 | clReleaseMemObject(pht_values); | |
| 692 | clReleaseMemObject(pair_word_a); | |
| 693 | clReleaseMemObject(pair_word_b); | |
| 694 | clReleaseMemObject(pair_count); | |
| 695 | clReleaseMemObject(pair_mi); | |
| 696 | clReleaseMemObject(pair_flags); | |
| 697 | clReleaseMemObject(pair_next_free); | |
| 698 | clReleaseMemObject(word_count); | |
| 699 | clReleaseMemObject(total_pair_count); | |
| 700 | clReleaseMemObject(d_flat_words); | |
| 701 | clReleaseMemObject(d_sent_offsets); | |
| 702 | clReleaseMemObject(d_sent_lengths); | |
| 703 | ||
| 704 | clReleaseKernel(k_count); | |
| 705 | clReleaseKernel(k_count_large); | |
| 706 | clReleaseKernel(k_read); | |
| 707 | clReleaseProgram(program); | |
| 708 | clReleaseCommandQueue(queue); | |
| 709 | clReleaseContext(ctx); | |
| 710 | ||
| 711 | free(bench_offsets); | |
| 712 | free(bench_lengths); | |
| 713 | free(bench_words); | |
| 714 | free(combined); | |
| 715 | free(ht_src); | |
| 716 | free(as_src); | |
| 717 | free(ct_src); | |
| 718 | ||
| 719 | return fail_count > 0 ? 1 : 0; | |
| 720 | } |
| … | … | @@ -1,224 +0,0 @@ |
| 1 | /* test-divergence.cu — Test T7 for polysemy detection | |
| 2 | * | |
| 3 | * T7: Polysemy detection across 2 compartments | |
| 4 | * - 10 words, 2 compartments with overlapping vocabulary | |
| 5 | * - Word "bank" (word 5) has different MI neighborhoods: | |
| 6 | * Comp A: high MI with money/loan/account | |
| 7 | * Comp B: high MI with river/fish/water | |
| 8 | * - Non-polysemous words have similar neighborhoods | |
| 9 | * | |
| 10 | * Build: | |
| 11 | * nvcc -O2 -arch=sm_75 -rdc=true \ | |
| 12 | * -o test-divergence \ | |
| 13 | * test-divergence.cu gpu-divergence.cu \ | |
| 14 | * -lcudadevrt -lm | |
| 15 | */ | |
| 16 | ||
| 17 | #include <cstdio> | |
| 18 | #include <cstdlib> | |
| 19 | #include <cstring> | |
| 20 | #include <cmath> | |
| 21 | #include <cstdint> | |
| 22 | ||
| 23 | #define DIV_TOP_K 32 | |
| 24 | ||
| 25 | extern "C" void divergence_run( | |
| 26 | const uint32_t* d_pair_a_word_a, | |
| 27 | const uint32_t* d_pair_a_word_b, | |
| 28 | const double* d_pair_a_mi, | |
| 29 | const double* d_pair_a_count, | |
| 30 | uint32_t num_pairs_a, | |
| 31 | const uint32_t* d_pair_b_word_a, | |
| 32 | const uint32_t* d_pair_b_word_b, | |
| 33 | const double* d_pair_b_mi, | |
| 34 | const double* d_pair_b_count, | |
| 35 | uint32_t num_pairs_b, | |
| 36 | uint32_t num_words, | |
| 37 | double polysemy_threshold, | |
| 38 | uint32_t* d_nbr_a_ids, | |
| 39 | double* d_nbr_a_mi, | |
| 40 | uint32_t* d_nbr_a_count, | |
| 41 | uint32_t* d_nbr_b_ids, | |
| 42 | double* d_nbr_b_mi, | |
| 43 | uint32_t* d_nbr_b_count, | |
| 44 | double* d_divergence, | |
| 45 | uint32_t* d_polysemy_flag); | |
| 46 | ||
| 47 | int test_t7_polysemy() { | |
| 48 | printf("T7: Polysemy detection (2 compartments, 10 words)\n"); | |
| 49 | ||
| 50 | /* Words: | |
| 51 | * 0=the, 1=money, 2=loan, 3=account, 4=interest | |
| 52 | * 5=bank (POLYSEMOUS), 6=river, 7=fish, 8=water, 9=large | |
| 53 | * | |
| 54 | * Compartment A (financial text): | |
| 55 | * bank(5) has high MI with: money(1), loan(2), account(3), interest(4) | |
| 56 | * the(0) is function word: moderate MI with everything | |
| 57 | * large(9): moderate MI with money(1), loan(2) | |
| 58 | * | |
| 59 | * Compartment B (nature text): | |
| 60 | * bank(5) has high MI with: river(6), fish(7), water(8) | |
| 61 | * the(0) is function word: same pattern (similar neighborhoods) | |
| 62 | * large(9): moderate MI with river(6), fish(7) | |
| 63 | * | |
| 64 | * Expected: bank(5) flagged as polysemous (high divergence) | |
| 65 | * the(0), large(9) NOT flagged (similar neighborhoods) | |
| 66 | */ | |
| 67 | ||
| 68 | const uint32_t NUM_WORDS = 10; | |
| 69 | ||
| 70 | /* Compartment A pairs (financial context) */ | |
| 71 | uint32_t h_a_wa[] = {5,5,5,5, 0,0,0,0,0,0,0,0,0, 1,1,1, 2,2, 3, 9,9}; | |
| 72 | uint32_t h_a_wb[] = {1,2,3,4, 1,2,3,4,5,6,7,8,9, 2,3,4, 3,4, 4, 1,2}; | |
| 73 | double h_a_mi[] = {6.0,5.5,5.0,4.8, 2.0,1.8,1.9,2.1,2.0,1.5,1.4,1.3,1.7, 3.0,2.8,2.5, 2.6,2.3, 2.0, 3.2,2.9}; | |
| 74 | int n_a = 21; | |
| 75 | ||
| 76 | /* Compartment B pairs (nature context) */ | |
| 77 | uint32_t h_b_wa[] = {5,5,5, 0,0,0,0,0,0,0,0,0, 6,6, 7, 9,9}; | |
| 78 | uint32_t h_b_wb[] = {6,7,8, 1,2,3,5,6,7,8,9,4, 7,8, 8, 6,7}; | |
| 79 | double h_b_mi[] = {6.5,5.8,5.2, 1.8,1.6,1.7,1.9,2.1,2.0,2.2,1.8,1.5, 3.5,3.0, 2.8, 3.0,2.7}; | |
| 80 | int n_b = 17; | |
| 81 | ||
| 82 | /* All pairs have count > 0 */ | |
| 83 | double h_a_count[21], h_b_count[17]; | |
| 84 | for (int i = 0; i < n_a; i++) h_a_count[i] = 10.0; | |
| 85 | for (int i = 0; i < n_b; i++) h_b_count[i] = 10.0; | |
| 86 | ||
| 87 | /* Upload compartment A */ | |
| 88 | uint32_t *d_a_wa, *d_a_wb; | |
| 89 | double *d_a_mi, *d_a_count; | |
| 90 | cudaMalloc(&d_a_wa, n_a * sizeof(uint32_t)); | |
| 91 | cudaMalloc(&d_a_wb, n_a * sizeof(uint32_t)); | |
| 92 | cudaMalloc(&d_a_mi, n_a * sizeof(double)); | |
| 93 | cudaMalloc(&d_a_count, n_a * sizeof(double)); | |
| 94 | cudaMemcpy(d_a_wa, h_a_wa, n_a * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 95 | cudaMemcpy(d_a_wb, h_a_wb, n_a * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 96 | cudaMemcpy(d_a_mi, h_a_mi, n_a * sizeof(double), cudaMemcpyHostToDevice); | |
| 97 | cudaMemcpy(d_a_count, h_a_count, n_a * sizeof(double), cudaMemcpyHostToDevice); | |
| 98 | ||
| 99 | /* Upload compartment B */ | |
| 100 | uint32_t *d_b_wa, *d_b_wb; | |
| 101 | double *d_b_mi, *d_b_count; | |
| 102 | cudaMalloc(&d_b_wa, n_b * sizeof(uint32_t)); | |
| 103 | cudaMalloc(&d_b_wb, n_b * sizeof(uint32_t)); | |
| 104 | cudaMalloc(&d_b_mi, n_b * sizeof(double)); | |
| 105 | cudaMalloc(&d_b_count, n_b * sizeof(double)); | |
| 106 | cudaMemcpy(d_b_wa, h_b_wa, n_b * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 107 | cudaMemcpy(d_b_wb, h_b_wb, n_b * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 108 | cudaMemcpy(d_b_mi, h_b_mi, n_b * sizeof(double), cudaMemcpyHostToDevice); | |
| 109 | cudaMemcpy(d_b_count, h_b_count, n_b * sizeof(double), cudaMemcpyHostToDevice); | |
| 110 | ||
| 111 | /* Work buffers */ | |
| 112 | uint32_t *d_nbr_a_ids, *d_nbr_b_ids, *d_nbr_a_cnt, *d_nbr_b_cnt; | |
| 113 | double *d_nbr_a_mi, *d_nbr_b_mi; | |
| 114 | cudaMalloc(&d_nbr_a_ids, NUM_WORDS * DIV_TOP_K * sizeof(uint32_t)); | |
| 115 | cudaMalloc(&d_nbr_a_mi, NUM_WORDS * DIV_TOP_K * sizeof(double)); | |
| 116 | cudaMalloc(&d_nbr_a_cnt, NUM_WORDS * sizeof(uint32_t)); | |
| 117 | cudaMalloc(&d_nbr_b_ids, NUM_WORDS * DIV_TOP_K * sizeof(uint32_t)); | |
| 118 | cudaMalloc(&d_nbr_b_mi, NUM_WORDS * DIV_TOP_K * sizeof(double)); | |
| 119 | cudaMalloc(&d_nbr_b_cnt, NUM_WORDS * sizeof(uint32_t)); | |
| 120 | ||
| 121 | /* Output */ | |
| 122 | double *d_divergence; | |
| 123 | uint32_t *d_polysemy_flag; | |
| 124 | cudaMalloc(&d_divergence, NUM_WORDS * sizeof(double)); | |
| 125 | cudaMalloc(&d_polysemy_flag, NUM_WORDS * sizeof(uint32_t)); | |
| 126 | ||
| 127 | /* Run divergence */ | |
| 128 | double threshold = 0.5; /* divergence > 0.5 → polysemous */ | |
| 129 | divergence_run( | |
| 130 | d_a_wa, d_a_wb, d_a_mi, d_a_count, n_a, | |
| 131 | d_b_wa, d_b_wb, d_b_mi, d_b_count, n_b, | |
| 132 | NUM_WORDS, threshold, | |
| 133 | d_nbr_a_ids, d_nbr_a_mi, d_nbr_a_cnt, | |
| 134 | d_nbr_b_ids, d_nbr_b_mi, d_nbr_b_cnt, | |
| 135 | d_divergence, d_polysemy_flag); | |
| 136 | ||
| 137 | /* Read results */ | |
| 138 | double h_div[10]; | |
| 139 | uint32_t h_poly[10]; | |
| 140 | cudaMemcpy(h_div, d_divergence, NUM_WORDS * sizeof(double), cudaMemcpyDeviceToHost); | |
| 141 | cudaMemcpy(h_poly, d_polysemy_flag, NUM_WORDS * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 142 | ||
| 143 | int pass = 1; | |
| 144 | ||
| 145 | const char* word_names[] = {"the", "money", "loan", "account", "interest", | |
| 146 | "bank", "river", "fish", "water", "large"}; | |
| 147 | ||
| 148 | printf(" Divergence scores:\n"); | |
| 149 | for (int i = 0; i < (int)NUM_WORDS; i++) { | |
| 150 | printf(" %8s (w%d): div=%.4f %s\n", | |
| 151 | word_names[i], i, h_div[i], | |
| 152 | h_poly[i] ? "POLYSEMOUS" : ""); | |
| 153 | } | |
| 154 | ||
| 155 | /* bank(5) should be flagged as polysemous (high divergence) */ | |
| 156 | if (!h_poly[5]) { | |
| 157 | printf(" FAIL: bank(5) should be flagged as polysemous (div=%.4f)\n", h_div[5]); | |
| 158 | pass = 0; | |
| 159 | } | |
| 160 | ||
| 161 | /* bank's divergence should be high (neighborhoods are completely different) */ | |
| 162 | if (h_div[5] < 0.3) { | |
| 163 | printf(" FAIL: bank(5) divergence=%.4f too low (expected > 0.3)\n", h_div[5]); | |
| 164 | pass = 0; | |
| 165 | } | |
| 166 | ||
| 167 | /* the(0) should have low divergence (similar neighborhoods in both) */ | |
| 168 | /* "the" appears with most words in both compartments → similar neighborhood */ | |
| 169 | if (h_div[0] > 0.5) { | |
| 170 | printf(" WARN: the(0) divergence=%.4f seems high for a function word\n", h_div[0]); | |
| 171 | /* Not a hard failure — depends on exact partner overlap */ | |
| 172 | } | |
| 173 | ||
| 174 | /* bank should have HIGHER divergence than "the" */ | |
| 175 | if (h_div[5] <= h_div[0]) { | |
| 176 | printf(" FAIL: bank(5) should have higher divergence than the(0)\n"); | |
| 177 | printf(" bank=%.4f, the=%.4f\n", h_div[5], h_div[0]); | |
| 178 | pass = 0; | |
| 179 | } | |
| 180 | ||
| 181 | /* Count total polysemous words — should be selective (not everything) */ | |
| 182 | int poly_count = 0; | |
| 183 | for (int i = 0; i < (int)NUM_WORDS; i++) { | |
| 184 | if (h_poly[i]) poly_count++; | |
| 185 | } | |
| 186 | printf(" Total polysemous words: %d / %d\n", poly_count, (int)NUM_WORDS); | |
| 187 | ||
| 188 | /* Shouldn't flag too many words */ | |
| 189 | if (poly_count > 5) { | |
| 190 | printf(" FAIL: too many polysemous words (%d), expected <= 5\n", poly_count); | |
| 191 | pass = 0; | |
| 192 | } | |
| 193 | ||
| 194 | /* Neighborhood sanity check: read back neighbor counts */ | |
| 195 | uint32_t h_cnt_a[10], h_cnt_b[10]; | |
| 196 | cudaMemcpy(h_cnt_a, d_nbr_a_cnt, NUM_WORDS * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 197 | cudaMemcpy(h_cnt_b, d_nbr_b_cnt, NUM_WORDS * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 198 | printf(" Neighbor counts (A/B):\n"); | |
| 199 | for (int i = 0; i < (int)NUM_WORDS; i++) { | |
| 200 | printf(" %8s: A=%u B=%u\n", word_names[i], h_cnt_a[i], h_cnt_b[i]); | |
| 201 | } | |
| 202 | ||
| 203 | /* Cleanup */ | |
| 204 | cudaFree(d_a_wa); cudaFree(d_a_wb); cudaFree(d_a_mi); cudaFree(d_a_count); | |
| 205 | cudaFree(d_b_wa); cudaFree(d_b_wb); cudaFree(d_b_mi); cudaFree(d_b_count); | |
| 206 | cudaFree(d_nbr_a_ids); cudaFree(d_nbr_a_mi); cudaFree(d_nbr_a_cnt); | |
| 207 | cudaFree(d_nbr_b_ids); cudaFree(d_nbr_b_mi); cudaFree(d_nbr_b_cnt); | |
| 208 | cudaFree(d_divergence); cudaFree(d_polysemy_flag); | |
| 209 | ||
| 210 | printf(" T7: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 211 | return pass; | |
| 212 | } | |
| 213 | ||
| 214 | int main() { | |
| 215 | printf("=== Divergence Tests ===\n\n"); | |
| 216 | ||
| 217 | int t7 = test_t7_polysemy(); | |
| 218 | ||
| 219 | printf("=== Results ===\n"); | |
| 220 | printf("T7 (Polysemy): %s\n", t7 ? "PASS" : "FAIL"); | |
| 221 | printf("Overall: %s\n", t7 ? "ALL PASSED" : "FAILED"); | |
| 222 | ||
| 223 | return t7 ? 0 : 1; | |
| 224 | } |
| … | … | @@ -1,532 +0,0 @@ |
| 1 | /* | |
| 2 | * test-hashtable.c -- Standalone test for GPU hash table | |
| 3 | * | |
| 4 | * Compile: gcc -O2 -o test-hashtable test-hashtable.c -lOpenCL | |
| 5 | * Run: ./test-hashtable | |
| 6 | * | |
| 7 | * Tests: | |
| 8 | * 1. Bulk insert 1M keys | |
| 9 | * 2. Lookup all inserted keys (verify 100% hit) | |
| 10 | * 3. Lookup non-existent keys (verify 100% miss) | |
| 11 | * 4. Delete some keys, verify they're gone | |
| 12 | * 5. Insert-or-increment (counting) | |
| 13 | * 6. Iterate and verify count | |
| 14 | * 7. Performance: inserts/sec and lookups/sec | |
| 15 | */ | |
| 16 | ||
| 17 | #include <stdio.h> | |
| 18 | #include <stdlib.h> | |
| 19 | #include <stdint.h> | |
| 20 | #include <string.h> | |
| 21 | #include <time.h> | |
| 22 | #include <CL/cl.h> | |
| 23 | ||
| 24 | /* ─── Configuration ─── */ | |
| 25 | ||
| 26 | /* Table capacity — must be power of 2. | |
| 27 | * At 50% load factor, 4M slots supports 2M entries. */ | |
| 28 | #define TABLE_CAPACITY (4 * 1024 * 1024) /* 4M slots */ | |
| 29 | #define NUM_TEST_ITEMS (1 * 1024 * 1024) /* 1M test entries */ | |
| 30 | ||
| 31 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 32 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 33 | ||
| 34 | /* ─── Error checking ─── */ | |
| 35 | ||
| 36 | #define CL_CHECK(err, msg) do { \ | |
| 37 | if ((err) != CL_SUCCESS) { \ | |
| 38 | fprintf(stderr, "OpenCL error %d at %s:%d: %s\n", \ | |
| 39 | (err), __FILE__, __LINE__, (msg)); \ | |
| 40 | exit(1); \ | |
| 41 | } \ | |
| 42 | } while(0) | |
| 43 | ||
| 44 | /* ─── Read kernel source ─── */ | |
| 45 | ||
| 46 | char* read_file(const char* path, size_t* len) | |
| 47 | { | |
| 48 | FILE* f = fopen(path, "r"); | |
| 49 | if (!f) { fprintf(stderr, "Cannot open %s\n", path); exit(1); } | |
| 50 | fseek(f, 0, SEEK_END); | |
| 51 | *len = ftell(f); | |
| 52 | fseek(f, 0, SEEK_SET); | |
| 53 | char* buf = malloc(*len + 1); | |
| 54 | fread(buf, 1, *len, f); | |
| 55 | buf[*len] = '\0'; | |
| 56 | fclose(f); | |
| 57 | return buf; | |
| 58 | } | |
| 59 | ||
| 60 | /* ─── Simple PRNG (splitmix64) ─── */ | |
| 61 | ||
| 62 | static uint64_t rng_state = 0x12345678DEADBEEFULL; | |
| 63 | ||
| 64 | uint64_t next_random(void) | |
| 65 | { | |
| 66 | rng_state += 0x9E3779B97F4A7C15ULL; | |
| 67 | uint64_t z = rng_state; | |
| 68 | z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; | |
| 69 | z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; | |
| 70 | return z ^ (z >> 31); | |
| 71 | } | |
| 72 | ||
| 73 | /* ─── Timing ─── */ | |
| 74 | ||
| 75 | double now_ms(void) | |
| 76 | { | |
| 77 | struct timespec ts; | |
| 78 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 79 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; | |
| 80 | } | |
| 81 | ||
| 82 | /* ─── Main ─── */ | |
| 83 | ||
| 84 | int main(int argc, char** argv) | |
| 85 | { | |
| 86 | cl_int err; | |
| 87 | ||
| 88 | printf("=== GPU Hash Table Test ===\n"); | |
| 89 | printf("Table capacity: %d slots (%lu MB)\n", | |
| 90 | TABLE_CAPACITY, | |
| 91 | (unsigned long)(TABLE_CAPACITY * (sizeof(uint64_t) + sizeof(uint32_t))) / (1024*1024)); | |
| 92 | printf("Test items: %d\n\n", NUM_TEST_ITEMS); | |
| 93 | ||
| 94 | /* ─── OpenCL setup ─── */ | |
| 95 | ||
| 96 | cl_platform_id platform; | |
| 97 | err = clGetPlatformIDs(1, &platform, NULL); | |
| 98 | CL_CHECK(err, "clGetPlatformIDs"); | |
| 99 | ||
| 100 | cl_device_id device; | |
| 101 | err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL); | |
| 102 | CL_CHECK(err, "clGetDeviceIDs"); | |
| 103 | ||
| 104 | /* Print device name */ | |
| 105 | char dev_name[256]; | |
| 106 | clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(dev_name), dev_name, NULL); | |
| 107 | printf("GPU: %s\n", dev_name); | |
| 108 | ||
| 109 | /* Check for int64 atomics */ | |
| 110 | char extensions[4096]; | |
| 111 | clGetDeviceInfo(device, CL_DEVICE_EXTENSIONS, sizeof(extensions), extensions, NULL); | |
| 112 | if (!strstr(extensions, "cl_khr_int64_base_atomics")) { | |
| 113 | fprintf(stderr, "ERROR: Device does not support cl_khr_int64_base_atomics\n"); | |
| 114 | return 1; | |
| 115 | } | |
| 116 | printf("cl_khr_int64_base_atomics: supported\n\n"); | |
| 117 | ||
| 118 | cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err); | |
| 119 | CL_CHECK(err, "clCreateContext"); | |
| 120 | ||
| 121 | cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err); | |
| 122 | CL_CHECK(err, "clCreateCommandQueue"); | |
| 123 | ||
| 124 | /* ─── Build kernel ─── */ | |
| 125 | ||
| 126 | /* Find kernel source relative to this test file or in same dir */ | |
| 127 | const char* kernel_paths[] = { | |
| 128 | "opencog/gpu/gpu-hashtable.cl", | |
| 129 | "gpu-hashtable.cl", | |
| 130 | NULL | |
| 131 | }; | |
| 132 | char* src = NULL; | |
| 133 | size_t src_len = 0; | |
| 134 | for (int i = 0; kernel_paths[i]; i++) { | |
| 135 | FILE* f = fopen(kernel_paths[i], "r"); | |
| 136 | if (f) { | |
| 137 | fclose(f); | |
| 138 | src = read_file(kernel_paths[i], &src_len); | |
| 139 | printf("Kernel source: %s\n", kernel_paths[i]); | |
| 140 | break; | |
| 141 | } | |
| 142 | } | |
| 143 | if (!src) { | |
| 144 | fprintf(stderr, "Cannot find gpu-hashtable.cl\n"); | |
| 145 | return 1; | |
| 146 | } | |
| 147 | ||
| 148 | cl_program program = clCreateProgramWithSource(ctx, 1, | |
| 149 | (const char**)&src, &src_len, &err); | |
| 150 | CL_CHECK(err, "clCreateProgramWithSource"); | |
| 151 | ||
| 152 | err = clBuildProgram(program, 1, &device, "-cl-std=CL1.2", NULL, NULL); | |
| 153 | if (err != CL_SUCCESS) { | |
| 154 | char log[16384]; | |
| 155 | clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, | |
| 156 | sizeof(log), log, NULL); | |
| 157 | fprintf(stderr, "Build error:\n%s\n", log); | |
| 158 | return 1; | |
| 159 | } | |
| 160 | printf("Kernel compiled successfully\n\n"); | |
| 161 | ||
| 162 | /* Create kernels */ | |
| 163 | cl_kernel k_insert = clCreateKernel(program, "ht_insert", &err); | |
| 164 | CL_CHECK(err, "create ht_insert"); | |
| 165 | cl_kernel k_lookup = clCreateKernel(program, "ht_lookup", &err); | |
| 166 | CL_CHECK(err, "create ht_lookup"); | |
| 167 | cl_kernel k_delete = clCreateKernel(program, "ht_delete", &err); | |
| 168 | CL_CHECK(err, "create ht_delete"); | |
| 169 | cl_kernel k_inc = clCreateKernel(program, "ht_insert_or_increment", &err); | |
| 170 | CL_CHECK(err, "create ht_insert_or_increment"); | |
| 171 | cl_kernel k_iter = clCreateKernel(program, "ht_iterate", &err); | |
| 172 | CL_CHECK(err, "create ht_iterate"); | |
| 173 | ||
| 174 | /* ─── Allocate table on GPU ─── */ | |
| 175 | ||
| 176 | cl_ulong capacity = TABLE_CAPACITY; | |
| 177 | ||
| 178 | cl_mem d_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 179 | sizeof(uint64_t) * TABLE_CAPACITY, NULL, &err); | |
| 180 | CL_CHECK(err, "alloc table keys"); | |
| 181 | ||
| 182 | cl_mem d_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 183 | sizeof(uint32_t) * TABLE_CAPACITY, NULL, &err); | |
| 184 | CL_CHECK(err, "alloc table values"); | |
| 185 | ||
| 186 | /* Initialize table to empty (all 0xFF bytes) */ | |
| 187 | uint8_t pattern_ff = 0xFF; | |
| 188 | err = clEnqueueFillBuffer(queue, d_keys, &pattern_ff, 1, | |
| 189 | 0, sizeof(uint64_t) * TABLE_CAPACITY, 0, NULL, NULL); | |
| 190 | CL_CHECK(err, "fill table keys"); | |
| 191 | err = clEnqueueFillBuffer(queue, d_values, &pattern_ff, 1, | |
| 192 | 0, sizeof(uint32_t) * TABLE_CAPACITY, 0, NULL, NULL); | |
| 193 | CL_CHECK(err, "fill table values"); | |
| 194 | clFinish(queue); | |
| 195 | ||
| 196 | /* ─── Generate test data ─── */ | |
| 197 | ||
| 198 | uint64_t* h_keys = malloc(sizeof(uint64_t) * NUM_TEST_ITEMS); | |
| 199 | uint32_t* h_values = malloc(sizeof(uint32_t) * NUM_TEST_ITEMS); | |
| 200 | uint32_t* h_results = malloc(sizeof(uint32_t) * NUM_TEST_ITEMS); | |
| 201 | ||
| 202 | rng_state = 0x12345678DEADBEEFULL; | |
| 203 | for (int i = 0; i < NUM_TEST_ITEMS; i++) { | |
| 204 | uint64_t k = next_random(); | |
| 205 | /* Avoid sentinel key */ | |
| 206 | if (k == HT_EMPTY_KEY) k = 0; | |
| 207 | h_keys[i] = k; | |
| 208 | h_values[i] = (uint32_t)(i & 0xFFFFFFFF); | |
| 209 | } | |
| 210 | ||
| 211 | /* Upload test data to GPU */ | |
| 212 | cl_mem d_in_keys = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 213 | sizeof(uint64_t) * NUM_TEST_ITEMS, h_keys, &err); | |
| 214 | CL_CHECK(err, "alloc input keys"); | |
| 215 | cl_mem d_in_values = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 216 | sizeof(uint32_t) * NUM_TEST_ITEMS, h_values, &err); | |
| 217 | CL_CHECK(err, "alloc input values"); | |
| 218 | cl_mem d_out_values = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 219 | sizeof(uint32_t) * NUM_TEST_ITEMS, NULL, &err); | |
| 220 | CL_CHECK(err, "alloc output values"); | |
| 221 | ||
| 222 | size_t global_size = NUM_TEST_ITEMS; | |
| 223 | size_t local_size = 256; | |
| 224 | /* Round up global size to multiple of local size */ | |
| 225 | global_size = ((global_size + local_size - 1) / local_size) * local_size; | |
| 226 | ||
| 227 | cl_uint num_items = NUM_TEST_ITEMS; | |
| 228 | ||
| 229 | /* ═══ TEST 1: Bulk Insert ═══ */ | |
| 230 | ||
| 231 | printf("--- Test 1: Insert %d items ---\n", NUM_TEST_ITEMS); | |
| 232 | ||
| 233 | clSetKernelArg(k_insert, 0, sizeof(cl_mem), &d_keys); | |
| 234 | clSetKernelArg(k_insert, 1, sizeof(cl_mem), &d_values); | |
| 235 | clSetKernelArg(k_insert, 2, sizeof(cl_ulong), &capacity); | |
| 236 | clSetKernelArg(k_insert, 3, sizeof(cl_mem), &d_in_keys); | |
| 237 | clSetKernelArg(k_insert, 4, sizeof(cl_mem), &d_in_values); | |
| 238 | clSetKernelArg(k_insert, 5, sizeof(cl_uint), &num_items); | |
| 239 | ||
| 240 | double t0 = now_ms(); | |
| 241 | err = clEnqueueNDRangeKernel(queue, k_insert, 1, NULL, | |
| 242 | &global_size, &local_size, 0, NULL, NULL); | |
| 243 | CL_CHECK(err, "enqueue insert"); | |
| 244 | clFinish(queue); | |
| 245 | double t1 = now_ms(); | |
| 246 | ||
| 247 | printf(" Inserted %d items in %.1f ms (%.1f M keys/sec)\n", | |
| 248 | NUM_TEST_ITEMS, t1-t0, NUM_TEST_ITEMS / ((t1-t0) / 1000.0) / 1e6); | |
| 249 | ||
| 250 | /* ═══ TEST 2: Lookup all (should all hit) ═══ */ | |
| 251 | ||
| 252 | printf("--- Test 2: Lookup %d items (expect all found) ---\n", NUM_TEST_ITEMS); | |
| 253 | ||
| 254 | clSetKernelArg(k_lookup, 0, sizeof(cl_mem), &d_keys); | |
| 255 | clSetKernelArg(k_lookup, 1, sizeof(cl_mem), &d_values); | |
| 256 | clSetKernelArg(k_lookup, 2, sizeof(cl_ulong), &capacity); | |
| 257 | clSetKernelArg(k_lookup, 3, sizeof(cl_mem), &d_in_keys); | |
| 258 | clSetKernelArg(k_lookup, 4, sizeof(cl_mem), &d_out_values); | |
| 259 | clSetKernelArg(k_lookup, 5, sizeof(cl_uint), &num_items); | |
| 260 | ||
| 261 | t0 = now_ms(); | |
| 262 | err = clEnqueueNDRangeKernel(queue, k_lookup, 1, NULL, | |
| 263 | &global_size, &local_size, 0, NULL, NULL); | |
| 264 | CL_CHECK(err, "enqueue lookup"); | |
| 265 | clFinish(queue); | |
| 266 | t1 = now_ms(); | |
| 267 | ||
| 268 | /* Read back results */ | |
| 269 | err = clEnqueueReadBuffer(queue, d_out_values, CL_TRUE, 0, | |
| 270 | sizeof(uint32_t) * NUM_TEST_ITEMS, h_results, 0, NULL, NULL); | |
| 271 | CL_CHECK(err, "read lookup results"); | |
| 272 | ||
| 273 | int hits = 0, misses = 0, wrong = 0; | |
| 274 | for (int i = 0; i < NUM_TEST_ITEMS; i++) { | |
| 275 | if (h_results[i] == HT_EMPTY_VALUE) | |
| 276 | misses++; | |
| 277 | else if (h_results[i] == h_values[i]) | |
| 278 | hits++; | |
| 279 | else | |
| 280 | wrong++; | |
| 281 | } | |
| 282 | ||
| 283 | printf(" Lookup in %.1f ms (%.1f M keys/sec)\n", | |
| 284 | t1-t0, NUM_TEST_ITEMS / ((t1-t0) / 1000.0) / 1e6); | |
| 285 | printf(" Hits: %d Misses: %d Wrong: %d\n", hits, misses, wrong); | |
| 286 | printf(" %s\n\n", (hits == NUM_TEST_ITEMS && misses == 0 && wrong == 0) | |
| 287 | ? "PASS" : "FAIL"); | |
| 288 | ||
| 289 | /* ═══ TEST 3: Lookup non-existent keys (should all miss) ═══ */ | |
| 290 | ||
| 291 | printf("--- Test 3: Lookup %d non-existent keys ---\n", NUM_TEST_ITEMS); | |
| 292 | ||
| 293 | /* Generate different keys */ | |
| 294 | rng_state = 0xABCDABCDABCDABCDULL; | |
| 295 | uint64_t* h_miss_keys = malloc(sizeof(uint64_t) * NUM_TEST_ITEMS); | |
| 296 | for (int i = 0; i < NUM_TEST_ITEMS; i++) { | |
| 297 | uint64_t k = next_random(); | |
| 298 | if (k == HT_EMPTY_KEY) k = 0; | |
| 299 | h_miss_keys[i] = k; | |
| 300 | } | |
| 301 | ||
| 302 | cl_mem d_miss_keys = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 303 | sizeof(uint64_t) * NUM_TEST_ITEMS, h_miss_keys, &err); | |
| 304 | CL_CHECK(err, "alloc miss keys"); | |
| 305 | ||
| 306 | clSetKernelArg(k_lookup, 3, sizeof(cl_mem), &d_miss_keys); | |
| 307 | ||
| 308 | t0 = now_ms(); | |
| 309 | err = clEnqueueNDRangeKernel(queue, k_lookup, 1, NULL, | |
| 310 | &global_size, &local_size, 0, NULL, NULL); | |
| 311 | CL_CHECK(err, "enqueue miss lookup"); | |
| 312 | clFinish(queue); | |
| 313 | t1 = now_ms(); | |
| 314 | ||
| 315 | err = clEnqueueReadBuffer(queue, d_out_values, CL_TRUE, 0, | |
| 316 | sizeof(uint32_t) * NUM_TEST_ITEMS, h_results, 0, NULL, NULL); | |
| 317 | CL_CHECK(err, "read miss results"); | |
| 318 | ||
| 319 | misses = 0; | |
| 320 | for (int i = 0; i < NUM_TEST_ITEMS; i++) { | |
| 321 | if (h_results[i] == HT_EMPTY_VALUE) misses++; | |
| 322 | } | |
| 323 | ||
| 324 | printf(" Lookup in %.1f ms\n", t1-t0); | |
| 325 | printf(" Misses: %d / %d\n", misses, NUM_TEST_ITEMS); | |
| 326 | printf(" %s\n\n", (misses == NUM_TEST_ITEMS) ? "PASS" : "FAIL"); | |
| 327 | ||
| 328 | /* ═══ TEST 4: Delete first 1000 keys, verify ═══ */ | |
| 329 | ||
| 330 | int num_delete = 1000; | |
| 331 | printf("--- Test 4: Delete %d keys ---\n", num_delete); | |
| 332 | ||
| 333 | cl_mem d_del_keys = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 334 | sizeof(uint64_t) * num_delete, h_keys, &err); | |
| 335 | CL_CHECK(err, "alloc del keys"); | |
| 336 | ||
| 337 | cl_uint nd = num_delete; | |
| 338 | clSetKernelArg(k_delete, 0, sizeof(cl_mem), &d_keys); | |
| 339 | clSetKernelArg(k_delete, 1, sizeof(cl_mem), &d_values); | |
| 340 | clSetKernelArg(k_delete, 2, sizeof(cl_ulong), &capacity); | |
| 341 | clSetKernelArg(k_delete, 3, sizeof(cl_mem), &d_del_keys); | |
| 342 | clSetKernelArg(k_delete, 4, sizeof(cl_uint), &nd); | |
| 343 | ||
| 344 | size_t del_global = ((num_delete + local_size - 1) / local_size) * local_size; | |
| 345 | err = clEnqueueNDRangeKernel(queue, k_delete, 1, NULL, | |
| 346 | &del_global, &local_size, 0, NULL, NULL); | |
| 347 | CL_CHECK(err, "enqueue delete"); | |
| 348 | clFinish(queue); | |
| 349 | ||
| 350 | /* Lookup deleted keys — should miss */ | |
| 351 | cl_mem d_del_query = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 352 | sizeof(uint64_t) * num_delete, h_keys, &err); | |
| 353 | cl_mem d_del_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 354 | sizeof(uint32_t) * num_delete, NULL, &err); | |
| 355 | ||
| 356 | clSetKernelArg(k_lookup, 3, sizeof(cl_mem), &d_del_query); | |
| 357 | clSetKernelArg(k_lookup, 4, sizeof(cl_mem), &d_del_out); | |
| 358 | clSetKernelArg(k_lookup, 5, sizeof(cl_uint), &nd); | |
| 359 | ||
| 360 | err = clEnqueueNDRangeKernel(queue, k_lookup, 1, NULL, | |
| 361 | &del_global, &local_size, 0, NULL, NULL); | |
| 362 | CL_CHECK(err, "enqueue del verify"); | |
| 363 | clFinish(queue); | |
| 364 | ||
| 365 | uint32_t* h_del_results = malloc(sizeof(uint32_t) * num_delete); | |
| 366 | err = clEnqueueReadBuffer(queue, d_del_out, CL_TRUE, 0, | |
| 367 | sizeof(uint32_t) * num_delete, h_del_results, 0, NULL, NULL); | |
| 368 | ||
| 369 | int del_gone = 0; | |
| 370 | for (int i = 0; i < num_delete; i++) { | |
| 371 | if (h_del_results[i] == HT_EMPTY_VALUE) del_gone++; | |
| 372 | } | |
| 373 | printf(" Deleted keys returning empty: %d / %d\n", del_gone, num_delete); | |
| 374 | printf(" %s\n\n", (del_gone == num_delete) ? "PASS" : "FAIL"); | |
| 375 | ||
| 376 | /* ═══ TEST 5: Insert-or-increment ═══ */ | |
| 377 | ||
| 378 | printf("--- Test 5: Insert-or-increment (counting) ---\n"); | |
| 379 | ||
| 380 | /* Re-initialize table: keys to 0xFF (empty), values to 0 (for counting) */ | |
| 381 | err = clEnqueueFillBuffer(queue, d_keys, &pattern_ff, 1, | |
| 382 | 0, sizeof(uint64_t) * TABLE_CAPACITY, 0, NULL, NULL); | |
| 383 | CL_CHECK(err, "refill keys"); | |
| 384 | uint8_t pattern_00 = 0x00; | |
| 385 | err = clEnqueueFillBuffer(queue, d_values, &pattern_00, 1, | |
| 386 | 0, sizeof(uint32_t) * TABLE_CAPACITY, 0, NULL, NULL); | |
| 387 | CL_CHECK(err, "refill values to 0"); | |
| 388 | clFinish(queue); | |
| 389 | ||
| 390 | /* Insert same 100 keys 1000 times each = 100K operations */ | |
| 391 | int inc_unique = 100; | |
| 392 | int inc_repeats = 1000; | |
| 393 | int inc_total = inc_unique * inc_repeats; | |
| 394 | uint64_t* h_inc_keys = malloc(sizeof(uint64_t) * inc_total); | |
| 395 | ||
| 396 | rng_state = 0xFEEDFACECAFEBABEULL; | |
| 397 | uint64_t base_keys[100]; | |
| 398 | for (int i = 0; i < inc_unique; i++) { | |
| 399 | base_keys[i] = next_random(); | |
| 400 | if (base_keys[i] == HT_EMPTY_KEY) base_keys[i] = 0; | |
| 401 | } | |
| 402 | for (int r = 0; r < inc_repeats; r++) { | |
| 403 | for (int i = 0; i < inc_unique; i++) { | |
| 404 | h_inc_keys[r * inc_unique + i] = base_keys[i]; | |
| 405 | } | |
| 406 | } | |
| 407 | ||
| 408 | cl_mem d_inc_keys = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 409 | sizeof(uint64_t) * inc_total, h_inc_keys, &err); | |
| 410 | CL_CHECK(err, "alloc inc keys"); | |
| 411 | ||
| 412 | cl_uint inc_n = inc_total; | |
| 413 | clSetKernelArg(k_inc, 0, sizeof(cl_mem), &d_keys); | |
| 414 | clSetKernelArg(k_inc, 1, sizeof(cl_mem), &d_values); | |
| 415 | clSetKernelArg(k_inc, 2, sizeof(cl_ulong), &capacity); | |
| 416 | clSetKernelArg(k_inc, 3, sizeof(cl_mem), &d_inc_keys); | |
| 417 | clSetKernelArg(k_inc, 4, sizeof(cl_uint), &inc_n); | |
| 418 | ||
| 419 | size_t inc_global = ((inc_total + local_size - 1) / local_size) * local_size; | |
| 420 | t0 = now_ms(); | |
| 421 | err = clEnqueueNDRangeKernel(queue, k_inc, 1, NULL, | |
| 422 | &inc_global, &local_size, 0, NULL, NULL); | |
| 423 | CL_CHECK(err, "enqueue increment"); | |
| 424 | clFinish(queue); | |
| 425 | t1 = now_ms(); | |
| 426 | printf(" %d increments in %.1f ms\n", inc_total, t1-t0); | |
| 427 | ||
| 428 | /* Lookup the 100 keys, verify counts = 1000 (first insert = 1, then 999 increments) */ | |
| 429 | cl_mem d_inc_query = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 430 | sizeof(uint64_t) * inc_unique, base_keys, &err); | |
| 431 | cl_mem d_inc_out = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 432 | sizeof(uint32_t) * inc_unique, NULL, &err); | |
| 433 | cl_uint inc_u = inc_unique; | |
| 434 | ||
| 435 | clSetKernelArg(k_lookup, 3, sizeof(cl_mem), &d_inc_query); | |
| 436 | clSetKernelArg(k_lookup, 4, sizeof(cl_mem), &d_inc_out); | |
| 437 | clSetKernelArg(k_lookup, 5, sizeof(cl_uint), &inc_u); | |
| 438 | ||
| 439 | size_t inc_q_global = ((inc_unique + local_size - 1) / local_size) * local_size; | |
| 440 | err = clEnqueueNDRangeKernel(queue, k_lookup, 1, NULL, | |
| 441 | &inc_q_global, &local_size, 0, NULL, NULL); | |
| 442 | clFinish(queue); | |
| 443 | ||
| 444 | uint32_t* h_inc_results = malloc(sizeof(uint32_t) * inc_unique); | |
| 445 | clEnqueueReadBuffer(queue, d_inc_out, CL_TRUE, 0, | |
| 446 | sizeof(uint32_t) * inc_unique, h_inc_results, 0, NULL, NULL); | |
| 447 | ||
| 448 | int correct_counts = 0; | |
| 449 | for (int i = 0; i < inc_unique; i++) { | |
| 450 | if (h_inc_results[i] == (uint32_t)inc_repeats) correct_counts++; | |
| 451 | else if (i < 5) printf(" key %d: expected %d got %u\n", i, inc_repeats, h_inc_results[i]); | |
| 452 | } | |
| 453 | printf(" Correct counts (%d): %d / %d\n", inc_repeats, correct_counts, inc_unique); | |
| 454 | printf(" %s\n\n", (correct_counts == inc_unique) ? "PASS" : "FAIL"); | |
| 455 | ||
| 456 | /* ═══ TEST 6: Iterate ═══ */ | |
| 457 | ||
| 458 | printf("--- Test 6: Iterate (collect non-empty entries) ---\n"); | |
| 459 | ||
| 460 | cl_mem d_iter_keys = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 461 | sizeof(uint64_t) * inc_unique * 2, NULL, &err); | |
| 462 | cl_mem d_iter_values = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 463 | sizeof(uint32_t) * inc_unique * 2, NULL, &err); | |
| 464 | uint32_t zero = 0; | |
| 465 | cl_mem d_iter_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 466 | sizeof(uint32_t), &zero, &err); | |
| 467 | cl_uint max_out = inc_unique * 2; | |
| 468 | ||
| 469 | clSetKernelArg(k_iter, 0, sizeof(cl_mem), &d_keys); | |
| 470 | clSetKernelArg(k_iter, 1, sizeof(cl_mem), &d_values); | |
| 471 | clSetKernelArg(k_iter, 2, sizeof(cl_ulong), &capacity); | |
| 472 | clSetKernelArg(k_iter, 3, sizeof(cl_mem), &d_iter_keys); | |
| 473 | clSetKernelArg(k_iter, 4, sizeof(cl_mem), &d_iter_values); | |
| 474 | clSetKernelArg(k_iter, 5, sizeof(cl_mem), &d_iter_count); | |
| 475 | clSetKernelArg(k_iter, 6, sizeof(cl_uint), &max_out); | |
| 476 | ||
| 477 | size_t iter_global = ((TABLE_CAPACITY + local_size - 1) / local_size) * local_size; | |
| 478 | t0 = now_ms(); | |
| 479 | err = clEnqueueNDRangeKernel(queue, k_iter, 1, NULL, | |
| 480 | &iter_global, &local_size, 0, NULL, NULL); | |
| 481 | CL_CHECK(err, "enqueue iterate"); | |
| 482 | clFinish(queue); | |
| 483 | t1 = now_ms(); | |
| 484 | ||
| 485 | uint32_t iter_count; | |
| 486 | clEnqueueReadBuffer(queue, d_iter_count, CL_TRUE, 0, | |
| 487 | sizeof(uint32_t), &iter_count, 0, NULL, NULL); | |
| 488 | printf(" Iterated %u M slots in %.1f ms, found %u entries\n", | |
| 489 | TABLE_CAPACITY / (1024*1024), t1-t0, iter_count); | |
| 490 | printf(" Expected: %d entries\n", inc_unique); | |
| 491 | printf(" %s\n\n", (iter_count == (uint32_t)inc_unique) ? "PASS" : "FAIL"); | |
| 492 | ||
| 493 | /* ═══ Summary ═══ */ | |
| 494 | ||
| 495 | printf("=== All tests complete ===\n"); | |
| 496 | ||
| 497 | /* ─── Cleanup ─── */ | |
| 498 | ||
| 499 | clReleaseMemObject(d_keys); | |
| 500 | clReleaseMemObject(d_values); | |
| 501 | clReleaseMemObject(d_in_keys); | |
| 502 | clReleaseMemObject(d_in_values); | |
| 503 | clReleaseMemObject(d_out_values); | |
| 504 | clReleaseMemObject(d_miss_keys); | |
| 505 | clReleaseMemObject(d_del_keys); | |
| 506 | clReleaseMemObject(d_del_query); | |
| 507 | clReleaseMemObject(d_del_out); | |
| 508 | clReleaseMemObject(d_inc_keys); | |
| 509 | clReleaseMemObject(d_inc_query); | |
| 510 | clReleaseMemObject(d_inc_out); | |
| 511 | clReleaseMemObject(d_iter_keys); | |
| 512 | clReleaseMemObject(d_iter_values); | |
| 513 | clReleaseMemObject(d_iter_count); | |
| 514 | clReleaseKernel(k_insert); | |
| 515 | clReleaseKernel(k_lookup); | |
| 516 | clReleaseKernel(k_delete); | |
| 517 | clReleaseKernel(k_inc); | |
| 518 | clReleaseKernel(k_iter); | |
| 519 | clReleaseProgram(program); | |
| 520 | clReleaseCommandQueue(queue); | |
| 521 | clReleaseContext(ctx); | |
| 522 | free(h_keys); | |
| 523 | free(h_values); | |
| 524 | free(h_results); | |
| 525 | free(h_miss_keys); | |
| 526 | free(h_del_results); | |
| 527 | free(h_inc_keys); | |
| 528 | free(h_inc_results); | |
| 529 | free(src); | |
| 530 | ||
| 531 | return 0; | |
| 532 | } |
| … | … | @@ -1,393 +0,0 @@ |
| 1 | /* test-learning-loop.cu — Tests T8, T9, T10 for persistent learning kernel | |
| 2 | * | |
| 3 | * T8: Persistent kernel convergence (feed sentences, verify classes form) | |
| 4 | * T9: Unified memory sentence feeding (CPU writes while GPU reads) | |
| 5 | * T10: Performance (timing at scale) | |
| 6 | * | |
| 7 | * Build: | |
| 8 | * nvcc -O2 -arch=sm_75 -rdc=true \ | |
| 9 | * -o test-learning-loop \ | |
| 10 | * test-learning-loop.cu gpu-learning-loop.cu \ | |
| 11 | * -lcudadevrt -lm | |
| 12 | */ | |
| 13 | ||
| 14 | #include <cstdio> | |
| 15 | #include <cstdlib> | |
| 16 | #include <cstring> | |
| 17 | #include <cmath> | |
| 18 | #include <cstdint> | |
| 19 | #include <ctime> | |
| 20 | #include <unistd.h> | |
| 21 | #include "gpu-learning-types.h" | |
| 22 | ||
| 23 | extern "C" { | |
| 24 | LearningState* ll_init(uint32_t num_words); | |
| 25 | SentenceRing* ll_init_ring(); | |
| 26 | void ll_feed_sentence(SentenceRing* ring, uint32_t* words, uint32_t length); | |
| 27 | int ll_launch(LearningState* state, SentenceRing* ring, | |
| 28 | int* done_flag, int* pause_flag, | |
| 29 | uint32_t* stats_iteration, uint32_t* stats_pairs, | |
| 30 | uint32_t* stats_classes, double* stats_entropy); | |
| 31 | void ll_wait(); | |
| 32 | void ll_read_classes(LearningState* state, uint32_t* out, uint32_t n); | |
| 33 | void ll_shutdown(LearningState* state, SentenceRing* ring); | |
| 34 | } | |
| 35 | ||
| 36 | /* ─── Helper: Generate synthetic sentences ─── */ | |
| 37 | ||
| 38 | /* Create sentences with word clustering structure: | |
| 39 | * - Cluster A words (0-49) appear together | |
| 40 | * - Cluster B words (50-99) appear together | |
| 41 | * - Cross-cluster pairs are rare | |
| 42 | */ | |
| 43 | void generate_clustered_sentence(uint32_t* words, uint32_t* len, | |
| 44 | int cluster, int num_words_per_cluster, | |
| 45 | int sentence_len) { | |
| 46 | int base = cluster * num_words_per_cluster; | |
| 47 | *len = sentence_len; | |
| 48 | for (int i = 0; i < sentence_len; i++) { | |
| 49 | words[i] = base + (rand() % num_words_per_cluster); | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | /* ─── T8: Persistent kernel convergence ─── */ | |
| 54 | ||
| 55 | int test_t8_convergence() { | |
| 56 | printf("T8: Persistent kernel convergence\n"); | |
| 57 | ||
| 58 | const uint32_t NUM_WORDS = 100; | |
| 59 | const int WORDS_PER_CLUSTER = 50; | |
| 60 | const int NUM_SENTENCES = 200; | |
| 61 | const int SENTENCE_LEN = 5; | |
| 62 | ||
| 63 | srand(42); | |
| 64 | ||
| 65 | /* Initialize */ | |
| 66 | LearningState* state = ll_init(NUM_WORDS); | |
| 67 | SentenceRing* ring = ll_init_ring(); | |
| 68 | ||
| 69 | int* done_flag; | |
| 70 | int* pause_flag; | |
| 71 | uint32_t *stats_iter, *stats_pairs, *stats_classes; | |
| 72 | double* stats_entropy; | |
| 73 | cudaMallocManaged(&done_flag, sizeof(int)); | |
| 74 | cudaMallocManaged(&pause_flag, sizeof(int)); | |
| 75 | cudaMallocManaged(&stats_iter, sizeof(uint32_t)); | |
| 76 | cudaMallocManaged(&stats_pairs, sizeof(uint32_t)); | |
| 77 | cudaMallocManaged(&stats_classes, sizeof(uint32_t)); | |
| 78 | cudaMallocManaged(&stats_entropy, sizeof(double)); | |
| 79 | ||
| 80 | *done_flag = 0; | |
| 81 | *pause_flag = 0; | |
| 82 | *stats_iter = 0; | |
| 83 | *stats_pairs = 0; | |
| 84 | *stats_classes = 0; | |
| 85 | *stats_entropy = 0.0; | |
| 86 | ||
| 87 | /* Launch persistent kernel */ | |
| 88 | int err = ll_launch(state, ring, done_flag, pause_flag, | |
| 89 | stats_iter, stats_pairs, stats_classes, stats_entropy); | |
| 90 | if (err != 0) { | |
| 91 | printf(" FAIL: kernel launch failed\n"); | |
| 92 | return 0; | |
| 93 | } | |
| 94 | printf(" Persistent kernel launched\n"); | |
| 95 | ||
| 96 | /* Feed sentences: alternating cluster A and cluster B */ | |
| 97 | for (int s = 0; s < NUM_SENTENCES; s++) { | |
| 98 | uint32_t words[64]; | |
| 99 | uint32_t len; | |
| 100 | int cluster = (s % 2 == 0) ? 0 : 1; | |
| 101 | generate_clustered_sentence(words, &len, cluster, WORDS_PER_CLUSTER, SENTENCE_LEN); | |
| 102 | ll_feed_sentence(ring, words, len); | |
| 103 | ||
| 104 | /* Small delay every 10 sentences to let GPU catch up */ | |
| 105 | if (s % 10 == 0) usleep(1000); /* 1ms */ | |
| 106 | } | |
| 107 | ||
| 108 | printf(" Fed %d sentences\n", NUM_SENTENCES); | |
| 109 | ||
| 110 | /* Wait for processing — poll status */ | |
| 111 | int max_wait = 100; /* 10 seconds max */ | |
| 112 | uint32_t last_iter = 0; | |
| 113 | for (int w = 0; w < max_wait; w++) { | |
| 114 | usleep(100000); /* 100ms */ | |
| 115 | printf(" [%d] iter=%u pairs=%u classes=%u entropy=%.4f done=%d\n", | |
| 116 | w, *stats_iter, *stats_pairs, *stats_classes, *stats_entropy, *done_flag); | |
| 117 | ||
| 118 | if (*done_flag) break; | |
| 119 | ||
| 120 | /* If no progress for several polls, stop manually */ | |
| 121 | if (*stats_iter == last_iter && w > 20) { | |
| 122 | printf(" No progress, stopping manually\n"); | |
| 123 | *done_flag = 1; | |
| 124 | break; | |
| 125 | } | |
| 126 | last_iter = *stats_iter; | |
| 127 | } | |
| 128 | ||
| 129 | /* Wait for kernel to finish */ | |
| 130 | ll_wait(); | |
| 131 | ||
| 132 | /* Read final results */ | |
| 133 | uint32_t h_classes[100]; | |
| 134 | ll_read_classes(state, h_classes, NUM_WORDS); | |
| 135 | ||
| 136 | int pass = 1; | |
| 137 | ||
| 138 | /* Verify: some pairs were counted */ | |
| 139 | printf(" Final: %u pairs, %u classes, %u iterations\n", | |
| 140 | *stats_pairs, *stats_classes, *stats_iter); | |
| 141 | ||
| 142 | if (*stats_pairs == 0) { | |
| 143 | printf(" FAIL: no pairs counted\n"); | |
| 144 | pass = 0; | |
| 145 | } | |
| 146 | ||
| 147 | /* Verify: at least 1 iteration ran */ | |
| 148 | if (*stats_iter == 0) { | |
| 149 | printf(" FAIL: no iterations completed\n"); | |
| 150 | pass = 0; | |
| 151 | } | |
| 152 | ||
| 153 | /* Verify: some classes formed (not all singletons) */ | |
| 154 | if (*stats_classes > 0 && *stats_classes < NUM_WORDS) { | |
| 155 | printf(" Classes formed: %u (good — not all singletons)\n", *stats_classes); | |
| 156 | } else if (*stats_classes == 0) { | |
| 157 | printf(" WARN: no classes formed (may need more data or lower threshold)\n"); | |
| 158 | } | |
| 159 | ||
| 160 | /* Check that some cluster A words share a class */ | |
| 161 | int shared_a = 0, shared_b = 0; | |
| 162 | for (uint32_t i = 1; i < WORDS_PER_CLUSTER; i++) { | |
| 163 | if (h_classes[i] == h_classes[0] && h_classes[0] != 0xFFFFFFFFU) shared_a++; | |
| 164 | } | |
| 165 | for (uint32_t i = WORDS_PER_CLUSTER + 1; i < NUM_WORDS; i++) { | |
| 166 | if (h_classes[i] == h_classes[WORDS_PER_CLUSTER] && | |
| 167 | h_classes[WORDS_PER_CLUSTER] != 0xFFFFFFFFU) shared_b++; | |
| 168 | } | |
| 169 | printf(" Cluster A sharing: %d/%d Cluster B sharing: %d/%d\n", | |
| 170 | shared_a, WORDS_PER_CLUSTER - 1, shared_b, WORDS_PER_CLUSTER - 1); | |
| 171 | ||
| 172 | /* Cleanup */ | |
| 173 | ll_shutdown(state, ring); | |
| 174 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 175 | cudaFree(stats_iter); cudaFree(stats_pairs); | |
| 176 | cudaFree(stats_classes); cudaFree(stats_entropy); | |
| 177 | ||
| 178 | printf(" T8: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 179 | return pass; | |
| 180 | } | |
| 181 | ||
| 182 | /* ─── T9: Unified memory sentence feeding ─── */ | |
| 183 | ||
| 184 | int test_t9_unified_memory() { | |
| 185 | printf("T9: Unified memory sentence feeding\n"); | |
| 186 | ||
| 187 | const uint32_t NUM_WORDS = 50; | |
| 188 | const int NUM_SENTENCES = 100; | |
| 189 | ||
| 190 | srand(123); | |
| 191 | ||
| 192 | LearningState* state = ll_init(NUM_WORDS); | |
| 193 | SentenceRing* ring = ll_init_ring(); | |
| 194 | ||
| 195 | int* done_flag; | |
| 196 | int* pause_flag; | |
| 197 | uint32_t *stats_iter, *stats_pairs, *stats_classes; | |
| 198 | double* stats_entropy; | |
| 199 | cudaMallocManaged(&done_flag, sizeof(int)); | |
| 200 | cudaMallocManaged(&pause_flag, sizeof(int)); | |
| 201 | cudaMallocManaged(&stats_iter, sizeof(uint32_t)); | |
| 202 | cudaMallocManaged(&stats_pairs, sizeof(uint32_t)); | |
| 203 | cudaMallocManaged(&stats_classes, sizeof(uint32_t)); | |
| 204 | cudaMallocManaged(&stats_entropy, sizeof(double)); | |
| 205 | ||
| 206 | *done_flag = 0; | |
| 207 | *pause_flag = 0; | |
| 208 | *stats_iter = 0; | |
| 209 | *stats_pairs = 0; | |
| 210 | *stats_classes = 0; | |
| 211 | *stats_entropy = 0.0; | |
| 212 | ||
| 213 | /* Launch kernel */ | |
| 214 | ll_launch(state, ring, done_flag, pause_flag, | |
| 215 | stats_iter, stats_pairs, stats_classes, stats_entropy); | |
| 216 | ||
| 217 | /* Rapid-fire sentence feeding — test that CPU writes don't race with GPU reads */ | |
| 218 | int pass = 1; | |
| 219 | int total_fed = 0; | |
| 220 | ||
| 221 | for (int batch = 0; batch < 10; batch++) { | |
| 222 | /* Feed 10 sentences as fast as possible */ | |
| 223 | for (int s = 0; s < 10; s++) { | |
| 224 | uint32_t words[5]; | |
| 225 | for (int i = 0; i < 5; i++) words[i] = rand() % NUM_WORDS; | |
| 226 | ll_feed_sentence(ring, words, 5); | |
| 227 | total_fed++; | |
| 228 | } | |
| 229 | /* Brief pause between batches */ | |
| 230 | usleep(10000); /* 10ms */ | |
| 231 | } | |
| 232 | ||
| 233 | printf(" Fed %d sentences in rapid bursts\n", total_fed); | |
| 234 | ||
| 235 | /* Let GPU process */ | |
| 236 | usleep(500000); /* 500ms */ | |
| 237 | ||
| 238 | /* Stop and wait */ | |
| 239 | *done_flag = 1; | |
| 240 | ll_wait(); | |
| 241 | ||
| 242 | printf(" Final: %u pairs counted, %u iterations\n", *stats_pairs, *stats_iter); | |
| 243 | ||
| 244 | /* Verify all sentences were processed (pairs > 0) */ | |
| 245 | if (*stats_pairs == 0) { | |
| 246 | printf(" FAIL: no pairs counted — unified memory feeding broken\n"); | |
| 247 | pass = 0; | |
| 248 | } else { | |
| 249 | printf(" Pairs counted: %u (unified memory feeding works)\n", *stats_pairs); | |
| 250 | } | |
| 251 | ||
| 252 | /* Verify ring buffer indices are consistent */ | |
| 253 | printf(" Ring: write_idx=%u read_idx=%u\n", ring->write_idx, ring->read_idx); | |
| 254 | if (ring->read_idx < ring->write_idx - 64) { /* allow ring size lag */ | |
| 255 | printf(" WARN: GPU may not have consumed all sentences\n"); | |
| 256 | } | |
| 257 | ||
| 258 | /* Cleanup */ | |
| 259 | ll_shutdown(state, ring); | |
| 260 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 261 | cudaFree(stats_iter); cudaFree(stats_pairs); | |
| 262 | cudaFree(stats_classes); cudaFree(stats_entropy); | |
| 263 | ||
| 264 | printf(" T9: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 265 | return pass; | |
| 266 | } | |
| 267 | ||
| 268 | /* ─── T10: Performance ─── */ | |
| 269 | ||
| 270 | int test_t10_performance() { | |
| 271 | printf("T10: Performance (timing)\n"); | |
| 272 | ||
| 273 | const uint32_t NUM_WORDS = 5000; | |
| 274 | const int NUM_SENTENCES = 1000; | |
| 275 | const int SENTENCE_LEN = 8; | |
| 276 | ||
| 277 | srand(999); | |
| 278 | ||
| 279 | LearningState* state = ll_init(NUM_WORDS); | |
| 280 | SentenceRing* ring = ll_init_ring(); | |
| 281 | ||
| 282 | int* done_flag; | |
| 283 | int* pause_flag; | |
| 284 | uint32_t *stats_iter, *stats_pairs, *stats_classes; | |
| 285 | double* stats_entropy; | |
| 286 | cudaMallocManaged(&done_flag, sizeof(int)); | |
| 287 | cudaMallocManaged(&pause_flag, sizeof(int)); | |
| 288 | cudaMallocManaged(&stats_iter, sizeof(uint32_t)); | |
| 289 | cudaMallocManaged(&stats_pairs, sizeof(uint32_t)); | |
| 290 | cudaMallocManaged(&stats_classes, sizeof(uint32_t)); | |
| 291 | cudaMallocManaged(&stats_entropy, sizeof(double)); | |
| 292 | ||
| 293 | *done_flag = 0; | |
| 294 | *pause_flag = 0; | |
| 295 | *stats_iter = 0; | |
| 296 | ||
| 297 | struct timespec start, end; | |
| 298 | clock_gettime(CLOCK_MONOTONIC, &start); | |
| 299 | ||
| 300 | /* Launch kernel */ | |
| 301 | ll_launch(state, ring, done_flag, pause_flag, | |
| 302 | stats_iter, stats_pairs, stats_classes, stats_entropy); | |
| 303 | ||
| 304 | /* Feed all sentences */ | |
| 305 | for (int s = 0; s < NUM_SENTENCES; s++) { | |
| 306 | uint32_t words[64]; | |
| 307 | uint32_t len = SENTENCE_LEN; | |
| 308 | /* 10 clusters of 500 words each */ | |
| 309 | int cluster = s % 10; | |
| 310 | int base = cluster * 500; | |
| 311 | for (int i = 0; i < SENTENCE_LEN; i++) { | |
| 312 | words[i] = base + (rand() % 500); | |
| 313 | } | |
| 314 | ll_feed_sentence(ring, words, len); | |
| 315 | } | |
| 316 | ||
| 317 | /* Wait for processing */ | |
| 318 | int max_wait = 300; /* 30 seconds max */ | |
| 319 | for (int w = 0; w < max_wait; w++) { | |
| 320 | usleep(100000); | |
| 321 | if (*done_flag) break; | |
| 322 | if (w > 50 && *stats_iter > 0) { | |
| 323 | /* Have some iterations, stop for timing */ | |
| 324 | *done_flag = 1; | |
| 325 | break; | |
| 326 | } | |
| 327 | } | |
| 328 | ||
| 329 | ll_wait(); | |
| 330 | ||
| 331 | clock_gettime(CLOCK_MONOTONIC, &end); | |
| 332 | ||
| 333 | double elapsed = (end.tv_sec - start.tv_sec) + | |
| 334 | (end.tv_nsec - start.tv_nsec) / 1e9; | |
| 335 | ||
| 336 | int pass = 1; | |
| 337 | ||
| 338 | printf(" %d words, %d sentences, %d sentence length\n", | |
| 339 | (int)NUM_WORDS, NUM_SENTENCES, SENTENCE_LEN); | |
| 340 | printf(" Total time: %.3f seconds\n", elapsed); | |
| 341 | printf(" Pairs: %u, Iterations: %u, Classes: %u\n", | |
| 342 | *stats_pairs, *stats_iter, *stats_classes); | |
| 343 | ||
| 344 | if (*stats_iter > 0) { | |
| 345 | double ms_per_iter = (elapsed * 1000.0) / *stats_iter; | |
| 346 | printf(" Time per iteration: %.1f ms\n", ms_per_iter); | |
| 347 | ||
| 348 | /* Target: < 50ms per iteration at 5K words */ | |
| 349 | if (ms_per_iter < 50.0) { | |
| 350 | printf(" Performance: GOOD (< 50ms target)\n"); | |
| 351 | } else if (ms_per_iter < 200.0) { | |
| 352 | printf(" Performance: ACCEPTABLE (< 200ms)\n"); | |
| 353 | } else { | |
| 354 | printf(" Performance: SLOW (%.1f ms > 200ms target)\n", ms_per_iter); | |
| 355 | /* Don't fail — performance varies with system load */ | |
| 356 | } | |
| 357 | } | |
| 358 | ||
| 359 | double sentences_per_sec = (double)NUM_SENTENCES / elapsed; | |
| 360 | printf(" Sentence throughput: %.0f sentences/second\n", sentences_per_sec); | |
| 361 | ||
| 362 | if (*stats_pairs == 0) { | |
| 363 | printf(" FAIL: no pairs counted\n"); | |
| 364 | pass = 0; | |
| 365 | } | |
| 366 | ||
| 367 | /* Cleanup */ | |
| 368 | ll_shutdown(state, ring); | |
| 369 | cudaFree(done_flag); cudaFree(pause_flag); | |
| 370 | cudaFree(stats_iter); cudaFree(stats_pairs); | |
| 371 | cudaFree(stats_classes); cudaFree(stats_entropy); | |
| 372 | ||
| 373 | printf(" T10: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 374 | return pass; | |
| 375 | } | |
| 376 | ||
| 377 | /* ─── Main ─── */ | |
| 378 | ||
| 379 | int main() { | |
| 380 | printf("=== Learning Loop Tests ===\n\n"); | |
| 381 | ||
| 382 | int t8 = test_t8_convergence(); | |
| 383 | int t9 = test_t9_unified_memory(); | |
| 384 | int t10 = test_t10_performance(); | |
| 385 | ||
| 386 | printf("=== Results ===\n"); | |
| 387 | printf("T8 (Convergence): %s\n", t8 ? "PASS" : "FAIL"); | |
| 388 | printf("T9 (Unified memory): %s\n", t9 ? "PASS" : "FAIL"); | |
| 389 | printf("T10 (Performance): %s\n", t10 ? "PASS" : "FAIL"); | |
| 390 | printf("Overall: %s\n", (t8 && t9 && t10) ? "ALL PASSED" : "SOME FAILED"); | |
| 391 | ||
| 392 | return (t8 && t9 && t10) ? 0 : 1; | |
| 393 | } |
| … | … | @@ -1,961 +0,0 @@ |
| 1 | /* | |
| 2 | * test-mi.c -- Test GPU-resident MI computation | |
| 3 | * | |
| 4 | * Compile: gcc -O2 -o test-mi test-mi.c -lOpenCL -lm | |
| 5 | * Run: ./test-mi | |
| 6 | * | |
| 7 | * Tests the full pipeline: count sentences → compute MI → verify. | |
| 8 | * All data stays on GPU — no CPU↔GPU marshaling for MI. | |
| 9 | * | |
| 10 | * Tests: | |
| 11 | * 1. Manual MI verification (known counts → expected MI) | |
| 12 | * 2. Pipeline: count → MI (sentences → pairs → MI in one flow) | |
| 13 | * 3. Dirty-only MI (incremental recompute) | |
| 14 | * 4. MI statistics (count positive/above-threshold) | |
| 15 | * 5. MI filter (compact high-MI pairs) | |
| 16 | * 6. Benchmark: 1000 sentences → MI on all pairs | |
| 17 | */ | |
| 18 | ||
| 19 | #include <stdio.h> | |
| 20 | #include <stdlib.h> | |
| 21 | #include <stdint.h> | |
| 22 | #include <string.h> | |
| 23 | #include <math.h> | |
| 24 | #include <time.h> | |
| 25 | #include <CL/cl.h> | |
| 26 | ||
| 27 | /* ─── Pool capacities ─── */ | |
| 28 | ||
| 29 | #define WORD_CAPACITY (128 * 1024) | |
| 30 | #define PAIR_CAPACITY (4 * 1024 * 1024) | |
| 31 | #define SECTION_CAPACITY (1024 * 1024) | |
| 32 | #define WORD_HT_CAPACITY (256 * 1024) | |
| 33 | #define PAIR_HT_CAPACITY (8 * 1024 * 1024) | |
| 34 | #define SECTION_HT_CAPACITY (2 * 1024 * 1024) | |
| 35 | ||
| 36 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 37 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 38 | ||
| 39 | /* ─── Error checking ─── */ | |
| 40 | ||
| 41 | #define CL_CHECK(err, msg) do { \ | |
| 42 | if ((err) != CL_SUCCESS) { \ | |
| 43 | fprintf(stderr, "OpenCL error %d at %s:%d: %s\n", \ | |
| 44 | (err), __FILE__, __LINE__, (msg)); \ | |
| 45 | exit(1); \ | |
| 46 | } \ | |
| 47 | } while(0) | |
| 48 | ||
| 49 | /* ─── Read file ─── */ | |
| 50 | ||
| 51 | char* read_file(const char* path, size_t* len) | |
| 52 | { | |
| 53 | FILE* f = fopen(path, "r"); | |
| 54 | if (!f) { fprintf(stderr, "Cannot open %s\n", path); exit(1); } | |
| 55 | fseek(f, 0, SEEK_END); | |
| 56 | *len = ftell(f); | |
| 57 | fseek(f, 0, SEEK_SET); | |
| 58 | char* buf = malloc(*len + 1); | |
| 59 | size_t n = fread(buf, 1, *len, f); | |
| 60 | buf[n] = '\0'; | |
| 61 | *len = n; | |
| 62 | fclose(f); | |
| 63 | return buf; | |
| 64 | } | |
| 65 | ||
| 66 | /* ─── Timing ─── */ | |
| 67 | ||
| 68 | double now_ms(void) | |
| 69 | { | |
| 70 | struct timespec ts; | |
| 71 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 72 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; | |
| 73 | } | |
| 74 | ||
| 75 | /* ─── CPU MI for verification ─── */ | |
| 76 | ||
| 77 | double cpu_mi(double count, double left_marg, double right_marg, double n) | |
| 78 | { | |
| 79 | if (count < 1.0 || left_marg < 1e-10 || right_marg < 1e-10 || n < 1e-10) | |
| 80 | return 0.0; | |
| 81 | return log2(count * n / (left_marg * right_marg)); | |
| 82 | } | |
| 83 | ||
| 84 | /* ─── Reset pools ─── */ | |
| 85 | ||
| 86 | void reset_pools(cl_command_queue queue, | |
| 87 | cl_mem pht_keys, cl_mem pht_values, | |
| 88 | cl_mem pair_count, cl_mem pair_mi, cl_mem pair_flags, | |
| 89 | cl_mem word_count, | |
| 90 | cl_mem pair_next_free, cl_mem total_pair_count) | |
| 91 | { | |
| 92 | uint8_t pat_ff = 0xFF, pat_00 = 0x00; | |
| 93 | uint32_t zero = 0; | |
| 94 | ||
| 95 | clEnqueueFillBuffer(queue, pht_keys, &pat_ff, 1, 0, | |
| 96 | sizeof(uint64_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 97 | clEnqueueFillBuffer(queue, pht_values, &pat_ff, 1, 0, | |
| 98 | sizeof(uint32_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 99 | clEnqueueFillBuffer(queue, pair_count, &pat_00, 1, 0, | |
| 100 | sizeof(double) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 101 | clEnqueueFillBuffer(queue, pair_mi, &pat_00, 1, 0, | |
| 102 | sizeof(double) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 103 | clEnqueueFillBuffer(queue, pair_flags, &pat_00, 1, 0, | |
| 104 | sizeof(uint32_t) * PAIR_CAPACITY, 0, NULL, NULL); | |
| 105 | clEnqueueFillBuffer(queue, word_count, &pat_00, 1, 0, | |
| 106 | sizeof(double) * WORD_CAPACITY, 0, NULL, NULL); | |
| 107 | clEnqueueWriteBuffer(queue, pair_next_free, CL_FALSE, 0, | |
| 108 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 109 | clEnqueueWriteBuffer(queue, total_pair_count, CL_FALSE, 0, | |
| 110 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 111 | clFinish(queue); | |
| 112 | } | |
| 113 | ||
| 114 | /* ─── Main ─── */ | |
| 115 | ||
| 116 | int main(int argc, char** argv) | |
| 117 | { | |
| 118 | cl_int err; | |
| 119 | int pass_count = 0, fail_count = 0; | |
| 120 | ||
| 121 | printf("=== GPU MI Computation Test ===\n\n"); | |
| 122 | ||
| 123 | /* ─── OpenCL setup ─── */ | |
| 124 | ||
| 125 | cl_platform_id platform; | |
| 126 | err = clGetPlatformIDs(1, &platform, NULL); | |
| 127 | CL_CHECK(err, "platform"); | |
| 128 | ||
| 129 | cl_device_id device; | |
| 130 | err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL); | |
| 131 | CL_CHECK(err, "device"); | |
| 132 | ||
| 133 | char dev_name[256]; | |
| 134 | clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(dev_name), dev_name, NULL); | |
| 135 | printf("GPU: %s\n", dev_name); | |
| 136 | ||
| 137 | cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err); | |
| 138 | CL_CHECK(err, "context"); | |
| 139 | ||
| 140 | cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err); | |
| 141 | CL_CHECK(err, "queue"); | |
| 142 | ||
| 143 | /* ─── Load and concatenate all 4 kernel sources ─── */ | |
| 144 | ||
| 145 | size_t ht_len, as_len, ct_len, mi_len; | |
| 146 | char* ht_src = read_file("opencog/gpu/gpu-hashtable.cl", &ht_len); | |
| 147 | char* as_src = read_file("opencog/gpu/gpu-atomspace.cl", &as_len); | |
| 148 | char* ct_src = read_file("opencog/gpu/gpu-counting.cl", &ct_len); | |
| 149 | char* mi_src = read_file("opencog/gpu/gpu-mi.cl", &mi_len); | |
| 150 | ||
| 151 | size_t total_len = ht_len + 1 + as_len + 1 + ct_len + 1 + mi_len; | |
| 152 | char* combined = malloc(total_len + 1); | |
| 153 | size_t off = 0; | |
| 154 | memcpy(combined + off, ht_src, ht_len); off += ht_len; | |
| 155 | combined[off++] = '\n'; | |
| 156 | memcpy(combined + off, as_src, as_len); off += as_len; | |
| 157 | combined[off++] = '\n'; | |
| 158 | memcpy(combined + off, ct_src, ct_len); off += ct_len; | |
| 159 | combined[off++] = '\n'; | |
| 160 | memcpy(combined + off, mi_src, mi_len); off += mi_len; | |
| 161 | combined[off] = '\0'; | |
| 162 | ||
| 163 | cl_program program = clCreateProgramWithSource(ctx, 1, | |
| 164 | (const char**)&combined, &total_len, &err); | |
| 165 | CL_CHECK(err, "create program"); | |
| 166 | ||
| 167 | char build_opts[512]; | |
| 168 | snprintf(build_opts, sizeof(build_opts), | |
| 169 | "-cl-std=CL1.2 " | |
| 170 | "-DWORD_CAPACITY=%d " | |
| 171 | "-DPAIR_CAPACITY=%d " | |
| 172 | "-DSECTION_CAPACITY=%d " | |
| 173 | "-DWORD_HT_CAPACITY=%d " | |
| 174 | "-DPAIR_HT_CAPACITY=%d " | |
| 175 | "-DSECTION_HT_CAPACITY=%d", | |
| 176 | WORD_CAPACITY, PAIR_CAPACITY, SECTION_CAPACITY, | |
| 177 | WORD_HT_CAPACITY, PAIR_HT_CAPACITY, SECTION_HT_CAPACITY); | |
| 178 | ||
| 179 | err = clBuildProgram(program, 1, &device, build_opts, NULL, NULL); | |
| 180 | if (err != CL_SUCCESS) { | |
| 181 | char log[16384]; | |
| 182 | clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, | |
| 183 | sizeof(log), log, NULL); | |
| 184 | fprintf(stderr, "Build error:\n%s\n", log); | |
| 185 | return 1; | |
| 186 | } | |
| 187 | printf("All kernels compiled successfully\n\n"); | |
| 188 | ||
| 189 | /* ─── Create kernels ─── */ | |
| 190 | ||
| 191 | cl_kernel k_count = clCreateKernel(program, "count_sentence_pairs", &err); | |
| 192 | CL_CHECK(err, "kernel count_sentence_pairs"); | |
| 193 | cl_kernel k_mi_all = clCreateKernel(program, "compute_mi_resident", &err); | |
| 194 | CL_CHECK(err, "kernel compute_mi_resident"); | |
| 195 | cl_kernel k_mi_dirty = clCreateKernel(program, "compute_mi_dirty", &err); | |
| 196 | CL_CHECK(err, "kernel compute_mi_dirty"); | |
| 197 | cl_kernel k_mi_stats = clCreateKernel(program, "mi_stats", &err); | |
| 198 | CL_CHECK(err, "kernel mi_stats"); | |
| 199 | cl_kernel k_mi_filter = clCreateKernel(program, "mi_filter", &err); | |
| 200 | CL_CHECK(err, "kernel mi_filter"); | |
| 201 | cl_kernel k_read_mi = clCreateKernel(program, "read_pairs_with_mi", &err); | |
| 202 | CL_CHECK(err, "kernel read_pairs_with_mi"); | |
| 203 | ||
| 204 | size_t local_size = 256; | |
| 205 | ||
| 206 | /* ─── Allocate GPU buffers ─── */ | |
| 207 | ||
| 208 | uint32_t zero = 0; | |
| 209 | ||
| 210 | cl_mem pht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 211 | sizeof(uint64_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 212 | cl_mem pht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 213 | sizeof(uint32_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 214 | cl_mem pair_word_a = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 215 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 216 | cl_mem pair_word_b = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 217 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 218 | cl_mem pair_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 219 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 220 | cl_mem pair_mi = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 221 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 222 | cl_mem pair_flags = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 223 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 224 | cl_mem pair_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 225 | sizeof(uint32_t), &zero, &err); | |
| 226 | cl_mem word_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 227 | sizeof(double) * WORD_CAPACITY, NULL, &err); | |
| 228 | cl_mem total_pair_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 229 | sizeof(uint32_t), &zero, &err); | |
| 230 | ||
| 231 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 232 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 233 | ||
| 234 | printf("GPU buffers ready\n\n"); | |
| 235 | ||
| 236 | /* ═══════════════════════════════════════════════════════════════ | |
| 237 | * TEST 1: Manual MI verification | |
| 238 | * | |
| 239 | * Manually set pair counts and word marginals, then verify | |
| 240 | * that compute_mi_resident produces correct MI values. | |
| 241 | * | |
| 242 | * Setup (3 pairs, 4 words): | |
| 243 | * pair 0: (word0, word1) count=10 → MI = log2(10*100/(30*40)) | |
| 244 | * pair 1: (word0, word2) count=5 → MI = log2(5*100/(30*20)) | |
| 245 | * pair 2: (word1, word3) count=20 → MI = log2(20*100/(40*50)) | |
| 246 | * | |
| 247 | * Word marginals: word0=30, word1=40, word2=20, word3=50 | |
| 248 | * N = 100 (total pair observations) | |
| 249 | * ═══════════════════════════════════════════════════════════════ */ | |
| 250 | ||
| 251 | printf("--- Test 1: Manual MI verification ---\n"); | |
| 252 | ||
| 253 | /* Write pair data directly to GPU */ | |
| 254 | uint32_t h_pa[] = {0, 0, 1}; | |
| 255 | uint32_t h_pb[] = {1, 2, 3}; | |
| 256 | double h_pc[] = {10.0, 5.0, 20.0}; | |
| 257 | double h_wc[] = {30.0, 40.0, 20.0, 50.0}; | |
| 258 | uint32_t h_np = 3; | |
| 259 | ||
| 260 | clEnqueueWriteBuffer(queue, pair_word_a, CL_FALSE, 0, | |
| 261 | sizeof(uint32_t) * 3, h_pa, 0, NULL, NULL); | |
| 262 | clEnqueueWriteBuffer(queue, pair_word_b, CL_FALSE, 0, | |
| 263 | sizeof(uint32_t) * 3, h_pb, 0, NULL, NULL); | |
| 264 | clEnqueueWriteBuffer(queue, pair_count, CL_FALSE, 0, | |
| 265 | sizeof(double) * 3, h_pc, 0, NULL, NULL); | |
| 266 | clEnqueueWriteBuffer(queue, word_count, CL_FALSE, 0, | |
| 267 | sizeof(double) * 4, h_wc, 0, NULL, NULL); | |
| 268 | clEnqueueWriteBuffer(queue, pair_next_free, CL_FALSE, 0, | |
| 269 | sizeof(uint32_t), &h_np, 0, NULL, NULL); | |
| 270 | clFinish(queue); | |
| 271 | ||
| 272 | /* Compute expected MI on CPU */ | |
| 273 | double n_total = 100.0; | |
| 274 | double expected_mi[3]; | |
| 275 | expected_mi[0] = cpu_mi(10.0, 30.0, 40.0, 100.0); /* log2(10*100/1200) */ | |
| 276 | expected_mi[1] = cpu_mi(5.0, 30.0, 20.0, 100.0); /* log2(5*100/600) */ | |
| 277 | expected_mi[2] = cpu_mi(20.0, 40.0, 50.0, 100.0); /* log2(20*100/2000) */ | |
| 278 | ||
| 279 | printf(" Expected MI: [%.4f, %.4f, %.4f]\n", | |
| 280 | expected_mi[0], expected_mi[1], expected_mi[2]); | |
| 281 | ||
| 282 | /* Run MI kernel */ | |
| 283 | cl_uint np = 3; | |
| 284 | clSetKernelArg(k_mi_all, 0, sizeof(cl_mem), &pair_count); | |
| 285 | clSetKernelArg(k_mi_all, 1, sizeof(cl_mem), &pair_word_a); | |
| 286 | clSetKernelArg(k_mi_all, 2, sizeof(cl_mem), &pair_word_b); | |
| 287 | clSetKernelArg(k_mi_all, 3, sizeof(cl_mem), &pair_mi); | |
| 288 | clSetKernelArg(k_mi_all, 4, sizeof(cl_mem), &word_count); | |
| 289 | clSetKernelArg(k_mi_all, 5, sizeof(cl_double), &n_total); | |
| 290 | clSetKernelArg(k_mi_all, 6, sizeof(cl_uint), &np); | |
| 291 | ||
| 292 | size_t gs = ((np + local_size - 1) / local_size) * local_size; | |
| 293 | double t0 = now_ms(); | |
| 294 | err = clEnqueueNDRangeKernel(queue, k_mi_all, 1, NULL, | |
| 295 | &gs, &local_size, 0, NULL, NULL); | |
| 296 | CL_CHECK(err, "enqueue mi_all"); | |
| 297 | clFinish(queue); | |
| 298 | double t1 = now_ms(); | |
| 299 | ||
| 300 | /* Read back MI values */ | |
| 301 | double gpu_mi[3]; | |
| 302 | clEnqueueReadBuffer(queue, pair_mi, CL_TRUE, 0, | |
| 303 | sizeof(double) * 3, gpu_mi, 0, NULL, NULL); | |
| 304 | ||
| 305 | printf(" GPU MI: [%.4f, %.4f, %.4f]\n", | |
| 306 | gpu_mi[0], gpu_mi[1], gpu_mi[2]); | |
| 307 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 308 | ||
| 309 | int t1_pass = 1; | |
| 310 | for (int i = 0; i < 3; i++) { | |
| 311 | double diff = fabs(gpu_mi[i] - expected_mi[i]); | |
| 312 | if (diff > 0.001) { | |
| 313 | printf(" MISMATCH pair %d: gpu=%.6f expected=%.6f diff=%.6f\n", | |
| 314 | i, gpu_mi[i], expected_mi[i], diff); | |
| 315 | t1_pass = 0; | |
| 316 | } | |
| 317 | } | |
| 318 | printf(" %s\n\n", t1_pass ? "PASS" : "FAIL"); | |
| 319 | if (t1_pass) pass_count++; else fail_count++; | |
| 320 | ||
| 321 | /* ═══════════════════════════════════════════════════════════════ | |
| 322 | * TEST 2: Full pipeline — count sentences → compute MI | |
| 323 | * | |
| 324 | * Run count_sentence_pairs on test sentences, then immediately | |
| 325 | * run compute_mi_resident on the same GPU-resident data. | |
| 326 | * All data stays on GPU — zero transfers between stages. | |
| 327 | * | |
| 328 | * Sentence 1: [0, 1, 2, 3] (4 words, window=2) | |
| 329 | * Sentence 2: [4, 5, 6] (3 words) | |
| 330 | * | |
| 331 | * Expected: 8 pairs from counting, all get valid MI > 0. | |
| 332 | * ═══════════════════════════════════════════════════════════════ */ | |
| 333 | ||
| 334 | printf("--- Test 2: Full pipeline (count → MI) ---\n"); | |
| 335 | ||
| 336 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 337 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 338 | ||
| 339 | uint32_t sent_words[] = {0, 1, 2, 3, 4, 5, 6}; | |
| 340 | uint32_t sent_offsets[] = {0, 4}; | |
| 341 | uint32_t sent_lengths[] = {4, 3}; | |
| 342 | cl_uint num_sentences = 2; | |
| 343 | cl_uint tw = 7; | |
| 344 | cl_uint window_size = 2; | |
| 345 | ||
| 346 | cl_mem d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 347 | sizeof(uint32_t) * tw, sent_words, &err); | |
| 348 | cl_mem d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 349 | sizeof(uint32_t) * num_sentences, sent_offsets, &err); | |
| 350 | cl_mem d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 351 | sizeof(uint32_t) * num_sentences, sent_lengths, &err); | |
| 352 | ||
| 353 | /* Stage 1: Count pairs */ | |
| 354 | clSetKernelArg(k_count, 0, sizeof(cl_mem), &d_flat_words); | |
| 355 | clSetKernelArg(k_count, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 356 | clSetKernelArg(k_count, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 357 | clSetKernelArg(k_count, 3, sizeof(cl_uint), &num_sentences); | |
| 358 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &tw); | |
| 359 | clSetKernelArg(k_count, 5, sizeof(cl_uint), &window_size); | |
| 360 | clSetKernelArg(k_count, 6, sizeof(cl_mem), &pht_keys); | |
| 361 | clSetKernelArg(k_count, 7, sizeof(cl_mem), &pht_values); | |
| 362 | clSetKernelArg(k_count, 8, sizeof(cl_mem), &pair_word_a); | |
| 363 | clSetKernelArg(k_count, 9, sizeof(cl_mem), &pair_word_b); | |
| 364 | clSetKernelArg(k_count, 10, sizeof(cl_mem), &pair_count); | |
| 365 | clSetKernelArg(k_count, 11, sizeof(cl_mem), &pair_mi); | |
| 366 | clSetKernelArg(k_count, 12, sizeof(cl_mem), &pair_flags); | |
| 367 | clSetKernelArg(k_count, 13, sizeof(cl_mem), &pair_next_free); | |
| 368 | clSetKernelArg(k_count, 14, sizeof(cl_mem), &word_count); | |
| 369 | clSetKernelArg(k_count, 15, sizeof(cl_mem), &total_pair_count); | |
| 370 | ||
| 371 | t0 = now_ms(); | |
| 372 | gs = ((tw + local_size - 1) / local_size) * local_size; | |
| 373 | err = clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 374 | &gs, &local_size, 0, NULL, NULL); | |
| 375 | CL_CHECK(err, "enqueue count"); | |
| 376 | clFinish(queue); | |
| 377 | double t_count = now_ms() - t0; | |
| 378 | ||
| 379 | /* Read num_pairs and total for MI computation */ | |
| 380 | uint32_t h_num_pairs, h_total; | |
| 381 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 382 | sizeof(uint32_t), &h_num_pairs, 0, NULL, NULL); | |
| 383 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 384 | sizeof(uint32_t), &h_total, 0, NULL, NULL); | |
| 385 | ||
| 386 | printf(" Stage 1 (count): %u pairs, %u events in %.2f ms\n", | |
| 387 | h_num_pairs, h_total, t_count); | |
| 388 | ||
| 389 | /* Stage 2: Compute MI — data stays on GPU! */ | |
| 390 | double n = (double)h_total; | |
| 391 | clSetKernelArg(k_mi_all, 0, sizeof(cl_mem), &pair_count); | |
| 392 | clSetKernelArg(k_mi_all, 1, sizeof(cl_mem), &pair_word_a); | |
| 393 | clSetKernelArg(k_mi_all, 2, sizeof(cl_mem), &pair_word_b); | |
| 394 | clSetKernelArg(k_mi_all, 3, sizeof(cl_mem), &pair_mi); | |
| 395 | clSetKernelArg(k_mi_all, 4, sizeof(cl_mem), &word_count); | |
| 396 | clSetKernelArg(k_mi_all, 5, sizeof(cl_double), &n); | |
| 397 | clSetKernelArg(k_mi_all, 6, sizeof(cl_uint), &h_num_pairs); | |
| 398 | ||
| 399 | t0 = now_ms(); | |
| 400 | gs = ((h_num_pairs + local_size - 1) / local_size) * local_size; | |
| 401 | err = clEnqueueNDRangeKernel(queue, k_mi_all, 1, NULL, | |
| 402 | &gs, &local_size, 0, NULL, NULL); | |
| 403 | CL_CHECK(err, "enqueue mi_all pipeline"); | |
| 404 | clFinish(queue); | |
| 405 | double t_mi = now_ms() - t0; | |
| 406 | ||
| 407 | printf(" Stage 2 (MI): %u pairs in %.2f ms\n", h_num_pairs, t_mi); | |
| 408 | printf(" Total pipeline: %.2f ms (zero transfers between stages)\n", | |
| 409 | t_count + t_mi); | |
| 410 | ||
| 411 | /* Read back and verify with read_pairs_with_mi */ | |
| 412 | cl_mem d_out_wa = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 413 | sizeof(uint32_t) * h_num_pairs, NULL, &err); | |
| 414 | cl_mem d_out_wb = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 415 | sizeof(uint32_t) * h_num_pairs, NULL, &err); | |
| 416 | cl_mem d_out_cnt = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 417 | sizeof(double) * h_num_pairs, NULL, &err); | |
| 418 | cl_mem d_out_mi = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 419 | sizeof(double) * h_num_pairs, NULL, &err); | |
| 420 | cl_mem d_out_lm = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 421 | sizeof(double) * h_num_pairs, NULL, &err); | |
| 422 | cl_mem d_out_rm = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 423 | sizeof(double) * h_num_pairs, NULL, &err); | |
| 424 | ||
| 425 | clSetKernelArg(k_read_mi, 0, sizeof(cl_mem), &pair_word_a); | |
| 426 | clSetKernelArg(k_read_mi, 1, sizeof(cl_mem), &pair_word_b); | |
| 427 | clSetKernelArg(k_read_mi, 2, sizeof(cl_mem), &pair_count); | |
| 428 | clSetKernelArg(k_read_mi, 3, sizeof(cl_mem), &pair_mi); | |
| 429 | clSetKernelArg(k_read_mi, 4, sizeof(cl_mem), &pair_flags); | |
| 430 | clSetKernelArg(k_read_mi, 5, sizeof(cl_mem), &word_count); | |
| 431 | clSetKernelArg(k_read_mi, 6, sizeof(cl_mem), &d_out_wa); | |
| 432 | clSetKernelArg(k_read_mi, 7, sizeof(cl_mem), &d_out_wb); | |
| 433 | clSetKernelArg(k_read_mi, 8, sizeof(cl_mem), &d_out_cnt); | |
| 434 | clSetKernelArg(k_read_mi, 9, sizeof(cl_mem), &d_out_mi); | |
| 435 | clSetKernelArg(k_read_mi, 10, sizeof(cl_mem), &d_out_lm); | |
| 436 | clSetKernelArg(k_read_mi, 11, sizeof(cl_mem), &d_out_rm); | |
| 437 | clSetKernelArg(k_read_mi, 12, sizeof(cl_uint), &h_num_pairs); | |
| 438 | ||
| 439 | gs = ((h_num_pairs + local_size - 1) / local_size) * local_size; | |
| 440 | clEnqueueNDRangeKernel(queue, k_read_mi, 1, NULL, | |
| 441 | &gs, &local_size, 0, NULL, NULL); | |
| 442 | clFinish(queue); | |
| 443 | ||
| 444 | uint32_t* r_wa = malloc(sizeof(uint32_t) * h_num_pairs); | |
| 445 | uint32_t* r_wb = malloc(sizeof(uint32_t) * h_num_pairs); | |
| 446 | double* r_cnt = malloc(sizeof(double) * h_num_pairs); | |
| 447 | double* r_mi = malloc(sizeof(double) * h_num_pairs); | |
| 448 | double* r_lm = malloc(sizeof(double) * h_num_pairs); | |
| 449 | double* r_rm = malloc(sizeof(double) * h_num_pairs); | |
| 450 | ||
| 451 | clEnqueueReadBuffer(queue, d_out_wa, CL_TRUE, 0, sizeof(uint32_t)*h_num_pairs, r_wa, 0, NULL, NULL); | |
| 452 | clEnqueueReadBuffer(queue, d_out_wb, CL_TRUE, 0, sizeof(uint32_t)*h_num_pairs, r_wb, 0, NULL, NULL); | |
| 453 | clEnqueueReadBuffer(queue, d_out_cnt, CL_TRUE, 0, sizeof(double)*h_num_pairs, r_cnt, 0, NULL, NULL); | |
| 454 | clEnqueueReadBuffer(queue, d_out_mi, CL_TRUE, 0, sizeof(double)*h_num_pairs, r_mi, 0, NULL, NULL); | |
| 455 | clEnqueueReadBuffer(queue, d_out_lm, CL_TRUE, 0, sizeof(double)*h_num_pairs, r_lm, 0, NULL, NULL); | |
| 456 | clEnqueueReadBuffer(queue, d_out_rm, CL_TRUE, 0, sizeof(double)*h_num_pairs, r_rm, 0, NULL, NULL); | |
| 457 | ||
| 458 | int t2_pass = 1; | |
| 459 | int all_mi_valid = 1; | |
| 460 | printf(" Pair details:\n"); | |
| 461 | for (uint32_t i = 0; i < h_num_pairs; i++) { | |
| 462 | double exp = cpu_mi(r_cnt[i], r_lm[i], r_rm[i], n); | |
| 463 | double diff = fabs(r_mi[i] - exp); | |
| 464 | printf(" [%u] (%u,%u) cnt=%.0f lm=%.0f rm=%.0f MI=%.4f (exp=%.4f) %s\n", | |
| 465 | i, r_wa[i], r_wb[i], r_cnt[i], r_lm[i], r_rm[i], | |
| 466 | r_mi[i], exp, (diff < 0.001) ? "OK" : "MISMATCH"); | |
| 467 | if (diff > 0.001) { t2_pass = 0; all_mi_valid = 0; } | |
| 468 | } | |
| 469 | printf(" All MI values match CPU: %s\n", all_mi_valid ? "YES" : "NO"); | |
| 470 | t2_pass = t2_pass && (h_num_pairs == 8); | |
| 471 | printf(" %s\n\n", t2_pass ? "PASS" : "FAIL"); | |
| 472 | if (t2_pass) pass_count++; else fail_count++; | |
| 473 | ||
| 474 | free(r_wa); free(r_wb); free(r_cnt); free(r_mi); free(r_lm); free(r_rm); | |
| 475 | clReleaseMemObject(d_out_wa); clReleaseMemObject(d_out_wb); | |
| 476 | clReleaseMemObject(d_out_cnt); clReleaseMemObject(d_out_mi); | |
| 477 | clReleaseMemObject(d_out_lm); clReleaseMemObject(d_out_rm); | |
| 478 | ||
| 479 | /* ═══════════════════════════════════════════════════════════════ | |
| 480 | * TEST 3: Dirty-only MI recompute | |
| 481 | * | |
| 482 | * After counting, all pairs have flags=1 (dirty). | |
| 483 | * Run compute_mi_dirty — should compute MI and clear flags. | |
| 484 | * Then add more sentences, creating new dirty pairs. | |
| 485 | * Run compute_mi_dirty again — should only recompute dirty ones. | |
| 486 | * ═══════════════════════════════════════════════════════════════ */ | |
| 487 | ||
| 488 | printf("--- Test 3: Dirty-only MI recompute ---\n"); | |
| 489 | ||
| 490 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 491 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 492 | ||
| 493 | /* Count first batch: [0, 1, 2] window=2 → pairs (0,1),(0,2),(1,2) */ | |
| 494 | uint32_t batch1_words[] = {0, 1, 2}; | |
| 495 | uint32_t batch1_offset = 0; | |
| 496 | uint32_t batch1_length = 3; | |
| 497 | cl_uint b1_ns = 1, b1_tw = 3, b1_ws = 2; | |
| 498 | ||
| 499 | clReleaseMemObject(d_flat_words); | |
| 500 | clReleaseMemObject(d_sent_offsets); | |
| 501 | clReleaseMemObject(d_sent_lengths); | |
| 502 | ||
| 503 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 504 | sizeof(uint32_t) * 3, batch1_words, &err); | |
| 505 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 506 | sizeof(uint32_t), &batch1_offset, &err); | |
| 507 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 508 | sizeof(uint32_t), &batch1_length, &err); | |
| 509 | ||
| 510 | clSetKernelArg(k_count, 0, sizeof(cl_mem), &d_flat_words); | |
| 511 | clSetKernelArg(k_count, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 512 | clSetKernelArg(k_count, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 513 | clSetKernelArg(k_count, 3, sizeof(cl_uint), &b1_ns); | |
| 514 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &b1_tw); | |
| 515 | clSetKernelArg(k_count, 5, sizeof(cl_uint), &b1_ws); | |
| 516 | ||
| 517 | clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 518 | &local_size, &local_size, 0, NULL, NULL); | |
| 519 | clFinish(queue); | |
| 520 | ||
| 521 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 522 | sizeof(uint32_t), &h_num_pairs, 0, NULL, NULL); | |
| 523 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 524 | sizeof(uint32_t), &h_total, 0, NULL, NULL); | |
| 525 | ||
| 526 | printf(" Batch 1: %u pairs, %u events\n", h_num_pairs, h_total); | |
| 527 | ||
| 528 | /* Run dirty MI — should process all 3 pairs and clear flags */ | |
| 529 | n = (double)h_total; | |
| 530 | clSetKernelArg(k_mi_dirty, 0, sizeof(cl_mem), &pair_count); | |
| 531 | clSetKernelArg(k_mi_dirty, 1, sizeof(cl_mem), &pair_word_a); | |
| 532 | clSetKernelArg(k_mi_dirty, 2, sizeof(cl_mem), &pair_word_b); | |
| 533 | clSetKernelArg(k_mi_dirty, 3, sizeof(cl_mem), &pair_mi); | |
| 534 | clSetKernelArg(k_mi_dirty, 4, sizeof(cl_mem), &pair_flags); | |
| 535 | clSetKernelArg(k_mi_dirty, 5, sizeof(cl_mem), &word_count); | |
| 536 | clSetKernelArg(k_mi_dirty, 6, sizeof(cl_double), &n); | |
| 537 | clSetKernelArg(k_mi_dirty, 7, sizeof(cl_uint), &h_num_pairs); | |
| 538 | ||
| 539 | gs = ((h_num_pairs + local_size - 1) / local_size) * local_size; | |
| 540 | clEnqueueNDRangeKernel(queue, k_mi_dirty, 1, NULL, | |
| 541 | &gs, &local_size, 0, NULL, NULL); | |
| 542 | clFinish(queue); | |
| 543 | ||
| 544 | /* Verify flags are cleared */ | |
| 545 | uint32_t flags_after[3]; | |
| 546 | clEnqueueReadBuffer(queue, pair_flags, CL_TRUE, 0, | |
| 547 | sizeof(uint32_t) * 3, flags_after, 0, NULL, NULL); | |
| 548 | ||
| 549 | int all_clear = (flags_after[0] == 0 && flags_after[1] == 0 && flags_after[2] == 0); | |
| 550 | printf(" After dirty MI: flags=[%u,%u,%u] (all 0?) %s\n", | |
| 551 | flags_after[0], flags_after[1], flags_after[2], | |
| 552 | all_clear ? "YES" : "NO"); | |
| 553 | ||
| 554 | /* Read MI values after first batch */ | |
| 555 | double mi_batch1[3]; | |
| 556 | clEnqueueReadBuffer(queue, pair_mi, CL_TRUE, 0, | |
| 557 | sizeof(double) * 3, mi_batch1, 0, NULL, NULL); | |
| 558 | printf(" MI after batch 1: [%.4f, %.4f, %.4f]\n", | |
| 559 | mi_batch1[0], mi_batch1[1], mi_batch1[2]); | |
| 560 | ||
| 561 | /* Count second batch: [1, 2, 3] window=2 → new pair (1,3),(2,3) + existing (1,2) */ | |
| 562 | uint32_t batch2_words[] = {1, 2, 3}; | |
| 563 | uint32_t batch2_offset = 0; | |
| 564 | uint32_t batch2_length = 3; | |
| 565 | ||
| 566 | clReleaseMemObject(d_flat_words); | |
| 567 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 568 | sizeof(uint32_t) * 3, batch2_words, &err); | |
| 569 | ||
| 570 | clSetKernelArg(k_count, 0, sizeof(cl_mem), &d_flat_words); | |
| 571 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &b1_tw); | |
| 572 | ||
| 573 | clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 574 | &local_size, &local_size, 0, NULL, NULL); | |
| 575 | clFinish(queue); | |
| 576 | ||
| 577 | uint32_t h_num_pairs2, h_total2; | |
| 578 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 579 | sizeof(uint32_t), &h_num_pairs2, 0, NULL, NULL); | |
| 580 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 581 | sizeof(uint32_t), &h_total2, 0, NULL, NULL); | |
| 582 | ||
| 583 | printf(" Batch 2: %u total pairs, %u total events\n", h_num_pairs2, h_total2); | |
| 584 | ||
| 585 | /* Run dirty MI again — should only recompute dirty pairs */ | |
| 586 | n = (double)h_total2; | |
| 587 | clSetKernelArg(k_mi_dirty, 6, sizeof(cl_double), &n); | |
| 588 | clSetKernelArg(k_mi_dirty, 7, sizeof(cl_uint), &h_num_pairs2); | |
| 589 | ||
| 590 | gs = ((h_num_pairs2 + local_size - 1) / local_size) * local_size; | |
| 591 | clEnqueueNDRangeKernel(queue, k_mi_dirty, 1, NULL, | |
| 592 | &gs, &local_size, 0, NULL, NULL); | |
| 593 | clFinish(queue); | |
| 594 | ||
| 595 | /* Read all MI values */ | |
| 596 | double mi_batch2[5]; | |
| 597 | uint32_t flags_after2[5]; | |
| 598 | clEnqueueReadBuffer(queue, pair_mi, CL_TRUE, 0, | |
| 599 | sizeof(double) * h_num_pairs2, mi_batch2, 0, NULL, NULL); | |
| 600 | clEnqueueReadBuffer(queue, pair_flags, CL_TRUE, 0, | |
| 601 | sizeof(uint32_t) * h_num_pairs2, flags_after2, 0, NULL, NULL); | |
| 602 | ||
| 603 | printf(" MI after batch 2:\n"); | |
| 604 | int t3_pass = all_clear; | |
| 605 | for (uint32_t i = 0; i < h_num_pairs2; i++) { | |
| 606 | printf(" pair[%u] MI=%.4f flags=%u\n", i, mi_batch2[i], flags_after2[i]); | |
| 607 | if (flags_after2[i] != 0) t3_pass = 0; | |
| 608 | /* MI should be non-zero for all counted pairs */ | |
| 609 | if (fabs(mi_batch2[i]) < 0.001) t3_pass = 0; | |
| 610 | } | |
| 611 | printf(" All flags cleared: %s\n", | |
| 612 | (t3_pass) ? "YES" : "NO"); | |
| 613 | printf(" %s\n\n", t3_pass ? "PASS" : "FAIL"); | |
| 614 | if (t3_pass) pass_count++; else fail_count++; | |
| 615 | ||
| 616 | /* ═══════════════════════════════════════════════════════════════ | |
| 617 | * TEST 4: MI statistics | |
| 618 | * ═══════════════════════════════════════════════════════════════ */ | |
| 619 | ||
| 620 | printf("--- Test 4: MI statistics ---\n"); | |
| 621 | ||
| 622 | /* Use the data from Test 3 (5 pairs, all with MI > 0) */ | |
| 623 | cl_mem d_cnt_nz = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 624 | sizeof(uint32_t), &zero, &err); | |
| 625 | cl_mem d_cnt_pos = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 626 | sizeof(uint32_t), &zero, &err); | |
| 627 | cl_mem d_cnt_at = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 628 | sizeof(uint32_t), &zero, &err); | |
| 629 | ||
| 630 | cl_double threshold = 1.0; | |
| 631 | ||
| 632 | clSetKernelArg(k_mi_stats, 0, sizeof(cl_mem), &pair_mi); | |
| 633 | clSetKernelArg(k_mi_stats, 1, sizeof(cl_mem), &pair_count); | |
| 634 | clSetKernelArg(k_mi_stats, 2, sizeof(cl_uint), &h_num_pairs2); | |
| 635 | clSetKernelArg(k_mi_stats, 3, sizeof(cl_double), &threshold); | |
| 636 | clSetKernelArg(k_mi_stats, 4, sizeof(cl_mem), &d_cnt_nz); | |
| 637 | clSetKernelArg(k_mi_stats, 5, sizeof(cl_mem), &d_cnt_pos); | |
| 638 | clSetKernelArg(k_mi_stats, 6, sizeof(cl_mem), &d_cnt_at); | |
| 639 | ||
| 640 | gs = ((h_num_pairs2 + local_size - 1) / local_size) * local_size; | |
| 641 | clEnqueueNDRangeKernel(queue, k_mi_stats, 1, NULL, | |
| 642 | &gs, &local_size, 0, NULL, NULL); | |
| 643 | clFinish(queue); | |
| 644 | ||
| 645 | uint32_t cnt_nz, cnt_pos, cnt_at; | |
| 646 | clEnqueueReadBuffer(queue, d_cnt_nz, CL_TRUE, 0, sizeof(uint32_t), &cnt_nz, 0, NULL, NULL); | |
| 647 | clEnqueueReadBuffer(queue, d_cnt_pos, CL_TRUE, 0, sizeof(uint32_t), &cnt_pos, 0, NULL, NULL); | |
| 648 | clEnqueueReadBuffer(queue, d_cnt_at, CL_TRUE, 0, sizeof(uint32_t), &cnt_at, 0, NULL, NULL); | |
| 649 | ||
| 650 | printf(" Pairs with count > 0: %u (expected %u)\n", cnt_nz, h_num_pairs2); | |
| 651 | printf(" Pairs with MI > 0: %u\n", cnt_pos); | |
| 652 | printf(" Pairs with MI > %.1f: %u\n", threshold, cnt_at); | |
| 653 | ||
| 654 | /* Count expected on CPU */ | |
| 655 | int exp_pos = 0, exp_at = 0; | |
| 656 | for (uint32_t i = 0; i < h_num_pairs2; i++) { | |
| 657 | if (mi_batch2[i] > 0.0) exp_pos++; | |
| 658 | if (mi_batch2[i] > threshold) exp_at++; | |
| 659 | } | |
| 660 | ||
| 661 | int t4_pass = (cnt_nz == h_num_pairs2) && (cnt_pos == (uint32_t)exp_pos) | |
| 662 | && (cnt_at == (uint32_t)exp_at); | |
| 663 | printf(" Expected: pos=%d above=%.1f=%d\n", exp_pos, threshold, exp_at); | |
| 664 | printf(" %s\n\n", t4_pass ? "PASS" : "FAIL"); | |
| 665 | if (t4_pass) pass_count++; else fail_count++; | |
| 666 | ||
| 667 | clReleaseMemObject(d_cnt_nz); | |
| 668 | clReleaseMemObject(d_cnt_pos); | |
| 669 | clReleaseMemObject(d_cnt_at); | |
| 670 | ||
| 671 | /* ═══════════════════════════════════════════════════════════════ | |
| 672 | * TEST 5: MI filter (compact high-MI pairs) | |
| 673 | * ═══════════════════════════════════════════════════════════════ */ | |
| 674 | ||
| 675 | printf("--- Test 5: MI filter ---\n"); | |
| 676 | ||
| 677 | uint32_t max_output = 100; | |
| 678 | cl_mem d_filt_idx = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 679 | sizeof(uint32_t) * max_output, NULL, &err); | |
| 680 | cl_mem d_filt_mi = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 681 | sizeof(double) * max_output, NULL, &err); | |
| 682 | cl_mem d_filt_cnt = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 683 | sizeof(uint32_t), &zero, &err); | |
| 684 | ||
| 685 | cl_double mi_thresh = 1.0; | |
| 686 | ||
| 687 | clSetKernelArg(k_mi_filter, 0, sizeof(cl_mem), &pair_mi); | |
| 688 | clSetKernelArg(k_mi_filter, 1, sizeof(cl_mem), &pair_count); | |
| 689 | clSetKernelArg(k_mi_filter, 2, sizeof(cl_uint), &h_num_pairs2); | |
| 690 | clSetKernelArg(k_mi_filter, 3, sizeof(cl_double), &mi_thresh); | |
| 691 | clSetKernelArg(k_mi_filter, 4, sizeof(cl_mem), &d_filt_idx); | |
| 692 | clSetKernelArg(k_mi_filter, 5, sizeof(cl_mem), &d_filt_mi); | |
| 693 | clSetKernelArg(k_mi_filter, 6, sizeof(cl_mem), &d_filt_cnt); | |
| 694 | clSetKernelArg(k_mi_filter, 7, sizeof(cl_uint), &max_output); | |
| 695 | ||
| 696 | gs = ((h_num_pairs2 + local_size - 1) / local_size) * local_size; | |
| 697 | clEnqueueNDRangeKernel(queue, k_mi_filter, 1, NULL, | |
| 698 | &gs, &local_size, 0, NULL, NULL); | |
| 699 | clFinish(queue); | |
| 700 | ||
| 701 | uint32_t filt_count; | |
| 702 | clEnqueueReadBuffer(queue, d_filt_cnt, CL_TRUE, 0, | |
| 703 | sizeof(uint32_t), &filt_count, 0, NULL, NULL); | |
| 704 | ||
| 705 | printf(" Pairs with MI > %.1f: %u (expected %u)\n", | |
| 706 | mi_thresh, filt_count, cnt_at); | |
| 707 | ||
| 708 | if (filt_count > 0 && filt_count <= max_output) { | |
| 709 | uint32_t* f_idx = malloc(sizeof(uint32_t) * filt_count); | |
| 710 | double* f_mi = malloc(sizeof(double) * filt_count); | |
| 711 | clEnqueueReadBuffer(queue, d_filt_idx, CL_TRUE, 0, | |
| 712 | sizeof(uint32_t) * filt_count, f_idx, 0, NULL, NULL); | |
| 713 | clEnqueueReadBuffer(queue, d_filt_mi, CL_TRUE, 0, | |
| 714 | sizeof(double) * filt_count, f_mi, 0, NULL, NULL); | |
| 715 | ||
| 716 | printf(" Filtered pairs:\n"); | |
| 717 | for (uint32_t i = 0; i < filt_count; i++) { | |
| 718 | printf(" pair[%u] MI=%.4f\n", f_idx[i], f_mi[i]); | |
| 719 | } | |
| 720 | free(f_idx); free(f_mi); | |
| 721 | } | |
| 722 | ||
| 723 | int t5_pass = (filt_count == cnt_at); | |
| 724 | printf(" %s\n\n", t5_pass ? "PASS" : "FAIL"); | |
| 725 | if (t5_pass) pass_count++; else fail_count++; | |
| 726 | ||
| 727 | clReleaseMemObject(d_filt_idx); | |
| 728 | clReleaseMemObject(d_filt_mi); | |
| 729 | clReleaseMemObject(d_filt_cnt); | |
| 730 | ||
| 731 | /* ═══════════════════════════════════════════════════════════════ | |
| 732 | * TEST 6: Benchmark — 1000 sentences → MI | |
| 733 | * | |
| 734 | * Full pipeline: count 1000 sentences then compute MI. | |
| 735 | * Measures the end-to-end time with zero CPU↔GPU transfers | |
| 736 | * between stages. | |
| 737 | * ═══════════════════════════════════════════════════════════════ */ | |
| 738 | ||
| 739 | printf("--- Test 6: Benchmark (1000 sentences → MI) ---\n"); | |
| 740 | ||
| 741 | reset_pools(queue, pht_keys, pht_values, pair_count, pair_mi, | |
| 742 | pair_flags, word_count, pair_next_free, total_pair_count); | |
| 743 | ||
| 744 | /* Generate 1000 sentences */ | |
| 745 | int bench_ns = 1000; | |
| 746 | uint32_t* bench_offsets = malloc(sizeof(uint32_t) * bench_ns); | |
| 747 | uint32_t* bench_lengths = malloc(sizeof(uint32_t) * bench_ns); | |
| 748 | ||
| 749 | uint64_t rng = 0xCAFEBABEDEADBEEFULL; | |
| 750 | uint32_t bench_tw = 0; | |
| 751 | for (int s = 0; s < bench_ns; s++) { | |
| 752 | bench_offsets[s] = bench_tw; | |
| 753 | rng += 0x9E3779B97F4A7C15ULL; | |
| 754 | bench_lengths[s] = 5 + (uint32_t)((rng >> 32) % 16); | |
| 755 | bench_tw += bench_lengths[s]; | |
| 756 | } | |
| 757 | ||
| 758 | uint32_t* bench_words = malloc(sizeof(uint32_t) * bench_tw); | |
| 759 | rng = 0xFEEDFACE12345678ULL; | |
| 760 | for (uint32_t i = 0; i < bench_tw; i++) { | |
| 761 | rng += 0x9E3779B97F4A7C15ULL; | |
| 762 | double u = (double)(rng >> 32) / (double)0xFFFFFFFFU; | |
| 763 | bench_words[i] = (uint32_t)(u * u * 499.0); | |
| 764 | } | |
| 765 | ||
| 766 | printf(" Sentences: %d, Total words: %u\n", bench_ns, bench_tw); | |
| 767 | ||
| 768 | clReleaseMemObject(d_flat_words); | |
| 769 | clReleaseMemObject(d_sent_offsets); | |
| 770 | clReleaseMemObject(d_sent_lengths); | |
| 771 | ||
| 772 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 773 | sizeof(uint32_t) * bench_tw, bench_words, &err); | |
| 774 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 775 | sizeof(uint32_t) * bench_ns, bench_offsets, &err); | |
| 776 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 777 | sizeof(uint32_t) * bench_ns, bench_lengths, &err); | |
| 778 | ||
| 779 | cl_uint bns = bench_ns; | |
| 780 | cl_uint bench_window = 6; | |
| 781 | ||
| 782 | clSetKernelArg(k_count, 0, sizeof(cl_mem), &d_flat_words); | |
| 783 | clSetKernelArg(k_count, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 784 | clSetKernelArg(k_count, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 785 | clSetKernelArg(k_count, 3, sizeof(cl_uint), &bns); | |
| 786 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &bench_tw); | |
| 787 | clSetKernelArg(k_count, 5, sizeof(cl_uint), &bench_window); | |
| 788 | clSetKernelArg(k_count, 6, sizeof(cl_mem), &pht_keys); | |
| 789 | clSetKernelArg(k_count, 7, sizeof(cl_mem), &pht_values); | |
| 790 | clSetKernelArg(k_count, 8, sizeof(cl_mem), &pair_word_a); | |
| 791 | clSetKernelArg(k_count, 9, sizeof(cl_mem), &pair_word_b); | |
| 792 | clSetKernelArg(k_count, 10, sizeof(cl_mem), &pair_count); | |
| 793 | clSetKernelArg(k_count, 11, sizeof(cl_mem), &pair_mi); | |
| 794 | clSetKernelArg(k_count, 12, sizeof(cl_mem), &pair_flags); | |
| 795 | clSetKernelArg(k_count, 13, sizeof(cl_mem), &pair_next_free); | |
| 796 | clSetKernelArg(k_count, 14, sizeof(cl_mem), &word_count); | |
| 797 | clSetKernelArg(k_count, 15, sizeof(cl_mem), &total_pair_count); | |
| 798 | ||
| 799 | /* Stage 1: Count */ | |
| 800 | t0 = now_ms(); | |
| 801 | gs = ((bench_tw + local_size - 1) / local_size) * local_size; | |
| 802 | clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 803 | &gs, &local_size, 0, NULL, NULL); | |
| 804 | clFinish(queue); | |
| 805 | t1 = now_ms(); | |
| 806 | double count_ms = t1 - t0; | |
| 807 | ||
| 808 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 809 | sizeof(uint32_t), &h_num_pairs, 0, NULL, NULL); | |
| 810 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 811 | sizeof(uint32_t), &h_total, 0, NULL, NULL); | |
| 812 | ||
| 813 | printf(" Count: %u pairs, %u events in %.2f ms\n", | |
| 814 | h_num_pairs, h_total, count_ms); | |
| 815 | ||
| 816 | /* Stage 2: MI on all pairs */ | |
| 817 | n = (double)h_total; | |
| 818 | clSetKernelArg(k_mi_all, 5, sizeof(cl_double), &n); | |
| 819 | clSetKernelArg(k_mi_all, 6, sizeof(cl_uint), &h_num_pairs); | |
| 820 | ||
| 821 | t0 = now_ms(); | |
| 822 | gs = ((h_num_pairs + local_size - 1) / local_size) * local_size; | |
| 823 | clEnqueueNDRangeKernel(queue, k_mi_all, 1, NULL, | |
| 824 | &gs, &local_size, 0, NULL, NULL); | |
| 825 | clFinish(queue); | |
| 826 | t1 = now_ms(); | |
| 827 | double mi_ms = t1 - t0; | |
| 828 | ||
| 829 | printf(" MI: %u pairs in %.2f ms (%.1f M pairs/sec)\n", | |
| 830 | h_num_pairs, mi_ms, | |
| 831 | h_num_pairs / (mi_ms / 1000.0) / 1e6); | |
| 832 | ||
| 833 | /* Stage 3: MI stats */ | |
| 834 | cl_mem d_s_nz = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 835 | sizeof(uint32_t), &zero, &err); | |
| 836 | cl_mem d_s_pos = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 837 | sizeof(uint32_t), &zero, &err); | |
| 838 | cl_mem d_s_at = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 839 | sizeof(uint32_t), &zero, &err); | |
| 840 | ||
| 841 | threshold = 1.0; | |
| 842 | clSetKernelArg(k_mi_stats, 0, sizeof(cl_mem), &pair_mi); | |
| 843 | clSetKernelArg(k_mi_stats, 1, sizeof(cl_mem), &pair_count); | |
| 844 | clSetKernelArg(k_mi_stats, 2, sizeof(cl_uint), &h_num_pairs); | |
| 845 | clSetKernelArg(k_mi_stats, 3, sizeof(cl_double), &threshold); | |
| 846 | clSetKernelArg(k_mi_stats, 4, sizeof(cl_mem), &d_s_nz); | |
| 847 | clSetKernelArg(k_mi_stats, 5, sizeof(cl_mem), &d_s_pos); | |
| 848 | clSetKernelArg(k_mi_stats, 6, sizeof(cl_mem), &d_s_at); | |
| 849 | ||
| 850 | t0 = now_ms(); | |
| 851 | clEnqueueNDRangeKernel(queue, k_mi_stats, 1, NULL, | |
| 852 | &gs, &local_size, 0, NULL, NULL); | |
| 853 | clFinish(queue); | |
| 854 | double stats_ms = now_ms() - t0; | |
| 855 | ||
| 856 | clEnqueueReadBuffer(queue, d_s_nz, CL_TRUE, 0, sizeof(uint32_t), &cnt_nz, 0, NULL, NULL); | |
| 857 | clEnqueueReadBuffer(queue, d_s_pos, CL_TRUE, 0, sizeof(uint32_t), &cnt_pos, 0, NULL, NULL); | |
| 858 | clEnqueueReadBuffer(queue, d_s_at, CL_TRUE, 0, sizeof(uint32_t), &cnt_at, 0, NULL, NULL); | |
| 859 | ||
| 860 | printf(" Stats: %u nonzero, %u positive MI, %u MI>%.1f in %.2f ms\n", | |
| 861 | cnt_nz, cnt_pos, cnt_at, threshold, stats_ms); | |
| 862 | ||
| 863 | /* Stage 4: Dirty MI (incremental — count another batch, recompute dirty only) */ | |
| 864 | ||
| 865 | /* Reset only the counting state, keep existing MI values */ | |
| 866 | /* First record current pair count */ | |
| 867 | uint32_t pairs_before = h_num_pairs; | |
| 868 | ||
| 869 | /* Count same sentences again (adds to counts, marks dirty) */ | |
| 870 | clSetKernelArg(k_count, 3, sizeof(cl_uint), &bns); | |
| 871 | clSetKernelArg(k_count, 4, sizeof(cl_uint), &bench_tw); | |
| 872 | ||
| 873 | t0 = now_ms(); | |
| 874 | gs = ((bench_tw + local_size - 1) / local_size) * local_size; | |
| 875 | clEnqueueNDRangeKernel(queue, k_count, 1, NULL, | |
| 876 | &gs, &local_size, 0, NULL, NULL); | |
| 877 | clFinish(queue); | |
| 878 | double count2_ms = now_ms() - t0; | |
| 879 | ||
| 880 | clEnqueueReadBuffer(queue, pair_next_free, CL_TRUE, 0, | |
| 881 | sizeof(uint32_t), &h_num_pairs, 0, NULL, NULL); | |
| 882 | clEnqueueReadBuffer(queue, total_pair_count, CL_TRUE, 0, | |
| 883 | sizeof(uint32_t), &h_total, 0, NULL, NULL); | |
| 884 | ||
| 885 | printf(" Count (batch 2): %u pairs, %u events in %.2f ms\n", | |
| 886 | h_num_pairs, h_total, count2_ms); | |
| 887 | printf(" New pairs: %u (existing reused: %u)\n", | |
| 888 | h_num_pairs - pairs_before, pairs_before); | |
| 889 | ||
| 890 | /* Dirty MI recompute */ | |
| 891 | n = (double)h_total; | |
| 892 | clSetKernelArg(k_mi_dirty, 6, sizeof(cl_double), &n); | |
| 893 | clSetKernelArg(k_mi_dirty, 7, sizeof(cl_uint), &h_num_pairs); | |
| 894 | ||
| 895 | t0 = now_ms(); | |
| 896 | gs = ((h_num_pairs + local_size - 1) / local_size) * local_size; | |
| 897 | clEnqueueNDRangeKernel(queue, k_mi_dirty, 1, NULL, | |
| 898 | &gs, &local_size, 0, NULL, NULL); | |
| 899 | clFinish(queue); | |
| 900 | double dirty_ms = now_ms() - t0; | |
| 901 | ||
| 902 | printf(" Dirty MI: %u pairs scanned in %.2f ms\n", h_num_pairs, dirty_ms); | |
| 903 | printf("\n === Pipeline Summary ===\n"); | |
| 904 | printf(" Count (1000 sent): %.2f ms\n", count_ms); | |
| 905 | printf(" MI (all %u pairs): %.2f ms\n", pairs_before, mi_ms); | |
| 906 | printf(" Stats: %.2f ms\n", stats_ms); | |
| 907 | printf(" Count (batch 2): %.2f ms\n", count2_ms); | |
| 908 | printf(" MI (dirty only): %.2f ms\n", dirty_ms); | |
| 909 | printf(" Total pipeline: %.2f ms\n", | |
| 910 | count_ms + mi_ms + stats_ms + count2_ms + dirty_ms); | |
| 911 | printf(" CPU↔GPU transfers: 0 (all data GPU-resident)\n"); | |
| 912 | ||
| 913 | int t6_pass = (h_num_pairs > 0) && (h_total > 0) && (cnt_nz > 0); | |
| 914 | printf(" %s\n\n", t6_pass ? "PASS" : "FAIL"); | |
| 915 | if (t6_pass) pass_count++; else fail_count++; | |
| 916 | ||
| 917 | clReleaseMemObject(d_s_nz); | |
| 918 | clReleaseMemObject(d_s_pos); | |
| 919 | clReleaseMemObject(d_s_at); | |
| 920 | ||
| 921 | /* ═══ Summary ═══ */ | |
| 922 | ||
| 923 | printf("=== Results: %d PASS, %d FAIL ===\n", pass_count, fail_count); | |
| 924 | ||
| 925 | /* ─── Cleanup ─── */ | |
| 926 | ||
| 927 | clReleaseMemObject(pht_keys); | |
| 928 | clReleaseMemObject(pht_values); | |
| 929 | clReleaseMemObject(pair_word_a); | |
| 930 | clReleaseMemObject(pair_word_b); | |
| 931 | clReleaseMemObject(pair_count); | |
| 932 | clReleaseMemObject(pair_mi); | |
| 933 | clReleaseMemObject(pair_flags); | |
| 934 | clReleaseMemObject(pair_next_free); | |
| 935 | clReleaseMemObject(word_count); | |
| 936 | clReleaseMemObject(total_pair_count); | |
| 937 | clReleaseMemObject(d_flat_words); | |
| 938 | clReleaseMemObject(d_sent_offsets); | |
| 939 | clReleaseMemObject(d_sent_lengths); | |
| 940 | ||
| 941 | clReleaseKernel(k_count); | |
| 942 | clReleaseKernel(k_mi_all); | |
| 943 | clReleaseKernel(k_mi_dirty); | |
| 944 | clReleaseKernel(k_mi_stats); | |
| 945 | clReleaseKernel(k_mi_filter); | |
| 946 | clReleaseKernel(k_read_mi); | |
| 947 | clReleaseProgram(program); | |
| 948 | clReleaseCommandQueue(queue); | |
| 949 | clReleaseContext(ctx); | |
| 950 | ||
| 951 | free(bench_offsets); | |
| 952 | free(bench_lengths); | |
| 953 | free(bench_words); | |
| 954 | free(combined); | |
| 955 | free(ht_src); | |
| 956 | free(as_src); | |
| 957 | free(ct_src); | |
| 958 | free(mi_src); | |
| 959 | ||
| 960 | return fail_count > 0 ? 1 : 0; | |
| 961 | } |
| … | … | @@ -1,883 +0,0 @@ |
| 1 | /* | |
| 2 | * test-sections.c -- Test GPU section extraction kernel | |
| 3 | * | |
| 4 | * Compile: gcc -O2 -o test-sections test-sections.c -lOpenCL -lm | |
| 5 | * Run: ./test-sections | |
| 6 | * | |
| 7 | * Tests: | |
| 8 | * 1. Simple MST: 3-word sentence with 2 edges → 3 sections | |
| 9 | * 2. Star parse: all edges from one root → verify disjuncts | |
| 10 | * 3. Multi-sentence batch (no cross-boundary sections) | |
| 11 | * 4. Duplicate disjuncts: same parse seen twice → counts accumulate | |
| 12 | * 5. Readback kernel verification | |
| 13 | * 6. Benchmark: 1000 sentences with random MST edges | |
| 14 | */ | |
| 15 | ||
| 16 | #include <stdio.h> | |
| 17 | #include <stdlib.h> | |
| 18 | #include <stdint.h> | |
| 19 | #include <string.h> | |
| 20 | #include <math.h> | |
| 21 | #include <time.h> | |
| 22 | #include <CL/cl.h> | |
| 23 | ||
| 24 | /* ─── Pool capacities ─── */ | |
| 25 | ||
| 26 | #define WORD_CAPACITY (128 * 1024) | |
| 27 | #define PAIR_CAPACITY (4 * 1024 * 1024) | |
| 28 | #define SECTION_CAPACITY (1024 * 1024) | |
| 29 | #define WORD_HT_CAPACITY (256 * 1024) | |
| 30 | #define PAIR_HT_CAPACITY (8 * 1024 * 1024) | |
| 31 | #define SECTION_HT_CAPACITY (2 * 1024 * 1024) | |
| 32 | ||
| 33 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 34 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 35 | ||
| 36 | /* ─── Error checking ─── */ | |
| 37 | ||
| 38 | #define CL_CHECK(err, msg) do { \ | |
| 39 | if ((err) != CL_SUCCESS) { \ | |
| 40 | fprintf(stderr, "OpenCL error %d at %s:%d: %s\n", \ | |
| 41 | (err), __FILE__, __LINE__, (msg)); \ | |
| 42 | exit(1); \ | |
| 43 | } \ | |
| 44 | } while(0) | |
| 45 | ||
| 46 | /* ─── Read file ─── */ | |
| 47 | ||
| 48 | char* read_file(const char* path, size_t* len) | |
| 49 | { | |
| 50 | FILE* f = fopen(path, "r"); | |
| 51 | if (!f) { fprintf(stderr, "Cannot open %s\n", path); exit(1); } | |
| 52 | fseek(f, 0, SEEK_END); | |
| 53 | *len = ftell(f); | |
| 54 | fseek(f, 0, SEEK_SET); | |
| 55 | char* buf = malloc(*len + 1); | |
| 56 | size_t n = fread(buf, 1, *len, f); | |
| 57 | buf[n] = '\0'; | |
| 58 | *len = n; | |
| 59 | fclose(f); | |
| 60 | return buf; | |
| 61 | } | |
| 62 | ||
| 63 | /* ─── Timing ─── */ | |
| 64 | ||
| 65 | double now_ms(void) | |
| 66 | { | |
| 67 | struct timespec ts; | |
| 68 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 69 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; | |
| 70 | } | |
| 71 | ||
| 72 | /* ─── CPU-side disjunct hash (must match GPU) ─── */ | |
| 73 | ||
| 74 | static uint64_t cpu_fnv1a_init(void) { return 0xcbf29ce484222325ULL; } | |
| 75 | ||
| 76 | static uint64_t cpu_fnv1a_mix(uint64_t hash, uint64_t val) | |
| 77 | { | |
| 78 | hash ^= val; | |
| 79 | hash *= 0x100000001b3ULL; | |
| 80 | return hash; | |
| 81 | } | |
| 82 | ||
| 83 | static uint64_t cpu_hash_disjunct(uint32_t* words, uint32_t* dirs, uint32_t count) | |
| 84 | { | |
| 85 | uint64_t h = cpu_fnv1a_init(); | |
| 86 | for (uint32_t i = 0; i < count; i++) { | |
| 87 | uint64_t encoded = ((uint64_t)words[i] << 1) | (uint64_t)dirs[i]; | |
| 88 | h = cpu_fnv1a_mix(h, encoded); | |
| 89 | } | |
| 90 | if (h == HT_EMPTY_KEY) h = 0; | |
| 91 | return h; | |
| 92 | } | |
| 93 | ||
| 94 | /* CPU-side section_key (must match GPU) */ | |
| 95 | static uint64_t cpu_section_key(uint32_t word_idx, uint64_t disjunct_hash) | |
| 96 | { | |
| 97 | uint64_t key = disjunct_hash ^ ((uint64_t)word_idx * 0x9E3779B97F4A7C15ULL); | |
| 98 | if (key == HT_EMPTY_KEY) key = 0; | |
| 99 | return key; | |
| 100 | } | |
| 101 | ||
| 102 | /* ─── Helper: reset section pool and hash table ─── */ | |
| 103 | ||
| 104 | void reset_section_pool(cl_command_queue queue, | |
| 105 | cl_mem sht_keys, cl_mem sht_values, | |
| 106 | cl_mem sec_count, | |
| 107 | cl_mem sec_next_free, cl_mem total_sections) | |
| 108 | { | |
| 109 | uint8_t pat_ff = 0xFF; | |
| 110 | uint8_t pat_00 = 0x00; | |
| 111 | uint32_t zero = 0; | |
| 112 | ||
| 113 | clEnqueueFillBuffer(queue, sht_keys, &pat_ff, 1, 0, | |
| 114 | sizeof(uint64_t) * SECTION_HT_CAPACITY, 0, NULL, NULL); | |
| 115 | clEnqueueFillBuffer(queue, sht_values, &pat_ff, 1, 0, | |
| 116 | sizeof(uint32_t) * SECTION_HT_CAPACITY, 0, NULL, NULL); | |
| 117 | clEnqueueFillBuffer(queue, sec_count, &pat_00, 1, 0, | |
| 118 | sizeof(double) * SECTION_CAPACITY, 0, NULL, NULL); | |
| 119 | clEnqueueWriteBuffer(queue, sec_next_free, CL_FALSE, 0, | |
| 120 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 121 | clEnqueueWriteBuffer(queue, total_sections, CL_FALSE, 0, | |
| 122 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 123 | clFinish(queue); | |
| 124 | } | |
| 125 | ||
| 126 | /* ─── Main ─── */ | |
| 127 | ||
| 128 | int main(int argc, char** argv) | |
| 129 | { | |
| 130 | cl_int err; | |
| 131 | int pass_count = 0, fail_count = 0; | |
| 132 | ||
| 133 | printf("=== GPU Section Extraction Test ===\n\n"); | |
| 134 | ||
| 135 | /* ─── OpenCL setup ─── */ | |
| 136 | ||
| 137 | cl_platform_id platform; | |
| 138 | err = clGetPlatformIDs(1, &platform, NULL); | |
| 139 | CL_CHECK(err, "platform"); | |
| 140 | ||
| 141 | cl_device_id device; | |
| 142 | err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL); | |
| 143 | CL_CHECK(err, "device"); | |
| 144 | ||
| 145 | char dev_name[256]; | |
| 146 | clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(dev_name), dev_name, NULL); | |
| 147 | printf("GPU: %s\n", dev_name); | |
| 148 | ||
| 149 | cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err); | |
| 150 | CL_CHECK(err, "context"); | |
| 151 | ||
| 152 | cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err); | |
| 153 | CL_CHECK(err, "queue"); | |
| 154 | ||
| 155 | /* ─── Load and concatenate kernel sources ─── */ | |
| 156 | ||
| 157 | size_t ht_len, as_len, sc_len; | |
| 158 | char* ht_src = read_file("opencog/gpu/gpu-hashtable.cl", &ht_len); | |
| 159 | char* as_src = read_file("opencog/gpu/gpu-atomspace.cl", &as_len); | |
| 160 | char* sc_src = read_file("opencog/gpu/gpu-sections.cl", &sc_len); | |
| 161 | ||
| 162 | size_t total_len = ht_len + 1 + as_len + 1 + sc_len; | |
| 163 | char* combined = malloc(total_len + 1); | |
| 164 | memcpy(combined, ht_src, ht_len); | |
| 165 | combined[ht_len] = '\n'; | |
| 166 | memcpy(combined + ht_len + 1, as_src, as_len); | |
| 167 | combined[ht_len + 1 + as_len] = '\n'; | |
| 168 | memcpy(combined + ht_len + 1 + as_len + 1, sc_src, sc_len); | |
| 169 | combined[total_len] = '\0'; | |
| 170 | ||
| 171 | cl_program program = clCreateProgramWithSource(ctx, 1, | |
| 172 | (const char**)&combined, &total_len, &err); | |
| 173 | CL_CHECK(err, "create program"); | |
| 174 | ||
| 175 | char build_opts[512]; | |
| 176 | snprintf(build_opts, sizeof(build_opts), | |
| 177 | "-cl-std=CL1.2 " | |
| 178 | "-DWORD_CAPACITY=%d " | |
| 179 | "-DPAIR_CAPACITY=%d " | |
| 180 | "-DSECTION_CAPACITY=%d " | |
| 181 | "-DWORD_HT_CAPACITY=%d " | |
| 182 | "-DPAIR_HT_CAPACITY=%d " | |
| 183 | "-DSECTION_HT_CAPACITY=%d", | |
| 184 | WORD_CAPACITY, PAIR_CAPACITY, SECTION_CAPACITY, | |
| 185 | WORD_HT_CAPACITY, PAIR_HT_CAPACITY, SECTION_HT_CAPACITY); | |
| 186 | ||
| 187 | err = clBuildProgram(program, 1, &device, build_opts, NULL, NULL); | |
| 188 | if (err != CL_SUCCESS) { | |
| 189 | char log[16384]; | |
| 190 | clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, | |
| 191 | sizeof(log), log, NULL); | |
| 192 | fprintf(stderr, "Build error:\n%s\n", log); | |
| 193 | return 1; | |
| 194 | } | |
| 195 | printf("Kernels compiled successfully\n\n"); | |
| 196 | ||
| 197 | /* ─── Create kernels ─── */ | |
| 198 | ||
| 199 | cl_kernel k_extract = clCreateKernel(program, "extract_sections", &err); | |
| 200 | CL_CHECK(err, "kernel extract_sections"); | |
| 201 | cl_kernel k_read = clCreateKernel(program, "read_sections", &err); | |
| 202 | CL_CHECK(err, "kernel read_sections"); | |
| 203 | ||
| 204 | size_t local_size = 256; | |
| 205 | ||
| 206 | /* ─── Allocate GPU buffers ─── */ | |
| 207 | ||
| 208 | printf("Allocating GPU buffers...\n"); | |
| 209 | uint32_t zero = 0; | |
| 210 | ||
| 211 | /* Section hash table */ | |
| 212 | cl_mem sht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 213 | sizeof(uint64_t) * SECTION_HT_CAPACITY, NULL, &err); | |
| 214 | CL_CHECK(err, "sht_keys"); | |
| 215 | cl_mem sht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 216 | sizeof(uint32_t) * SECTION_HT_CAPACITY, NULL, &err); | |
| 217 | CL_CHECK(err, "sht_values"); | |
| 218 | ||
| 219 | /* Section pool SoA */ | |
| 220 | cl_mem sec_word = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 221 | sizeof(uint32_t) * SECTION_CAPACITY, NULL, &err); | |
| 222 | cl_mem sec_disjunct_hash = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 223 | sizeof(uint64_t) * SECTION_CAPACITY, NULL, &err); | |
| 224 | cl_mem sec_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 225 | sizeof(double) * SECTION_CAPACITY, NULL, &err); | |
| 226 | ||
| 227 | /* Section bump allocator */ | |
| 228 | cl_mem sec_next_free = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 229 | sizeof(uint32_t), &zero, &err); | |
| 230 | ||
| 231 | /* Total sections created (stats counter) */ | |
| 232 | cl_mem total_sections = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 233 | sizeof(uint32_t), &zero, &err); | |
| 234 | ||
| 235 | /* Initial reset */ | |
| 236 | reset_section_pool(queue, sht_keys, sht_values, sec_count, | |
| 237 | sec_next_free, total_sections); | |
| 238 | ||
| 239 | printf("GPU buffers ready\n\n"); | |
| 240 | ||
| 241 | /* ═══════════════════════════════════════════════════════════════ | |
| 242 | * TEST 1: Simple MST — 3-word sentence with chain parse | |
| 243 | * | |
| 244 | * Sentence: words [10, 20, 30] (word pool indices) | |
| 245 | * MST edges: (0,1), (1,2) — chain: 10—20—30 | |
| 246 | * | |
| 247 | * Expected sections: | |
| 248 | * Word 10 (pos 0): connectors = [(20, RIGHT)] | |
| 249 | * disjunct = "20+" | |
| 250 | * Word 20 (pos 1): connectors = [(10, LEFT), (30, RIGHT)] | |
| 251 | * disjunct = "10- 30+" | |
| 252 | * Word 30 (pos 2): connectors = [(20, LEFT)] | |
| 253 | * disjunct = "20-" | |
| 254 | * | |
| 255 | * = 3 unique sections | |
| 256 | * ═══════════════════════════════════════════════════════════════ */ | |
| 257 | ||
| 258 | printf("--- Test 1: Simple chain parse (3 words, 2 edges) ---\n"); | |
| 259 | ||
| 260 | uint32_t t1_words[] = {10, 20, 30}; | |
| 261 | uint32_t t1_sent_offsets[] = {0}; | |
| 262 | uint32_t t1_sent_lengths[] = {3}; | |
| 263 | uint32_t t1_edge_p1[] = {0, 1}; | |
| 264 | uint32_t t1_edge_p2[] = {1, 2}; | |
| 265 | uint32_t t1_edge_offsets[] = {0}; | |
| 266 | uint32_t t1_edge_counts[] = {2}; | |
| 267 | cl_uint t1_ns = 1, t1_tw = 3; | |
| 268 | ||
| 269 | cl_mem d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 270 | sizeof(uint32_t) * t1_tw, t1_words, &err); | |
| 271 | cl_mem d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 272 | sizeof(uint32_t) * t1_ns, t1_sent_offsets, &err); | |
| 273 | cl_mem d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 274 | sizeof(uint32_t) * t1_ns, t1_sent_lengths, &err); | |
| 275 | cl_mem d_edge_p1 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 276 | sizeof(uint32_t) * 2, t1_edge_p1, &err); | |
| 277 | cl_mem d_edge_p2 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 278 | sizeof(uint32_t) * 2, t1_edge_p2, &err); | |
| 279 | cl_mem d_edge_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 280 | sizeof(uint32_t) * t1_ns, t1_edge_offsets, &err); | |
| 281 | cl_mem d_edge_counts = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 282 | sizeof(uint32_t) * t1_ns, t1_edge_counts, &err); | |
| 283 | ||
| 284 | /* Set kernel args */ | |
| 285 | clSetKernelArg(k_extract, 0, sizeof(cl_mem), &d_flat_words); | |
| 286 | clSetKernelArg(k_extract, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 287 | clSetKernelArg(k_extract, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 288 | clSetKernelArg(k_extract, 3, sizeof(cl_uint), &t1_ns); | |
| 289 | clSetKernelArg(k_extract, 4, sizeof(cl_uint), &t1_tw); | |
| 290 | clSetKernelArg(k_extract, 5, sizeof(cl_mem), &d_edge_p1); | |
| 291 | clSetKernelArg(k_extract, 6, sizeof(cl_mem), &d_edge_p2); | |
| 292 | clSetKernelArg(k_extract, 7, sizeof(cl_mem), &d_edge_offsets); | |
| 293 | clSetKernelArg(k_extract, 8, sizeof(cl_mem), &d_edge_counts); | |
| 294 | clSetKernelArg(k_extract, 9, sizeof(cl_mem), &sht_keys); | |
| 295 | clSetKernelArg(k_extract, 10, sizeof(cl_mem), &sht_values); | |
| 296 | clSetKernelArg(k_extract, 11, sizeof(cl_mem), &sec_word); | |
| 297 | clSetKernelArg(k_extract, 12, sizeof(cl_mem), &sec_disjunct_hash); | |
| 298 | clSetKernelArg(k_extract, 13, sizeof(cl_mem), &sec_count); | |
| 299 | clSetKernelArg(k_extract, 14, sizeof(cl_mem), &sec_next_free); | |
| 300 | clSetKernelArg(k_extract, 15, sizeof(cl_mem), &total_sections); | |
| 301 | ||
| 302 | double t0 = now_ms(); | |
| 303 | size_t gs = ((t1_tw + local_size - 1) / local_size) * local_size; | |
| 304 | err = clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 305 | &gs, &local_size, 0, NULL, NULL); | |
| 306 | CL_CHECK(err, "enqueue extract"); | |
| 307 | clFinish(queue); | |
| 308 | double t1 = now_ms(); | |
| 309 | ||
| 310 | /* Read back results */ | |
| 311 | uint32_t h_num_sections, h_total_created; | |
| 312 | clEnqueueReadBuffer(queue, sec_next_free, CL_TRUE, 0, | |
| 313 | sizeof(uint32_t), &h_num_sections, 0, NULL, NULL); | |
| 314 | clEnqueueReadBuffer(queue, total_sections, CL_TRUE, 0, | |
| 315 | sizeof(uint32_t), &h_total_created, 0, NULL, NULL); | |
| 316 | ||
| 317 | /* Read section data */ | |
| 318 | uint32_t h_sec_words[8]; | |
| 319 | uint64_t h_sec_djh[8]; | |
| 320 | double h_sec_counts[8]; | |
| 321 | clEnqueueReadBuffer(queue, sec_word, CL_TRUE, 0, | |
| 322 | sizeof(uint32_t) * h_num_sections, h_sec_words, 0, NULL, NULL); | |
| 323 | clEnqueueReadBuffer(queue, sec_disjunct_hash, CL_TRUE, 0, | |
| 324 | sizeof(uint64_t) * h_num_sections, h_sec_djh, 0, NULL, NULL); | |
| 325 | clEnqueueReadBuffer(queue, sec_count, CL_TRUE, 0, | |
| 326 | sizeof(double) * h_num_sections, h_sec_counts, 0, NULL, NULL); | |
| 327 | ||
| 328 | /* Compute expected disjunct hashes on CPU */ | |
| 329 | /* Word 10 (pos 0): [(20, RIGHT=1)] */ | |
| 330 | uint32_t cw0[] = {20}; uint32_t cd0[] = {1}; | |
| 331 | uint64_t exp_djh_0 = cpu_hash_disjunct(cw0, cd0, 1); | |
| 332 | /* Word 20 (pos 1): [(10, LEFT=0), (30, RIGHT=1)] — already sorted */ | |
| 333 | uint32_t cw1[] = {10, 30}; uint32_t cd1[] = {0, 1}; | |
| 334 | uint64_t exp_djh_1 = cpu_hash_disjunct(cw1, cd1, 2); | |
| 335 | /* Word 30 (pos 2): [(20, LEFT=0)] */ | |
| 336 | uint32_t cw2[] = {20}; uint32_t cd2[] = {0}; | |
| 337 | uint64_t exp_djh_2 = cpu_hash_disjunct(cw2, cd2, 1); | |
| 338 | ||
| 339 | printf(" Sections created: %u (expected 3)\n", h_num_sections); | |
| 340 | printf(" Stats counter: %u (expected 3)\n", h_total_created); | |
| 341 | ||
| 342 | /* Verify each section exists with correct data */ | |
| 343 | int found_10 = 0, found_20 = 0, found_30 = 0; | |
| 344 | for (uint32_t i = 0; i < h_num_sections; i++) { | |
| 345 | if (h_sec_words[i] == 10 && h_sec_djh[i] == exp_djh_0 && | |
| 346 | fabs(h_sec_counts[i] - 1.0) < 0.01) found_10 = 1; | |
| 347 | if (h_sec_words[i] == 20 && h_sec_djh[i] == exp_djh_1 && | |
| 348 | fabs(h_sec_counts[i] - 1.0) < 0.01) found_20 = 1; | |
| 349 | if (h_sec_words[i] == 30 && h_sec_djh[i] == exp_djh_2 && | |
| 350 | fabs(h_sec_counts[i] - 1.0) < 0.01) found_30 = 1; | |
| 351 | } | |
| 352 | ||
| 353 | printf(" Section (word=10, djh=0x%016llx): %s\n", | |
| 354 | (unsigned long long)exp_djh_0, found_10 ? "found" : "MISSING"); | |
| 355 | printf(" Section (word=20, djh=0x%016llx): %s\n", | |
| 356 | (unsigned long long)exp_djh_1, found_20 ? "found" : "MISSING"); | |
| 357 | printf(" Section (word=30, djh=0x%016llx): %s\n", | |
| 358 | (unsigned long long)exp_djh_2, found_30 ? "found" : "MISSING"); | |
| 359 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 360 | ||
| 361 | int t1_pass = (h_num_sections == 3) && (h_total_created == 3) && | |
| 362 | found_10 && found_20 && found_30; | |
| 363 | printf(" %s\n\n", t1_pass ? "PASS" : "FAIL"); | |
| 364 | if (t1_pass) pass_count++; else fail_count++; | |
| 365 | ||
| 366 | /* ═══════════════════════════════════════════════════════════════ | |
| 367 | * TEST 2: Star parse — root word connected to all others | |
| 368 | * | |
| 369 | * Sentence: words [100, 101, 102, 103, 104] (5 words) | |
| 370 | * MST edges: (2,0), (2,1), (2,3), (2,4) — word 102 is root | |
| 371 | * | |
| 372 | * Expected sections: | |
| 373 | * Word 100 (pos 0): [(102, RIGHT)] — 1 connector | |
| 374 | * Word 101 (pos 1): [(102, RIGHT)] — 1 connector | |
| 375 | * Word 102 (pos 2): [(100, LEFT), (101, LEFT), (103, RIGHT), (104, RIGHT)] | |
| 376 | * Word 103 (pos 3): [(102, LEFT)] — 1 connector | |
| 377 | * Word 104 (pos 4): [(102, LEFT)] — 1 connector | |
| 378 | * | |
| 379 | * = 5 sections (4 unique disjuncts: leaf-left, leaf-right, root-4conn) | |
| 380 | * But words 100 and 101 have same disjunct hash ONLY if their | |
| 381 | * partner word pool index is the same (both connect to 102) AND | |
| 382 | * direction is the same (both RIGHT). So disjunct hash matches! | |
| 383 | * → 100 and 101 have same disjunct but different words → 2 sections | |
| 384 | * Similarly 103 and 104 connect LEFT to 102 → same disjunct → 2 sections | |
| 385 | * | |
| 386 | * Total unique sections (word, disjunct) pairs: 5 | |
| 387 | * ═══════════════════════════════════════════════════════════════ */ | |
| 388 | ||
| 389 | printf("--- Test 2: Star parse (5 words, root at center) ---\n"); | |
| 390 | ||
| 391 | reset_section_pool(queue, sht_keys, sht_values, sec_count, | |
| 392 | sec_next_free, total_sections); | |
| 393 | ||
| 394 | uint32_t t2_words[] = {100, 101, 102, 103, 104}; | |
| 395 | uint32_t t2_sent_offsets[] = {0}; | |
| 396 | uint32_t t2_sent_lengths[] = {5}; | |
| 397 | uint32_t t2_edge_p1[] = {2, 2, 2, 2}; | |
| 398 | uint32_t t2_edge_p2[] = {0, 1, 3, 4}; | |
| 399 | uint32_t t2_edge_offsets[] = {0}; | |
| 400 | uint32_t t2_edge_counts[] = {4}; | |
| 401 | cl_uint t2_ns = 1, t2_tw = 5; | |
| 402 | ||
| 403 | clReleaseMemObject(d_flat_words); | |
| 404 | clReleaseMemObject(d_sent_offsets); | |
| 405 | clReleaseMemObject(d_sent_lengths); | |
| 406 | clReleaseMemObject(d_edge_p1); | |
| 407 | clReleaseMemObject(d_edge_p2); | |
| 408 | clReleaseMemObject(d_edge_offsets); | |
| 409 | clReleaseMemObject(d_edge_counts); | |
| 410 | ||
| 411 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 412 | sizeof(uint32_t) * t2_tw, t2_words, &err); | |
| 413 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 414 | sizeof(uint32_t) * t2_ns, t2_sent_offsets, &err); | |
| 415 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 416 | sizeof(uint32_t) * t2_ns, t2_sent_lengths, &err); | |
| 417 | d_edge_p1 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 418 | sizeof(uint32_t) * 4, t2_edge_p1, &err); | |
| 419 | d_edge_p2 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 420 | sizeof(uint32_t) * 4, t2_edge_p2, &err); | |
| 421 | d_edge_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 422 | sizeof(uint32_t) * t2_ns, t2_edge_offsets, &err); | |
| 423 | d_edge_counts = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 424 | sizeof(uint32_t) * t2_ns, t2_edge_counts, &err); | |
| 425 | ||
| 426 | /* Set kernel args */ | |
| 427 | clSetKernelArg(k_extract, 0, sizeof(cl_mem), &d_flat_words); | |
| 428 | clSetKernelArg(k_extract, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 429 | clSetKernelArg(k_extract, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 430 | clSetKernelArg(k_extract, 3, sizeof(cl_uint), &t2_ns); | |
| 431 | clSetKernelArg(k_extract, 4, sizeof(cl_uint), &t2_tw); | |
| 432 | clSetKernelArg(k_extract, 5, sizeof(cl_mem), &d_edge_p1); | |
| 433 | clSetKernelArg(k_extract, 6, sizeof(cl_mem), &d_edge_p2); | |
| 434 | clSetKernelArg(k_extract, 7, sizeof(cl_mem), &d_edge_offsets); | |
| 435 | clSetKernelArg(k_extract, 8, sizeof(cl_mem), &d_edge_counts); | |
| 436 | ||
| 437 | t0 = now_ms(); | |
| 438 | gs = ((t2_tw + local_size - 1) / local_size) * local_size; | |
| 439 | err = clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 440 | &gs, &local_size, 0, NULL, NULL); | |
| 441 | CL_CHECK(err, "enqueue extract t2"); | |
| 442 | clFinish(queue); | |
| 443 | t1 = now_ms(); | |
| 444 | ||
| 445 | clEnqueueReadBuffer(queue, sec_next_free, CL_TRUE, 0, | |
| 446 | sizeof(uint32_t), &h_num_sections, 0, NULL, NULL); | |
| 447 | clEnqueueReadBuffer(queue, total_sections, CL_TRUE, 0, | |
| 448 | sizeof(uint32_t), &h_total_created, 0, NULL, NULL); | |
| 449 | ||
| 450 | /* Compute expected disjunct for root word 102 (pos 2): | |
| 451 | * Connectors: (100, LEFT=0), (101, LEFT=0), (103, RIGHT=1), (104, RIGHT=1) | |
| 452 | * Sorted: dir 0 first sorted by word → (100,0), (101,0), (103,1), (104,1) */ | |
| 453 | uint32_t root_cw[] = {100, 101, 103, 104}; | |
| 454 | uint32_t root_cd[] = {0, 0, 1, 1}; | |
| 455 | uint64_t exp_root_djh = cpu_hash_disjunct(root_cw, root_cd, 4); | |
| 456 | ||
| 457 | /* Read back all sections */ | |
| 458 | uint32_t h2_sec_words[8]; | |
| 459 | uint64_t h2_sec_djh[8]; | |
| 460 | double h2_sec_counts[8]; | |
| 461 | uint32_t n_read = (h_num_sections < 8) ? h_num_sections : 8; | |
| 462 | clEnqueueReadBuffer(queue, sec_word, CL_TRUE, 0, | |
| 463 | sizeof(uint32_t) * n_read, h2_sec_words, 0, NULL, NULL); | |
| 464 | clEnqueueReadBuffer(queue, sec_disjunct_hash, CL_TRUE, 0, | |
| 465 | sizeof(uint64_t) * n_read, h2_sec_djh, 0, NULL, NULL); | |
| 466 | clEnqueueReadBuffer(queue, sec_count, CL_TRUE, 0, | |
| 467 | sizeof(double) * n_read, h2_sec_counts, 0, NULL, NULL); | |
| 468 | ||
| 469 | printf(" Sections created: %u (expected 5)\n", h_num_sections); | |
| 470 | ||
| 471 | /* Find root section */ | |
| 472 | int found_root = 0; | |
| 473 | for (uint32_t i = 0; i < n_read; i++) { | |
| 474 | if (h2_sec_words[i] == 102 && h2_sec_djh[i] == exp_root_djh && | |
| 475 | fabs(h2_sec_counts[i] - 1.0) < 0.01) { | |
| 476 | found_root = 1; | |
| 477 | } | |
| 478 | } | |
| 479 | printf(" Root section (word=102, 4 connectors): %s\n", | |
| 480 | found_root ? "found" : "MISSING"); | |
| 481 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 482 | ||
| 483 | int t2_pass = (h_num_sections == 5) && found_root; | |
| 484 | printf(" %s\n\n", t2_pass ? "PASS" : "FAIL"); | |
| 485 | if (t2_pass) pass_count++; else fail_count++; | |
| 486 | ||
| 487 | /* ═══════════════════════════════════════════════════════════════ | |
| 488 | * TEST 3: Multi-sentence batch (no cross-boundary sections) | |
| 489 | * | |
| 490 | * Sentence 1: [10, 20, 30] edges: (0,1), (1,2) — chain | |
| 491 | * Sentence 2: [40, 50, 60] edges: (0,1), (0,2) — star from 40 | |
| 492 | * | |
| 493 | * flat_words = [10, 20, 30, 40, 50, 60] | |
| 494 | * flat_edges = [(0,1), (1,2), (0,1), (0,2)] | |
| 495 | * edge_offsets = [0, 2] | |
| 496 | * edge_counts = [2, 2] | |
| 497 | * | |
| 498 | * Expected: 6 sections (3 per sentence), none spanning boundary | |
| 499 | * ═══════════════════════════════════════════════════════════════ */ | |
| 500 | ||
| 501 | printf("--- Test 3: Multi-sentence batch ---\n"); | |
| 502 | ||
| 503 | reset_section_pool(queue, sht_keys, sht_values, sec_count, | |
| 504 | sec_next_free, total_sections); | |
| 505 | ||
| 506 | uint32_t t3_words[] = {10, 20, 30, 40, 50, 60}; | |
| 507 | uint32_t t3_sent_offsets[] = {0, 3}; | |
| 508 | uint32_t t3_sent_lengths[] = {3, 3}; | |
| 509 | uint32_t t3_edge_p1[] = {0, 1, 0, 0}; | |
| 510 | uint32_t t3_edge_p2[] = {1, 2, 1, 2}; | |
| 511 | uint32_t t3_edge_offsets[] = {0, 2}; | |
| 512 | uint32_t t3_edge_counts[] = {2, 2}; | |
| 513 | cl_uint t3_ns = 2, t3_tw = 6; | |
| 514 | ||
| 515 | clReleaseMemObject(d_flat_words); | |
| 516 | clReleaseMemObject(d_sent_offsets); | |
| 517 | clReleaseMemObject(d_sent_lengths); | |
| 518 | clReleaseMemObject(d_edge_p1); | |
| 519 | clReleaseMemObject(d_edge_p2); | |
| 520 | clReleaseMemObject(d_edge_offsets); | |
| 521 | clReleaseMemObject(d_edge_counts); | |
| 522 | ||
| 523 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 524 | sizeof(uint32_t) * t3_tw, t3_words, &err); | |
| 525 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 526 | sizeof(uint32_t) * t3_ns, t3_sent_offsets, &err); | |
| 527 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 528 | sizeof(uint32_t) * t3_ns, t3_sent_lengths, &err); | |
| 529 | d_edge_p1 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 530 | sizeof(uint32_t) * 4, t3_edge_p1, &err); | |
| 531 | d_edge_p2 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 532 | sizeof(uint32_t) * 4, t3_edge_p2, &err); | |
| 533 | d_edge_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 534 | sizeof(uint32_t) * t3_ns, t3_edge_offsets, &err); | |
| 535 | d_edge_counts = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 536 | sizeof(uint32_t) * t3_ns, t3_edge_counts, &err); | |
| 537 | ||
| 538 | clSetKernelArg(k_extract, 0, sizeof(cl_mem), &d_flat_words); | |
| 539 | clSetKernelArg(k_extract, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 540 | clSetKernelArg(k_extract, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 541 | clSetKernelArg(k_extract, 3, sizeof(cl_uint), &t3_ns); | |
| 542 | clSetKernelArg(k_extract, 4, sizeof(cl_uint), &t3_tw); | |
| 543 | clSetKernelArg(k_extract, 5, sizeof(cl_mem), &d_edge_p1); | |
| 544 | clSetKernelArg(k_extract, 6, sizeof(cl_mem), &d_edge_p2); | |
| 545 | clSetKernelArg(k_extract, 7, sizeof(cl_mem), &d_edge_offsets); | |
| 546 | clSetKernelArg(k_extract, 8, sizeof(cl_mem), &d_edge_counts); | |
| 547 | ||
| 548 | t0 = now_ms(); | |
| 549 | gs = ((t3_tw + local_size - 1) / local_size) * local_size; | |
| 550 | err = clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 551 | &gs, &local_size, 0, NULL, NULL); | |
| 552 | CL_CHECK(err, "enqueue extract t3"); | |
| 553 | clFinish(queue); | |
| 554 | t1 = now_ms(); | |
| 555 | ||
| 556 | clEnqueueReadBuffer(queue, sec_next_free, CL_TRUE, 0, | |
| 557 | sizeof(uint32_t), &h_num_sections, 0, NULL, NULL); | |
| 558 | ||
| 559 | /* Verify: sentence 1 chain gives 3 sections, sentence 2 star gives 3. | |
| 560 | * But sentence 1's word 10 has disjunct "20+" and sentence 2's word 50 | |
| 561 | * has disjunct "40-" — different disjuncts. All 6 words produce sections. | |
| 562 | * Are any (word, disjunct) pairs the same? No — different words, different | |
| 563 | * disjuncts. So 6 unique sections. */ | |
| 564 | printf(" Sections created: %u (expected 6)\n", h_num_sections); | |
| 565 | printf(" Time: %.2f ms\n", t1 - t0); | |
| 566 | ||
| 567 | int t3_pass = (h_num_sections == 6); | |
| 568 | printf(" %s\n\n", t3_pass ? "PASS" : "FAIL"); | |
| 569 | if (t3_pass) pass_count++; else fail_count++; | |
| 570 | ||
| 571 | /* ═══════════════════════════════════════════════════════════════ | |
| 572 | * TEST 4: Duplicate sections — same parse seen twice | |
| 573 | * | |
| 574 | * Process the SAME sentence twice → section counts should be 2.0 | |
| 575 | * | |
| 576 | * Sentence: [10, 20, 30] edges: (0,1), (1,2) | |
| 577 | * Run extract_sections TWICE without resetting. | |
| 578 | * | |
| 579 | * Expected: 3 sections, each with count = 2.0 | |
| 580 | * ═══════════════════════════════════════════════════════════════ */ | |
| 581 | ||
| 582 | printf("--- Test 4: Duplicate sections (same parse twice) ---\n"); | |
| 583 | ||
| 584 | reset_section_pool(queue, sht_keys, sht_values, sec_count, | |
| 585 | sec_next_free, total_sections); | |
| 586 | ||
| 587 | uint32_t t4_words[] = {10, 20, 30}; | |
| 588 | uint32_t t4_sent_offsets[] = {0}; | |
| 589 | uint32_t t4_sent_lengths[] = {3}; | |
| 590 | uint32_t t4_edge_p1[] = {0, 1}; | |
| 591 | uint32_t t4_edge_p2[] = {1, 2}; | |
| 592 | uint32_t t4_edge_offsets[] = {0}; | |
| 593 | uint32_t t4_edge_counts[] = {2}; | |
| 594 | cl_uint t4_ns = 1, t4_tw = 3; | |
| 595 | ||
| 596 | clReleaseMemObject(d_flat_words); | |
| 597 | clReleaseMemObject(d_sent_offsets); | |
| 598 | clReleaseMemObject(d_sent_lengths); | |
| 599 | clReleaseMemObject(d_edge_p1); | |
| 600 | clReleaseMemObject(d_edge_p2); | |
| 601 | clReleaseMemObject(d_edge_offsets); | |
| 602 | clReleaseMemObject(d_edge_counts); | |
| 603 | ||
| 604 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 605 | sizeof(uint32_t) * t4_tw, t4_words, &err); | |
| 606 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 607 | sizeof(uint32_t) * t4_ns, t4_sent_offsets, &err); | |
| 608 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 609 | sizeof(uint32_t) * t4_ns, t4_sent_lengths, &err); | |
| 610 | d_edge_p1 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 611 | sizeof(uint32_t) * 2, t4_edge_p1, &err); | |
| 612 | d_edge_p2 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 613 | sizeof(uint32_t) * 2, t4_edge_p2, &err); | |
| 614 | d_edge_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 615 | sizeof(uint32_t) * t4_ns, t4_edge_offsets, &err); | |
| 616 | d_edge_counts = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 617 | sizeof(uint32_t) * t4_ns, t4_edge_counts, &err); | |
| 618 | ||
| 619 | clSetKernelArg(k_extract, 0, sizeof(cl_mem), &d_flat_words); | |
| 620 | clSetKernelArg(k_extract, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 621 | clSetKernelArg(k_extract, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 622 | clSetKernelArg(k_extract, 3, sizeof(cl_uint), &t4_ns); | |
| 623 | clSetKernelArg(k_extract, 4, sizeof(cl_uint), &t4_tw); | |
| 624 | clSetKernelArg(k_extract, 5, sizeof(cl_mem), &d_edge_p1); | |
| 625 | clSetKernelArg(k_extract, 6, sizeof(cl_mem), &d_edge_p2); | |
| 626 | clSetKernelArg(k_extract, 7, sizeof(cl_mem), &d_edge_offsets); | |
| 627 | clSetKernelArg(k_extract, 8, sizeof(cl_mem), &d_edge_counts); | |
| 628 | ||
| 629 | /* Run TWICE */ | |
| 630 | gs = ((t4_tw + local_size - 1) / local_size) * local_size; | |
| 631 | err = clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 632 | &gs, &local_size, 0, NULL, NULL); | |
| 633 | CL_CHECK(err, "enqueue extract t4a"); | |
| 634 | clFinish(queue); | |
| 635 | ||
| 636 | err = clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 637 | &gs, &local_size, 0, NULL, NULL); | |
| 638 | CL_CHECK(err, "enqueue extract t4b"); | |
| 639 | clFinish(queue); | |
| 640 | ||
| 641 | clEnqueueReadBuffer(queue, sec_next_free, CL_TRUE, 0, | |
| 642 | sizeof(uint32_t), &h_num_sections, 0, NULL, NULL); | |
| 643 | ||
| 644 | /* Read counts */ | |
| 645 | double h4_counts[4]; | |
| 646 | clEnqueueReadBuffer(queue, sec_count, CL_TRUE, 0, | |
| 647 | sizeof(double) * h_num_sections, h4_counts, 0, NULL, NULL); | |
| 648 | ||
| 649 | printf(" Sections created: %u (expected 3 — dedup works)\n", h_num_sections); | |
| 650 | ||
| 651 | int all_count_2 = 1; | |
| 652 | for (uint32_t i = 0; i < h_num_sections; i++) { | |
| 653 | printf(" Section %u count: %.1f (expected 2.0)\n", i, h4_counts[i]); | |
| 654 | if (fabs(h4_counts[i] - 2.0) > 0.01) all_count_2 = 0; | |
| 655 | } | |
| 656 | ||
| 657 | int t4_pass = (h_num_sections == 3) && all_count_2; | |
| 658 | printf(" %s\n\n", t4_pass ? "PASS" : "FAIL"); | |
| 659 | if (t4_pass) pass_count++; else fail_count++; | |
| 660 | ||
| 661 | /* ═══════════════════════════════════════════════════════════════ | |
| 662 | * TEST 5: Readback kernel | |
| 663 | * | |
| 664 | * Use read_sections to verify section pool data matches | |
| 665 | * what extract_sections stored. (Reuse state from test 4.) | |
| 666 | * ═══════════════════════════════════════════════════════════════ */ | |
| 667 | ||
| 668 | printf("--- Test 5: Readback kernel ---\n"); | |
| 669 | ||
| 670 | uint32_t n_secs = h_num_sections; | |
| 671 | cl_mem d_out_word = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 672 | sizeof(uint32_t) * n_secs, NULL, &err); | |
| 673 | cl_mem d_out_djh = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 674 | sizeof(uint64_t) * n_secs, NULL, &err); | |
| 675 | cl_mem d_out_count = clCreateBuffer(ctx, CL_MEM_WRITE_ONLY, | |
| 676 | sizeof(double) * n_secs, NULL, &err); | |
| 677 | ||
| 678 | clSetKernelArg(k_read, 0, sizeof(cl_mem), &sec_word); | |
| 679 | clSetKernelArg(k_read, 1, sizeof(cl_mem), &sec_disjunct_hash); | |
| 680 | clSetKernelArg(k_read, 2, sizeof(cl_mem), &sec_count); | |
| 681 | clSetKernelArg(k_read, 3, sizeof(cl_mem), &d_out_word); | |
| 682 | clSetKernelArg(k_read, 4, sizeof(cl_mem), &d_out_djh); | |
| 683 | clSetKernelArg(k_read, 5, sizeof(cl_mem), &d_out_count); | |
| 684 | clSetKernelArg(k_read, 6, sizeof(cl_uint), &n_secs); | |
| 685 | ||
| 686 | gs = ((n_secs + local_size - 1) / local_size) * local_size; | |
| 687 | err = clEnqueueNDRangeKernel(queue, k_read, 1, NULL, | |
| 688 | &gs, &local_size, 0, NULL, NULL); | |
| 689 | CL_CHECK(err, "enqueue read_sections"); | |
| 690 | clFinish(queue); | |
| 691 | ||
| 692 | uint32_t rb_words[4]; | |
| 693 | uint64_t rb_djh[4]; | |
| 694 | double rb_counts[4]; | |
| 695 | clEnqueueReadBuffer(queue, d_out_word, CL_TRUE, 0, | |
| 696 | sizeof(uint32_t) * n_secs, rb_words, 0, NULL, NULL); | |
| 697 | clEnqueueReadBuffer(queue, d_out_djh, CL_TRUE, 0, | |
| 698 | sizeof(uint64_t) * n_secs, rb_djh, 0, NULL, NULL); | |
| 699 | clEnqueueReadBuffer(queue, d_out_count, CL_TRUE, 0, | |
| 700 | sizeof(double) * n_secs, rb_counts, 0, NULL, NULL); | |
| 701 | ||
| 702 | /* Should match test 4's data */ | |
| 703 | uint32_t h5_words[4]; | |
| 704 | uint64_t h5_djh[4]; | |
| 705 | double h5_counts[4]; | |
| 706 | clEnqueueReadBuffer(queue, sec_word, CL_TRUE, 0, | |
| 707 | sizeof(uint32_t) * n_secs, h5_words, 0, NULL, NULL); | |
| 708 | clEnqueueReadBuffer(queue, sec_disjunct_hash, CL_TRUE, 0, | |
| 709 | sizeof(uint64_t) * n_secs, h5_djh, 0, NULL, NULL); | |
| 710 | clEnqueueReadBuffer(queue, sec_count, CL_TRUE, 0, | |
| 711 | sizeof(double) * n_secs, h5_counts, 0, NULL, NULL); | |
| 712 | ||
| 713 | int readback_match = 1; | |
| 714 | for (uint32_t i = 0; i < n_secs; i++) { | |
| 715 | if (rb_words[i] != h5_words[i] || rb_djh[i] != h5_djh[i] || | |
| 716 | fabs(rb_counts[i] - h5_counts[i]) > 0.01) | |
| 717 | readback_match = 0; | |
| 718 | } | |
| 719 | printf(" Readback matches direct read: %s\n", readback_match ? "yes" : "NO"); | |
| 720 | ||
| 721 | int t5_pass = readback_match; | |
| 722 | printf(" %s\n\n", t5_pass ? "PASS" : "FAIL"); | |
| 723 | if (t5_pass) pass_count++; else fail_count++; | |
| 724 | ||
| 725 | clReleaseMemObject(d_out_word); | |
| 726 | clReleaseMemObject(d_out_djh); | |
| 727 | clReleaseMemObject(d_out_count); | |
| 728 | ||
| 729 | /* ═══════════════════════════════════════════════════════════════ | |
| 730 | * TEST 6: Benchmark — 1000 sentences with random MST edges | |
| 731 | * | |
| 732 | * Each sentence: 10-20 words, 9-19 MST edges (chain parse) | |
| 733 | * Total: ~15000 words, ~14000 edges | |
| 734 | * | |
| 735 | * Measures extract_sections throughput. | |
| 736 | * ═══════════════════════════════════════════════════════════════ */ | |
| 737 | ||
| 738 | printf("--- Test 6: Benchmark (1000 sentences) ---\n"); | |
| 739 | ||
| 740 | reset_section_pool(queue, sht_keys, sht_values, sec_count, | |
| 741 | sec_next_free, total_sections); | |
| 742 | ||
| 743 | srand(42); | |
| 744 | uint32_t bench_ns = 1000; | |
| 745 | uint32_t vocab_size = 500; /* word pool indices 0..499 */ | |
| 746 | ||
| 747 | /* Generate sentences */ | |
| 748 | uint32_t max_words = bench_ns * 25; | |
| 749 | uint32_t max_edges = bench_ns * 25; | |
| 750 | uint32_t* b_words = malloc(sizeof(uint32_t) * max_words); | |
| 751 | uint32_t* b_sent_offsets = malloc(sizeof(uint32_t) * bench_ns); | |
| 752 | uint32_t* b_sent_lengths = malloc(sizeof(uint32_t) * bench_ns); | |
| 753 | uint32_t* b_edge_p1 = malloc(sizeof(uint32_t) * max_edges); | |
| 754 | uint32_t* b_edge_p2 = malloc(sizeof(uint32_t) * max_edges); | |
| 755 | uint32_t* b_edge_offsets = malloc(sizeof(uint32_t) * bench_ns); | |
| 756 | uint32_t* b_edge_counts = malloc(sizeof(uint32_t) * bench_ns); | |
| 757 | ||
| 758 | uint32_t word_pos = 0, edge_pos = 0; | |
| 759 | for (uint32_t s = 0; s < bench_ns; s++) { | |
| 760 | uint32_t slen = 10 + (rand() % 11); /* 10-20 words */ | |
| 761 | b_sent_offsets[s] = word_pos; | |
| 762 | b_sent_lengths[s] = slen; | |
| 763 | b_edge_offsets[s] = edge_pos; | |
| 764 | b_edge_counts[s] = slen - 1; /* chain parse */ | |
| 765 | ||
| 766 | for (uint32_t w = 0; w < slen; w++) { | |
| 767 | b_words[word_pos++] = rand() % vocab_size; | |
| 768 | } | |
| 769 | /* Chain parse: pos 0-1, 1-2, ..., (slen-2)-(slen-1) */ | |
| 770 | for (uint32_t e = 0; e < slen - 1; e++) { | |
| 771 | b_edge_p1[edge_pos] = e; | |
| 772 | b_edge_p2[edge_pos] = e + 1; | |
| 773 | edge_pos++; | |
| 774 | } | |
| 775 | } | |
| 776 | ||
| 777 | printf(" Total words: %u\n", word_pos); | |
| 778 | printf(" Total edges: %u\n", edge_pos); | |
| 779 | ||
| 780 | clReleaseMemObject(d_flat_words); | |
| 781 | clReleaseMemObject(d_sent_offsets); | |
| 782 | clReleaseMemObject(d_sent_lengths); | |
| 783 | clReleaseMemObject(d_edge_p1); | |
| 784 | clReleaseMemObject(d_edge_p2); | |
| 785 | clReleaseMemObject(d_edge_offsets); | |
| 786 | clReleaseMemObject(d_edge_counts); | |
| 787 | ||
| 788 | d_flat_words = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 789 | sizeof(uint32_t) * word_pos, b_words, &err); | |
| 790 | d_sent_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 791 | sizeof(uint32_t) * bench_ns, b_sent_offsets, &err); | |
| 792 | d_sent_lengths = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 793 | sizeof(uint32_t) * bench_ns, b_sent_lengths, &err); | |
| 794 | d_edge_p1 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 795 | sizeof(uint32_t) * edge_pos, b_edge_p1, &err); | |
| 796 | d_edge_p2 = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 797 | sizeof(uint32_t) * edge_pos, b_edge_p2, &err); | |
| 798 | d_edge_offsets = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 799 | sizeof(uint32_t) * bench_ns, b_edge_offsets, &err); | |
| 800 | d_edge_counts = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 801 | sizeof(uint32_t) * bench_ns, b_edge_counts, &err); | |
| 802 | ||
| 803 | cl_uint tw_bench = word_pos; | |
| 804 | clSetKernelArg(k_extract, 0, sizeof(cl_mem), &d_flat_words); | |
| 805 | clSetKernelArg(k_extract, 1, sizeof(cl_mem), &d_sent_offsets); | |
| 806 | clSetKernelArg(k_extract, 2, sizeof(cl_mem), &d_sent_lengths); | |
| 807 | clSetKernelArg(k_extract, 3, sizeof(cl_uint), &bench_ns); | |
| 808 | clSetKernelArg(k_extract, 4, sizeof(cl_uint), &tw_bench); | |
| 809 | clSetKernelArg(k_extract, 5, sizeof(cl_mem), &d_edge_p1); | |
| 810 | clSetKernelArg(k_extract, 6, sizeof(cl_mem), &d_edge_p2); | |
| 811 | clSetKernelArg(k_extract, 7, sizeof(cl_mem), &d_edge_offsets); | |
| 812 | clSetKernelArg(k_extract, 8, sizeof(cl_mem), &d_edge_counts); | |
| 813 | ||
| 814 | /* Warm up */ | |
| 815 | gs = ((tw_bench + local_size - 1) / local_size) * local_size; | |
| 816 | clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 817 | &gs, &local_size, 0, NULL, NULL); | |
| 818 | clFinish(queue); | |
| 819 | ||
| 820 | /* Reset for actual benchmark */ | |
| 821 | reset_section_pool(queue, sht_keys, sht_values, sec_count, | |
| 822 | sec_next_free, total_sections); | |
| 823 | ||
| 824 | t0 = now_ms(); | |
| 825 | err = clEnqueueNDRangeKernel(queue, k_extract, 1, NULL, | |
| 826 | &gs, &local_size, 0, NULL, NULL); | |
| 827 | CL_CHECK(err, "enqueue benchmark"); | |
| 828 | clFinish(queue); | |
| 829 | t1 = now_ms(); | |
| 830 | ||
| 831 | clEnqueueReadBuffer(queue, sec_next_free, CL_TRUE, 0, | |
| 832 | sizeof(uint32_t), &h_num_sections, 0, NULL, NULL); | |
| 833 | ||
| 834 | double elapsed = t1 - t0; | |
| 835 | double secs_per_sec = bench_ns / (elapsed / 1000.0); | |
| 836 | double words_per_sec = word_pos / (elapsed / 1000.0); | |
| 837 | ||
| 838 | printf(" Sections created: %u\n", h_num_sections); | |
| 839 | printf(" Time: %.2f ms\n", elapsed); | |
| 840 | printf(" Throughput: %.0f sentences/sec, %.0f words/sec\n", | |
| 841 | secs_per_sec, words_per_sec); | |
| 842 | printf(" Throughput: %.1fM sections/sec\n", | |
| 843 | h_num_sections / (elapsed / 1000.0) / 1e6); | |
| 844 | ||
| 845 | /* Sanity: every word should produce a section in a chain parse, | |
| 846 | * but some (word, disjunct) pairs may collide. So sections < words | |
| 847 | * but > 0. */ | |
| 848 | int t6_pass = (h_num_sections > 0) && (h_num_sections <= word_pos) && | |
| 849 | (elapsed < 1000.0); | |
| 850 | printf(" %s\n\n", t6_pass ? "PASS" : "FAIL"); | |
| 851 | if (t6_pass) pass_count++; else fail_count++; | |
| 852 | ||
| 853 | /* ─── Summary ─── */ | |
| 854 | ||
| 855 | printf("=== Results: %d PASS, %d FAIL ===\n", pass_count, fail_count); | |
| 856 | ||
| 857 | /* Cleanup */ | |
| 858 | free(b_words); free(b_sent_offsets); free(b_sent_lengths); | |
| 859 | free(b_edge_p1); free(b_edge_p2); free(b_edge_offsets); free(b_edge_counts); | |
| 860 | free(ht_src); free(as_src); free(sc_src); free(combined); | |
| 861 | ||
| 862 | clReleaseMemObject(d_flat_words); | |
| 863 | clReleaseMemObject(d_sent_offsets); | |
| 864 | clReleaseMemObject(d_sent_lengths); | |
| 865 | clReleaseMemObject(d_edge_p1); | |
| 866 | clReleaseMemObject(d_edge_p2); | |
| 867 | clReleaseMemObject(d_edge_offsets); | |
| 868 | clReleaseMemObject(d_edge_counts); | |
| 869 | clReleaseMemObject(sht_keys); | |
| 870 | clReleaseMemObject(sht_values); | |
| 871 | clReleaseMemObject(sec_word); | |
| 872 | clReleaseMemObject(sec_disjunct_hash); | |
| 873 | clReleaseMemObject(sec_count); | |
| 874 | clReleaseMemObject(sec_next_free); | |
| 875 | clReleaseMemObject(total_sections); | |
| 876 | clReleaseKernel(k_extract); | |
| 877 | clReleaseKernel(k_read); | |
| 878 | clReleaseProgram(program); | |
| 879 | clReleaseCommandQueue(queue); | |
| 880 | clReleaseContext(ctx); | |
| 881 | ||
| 882 | return fail_count > 0 ? 1 : 0; | |
| 883 | } |
| … | … | @@ -1,606 +0,0 @@ |
| 1 | /* test-spanning-forest.cu — Tests T5 and T6 for grammar parse + surprise | |
| 2 | * | |
| 3 | * T5: Grammar parse (3 sentences with known MI, verify correct MST edges) | |
| 4 | * T6: Surprise (grammar vs word parse — verify surprise scoring) | |
| 5 | * | |
| 6 | * Build: | |
| 7 | * nvcc -O2 -arch=sm_75 -rdc=true \ | |
| 8 | * -o test-spanning-forest \ | |
| 9 | * test-spanning-forest.cu gpu-spanning-forest.cu \ | |
| 10 | * -lcudadevrt -lm | |
| 11 | */ | |
| 12 | ||
| 13 | #include <cstdio> | |
| 14 | #include <cstdlib> | |
| 15 | #include <cstring> | |
| 16 | #include <cmath> | |
| 17 | #include <cstdint> | |
| 18 | ||
| 19 | #define MAX_TREE_EDGES 63 /* MAX_SENTENCE_LEN - 1 */ | |
| 20 | ||
| 21 | /* Forward declaration */ | |
| 22 | extern "C" void spanning_forest_run( | |
| 23 | const uint32_t* d_sentence_words, | |
| 24 | const uint32_t* d_sentence_lengths, | |
| 25 | const uint32_t* d_sentence_offsets, | |
| 26 | uint32_t num_sentences, | |
| 27 | const uint32_t* d_word_class_id, | |
| 28 | const uint64_t* d_class_ht_keys, | |
| 29 | const double* d_class_ht_mi, | |
| 30 | uint32_t class_ht_capacity, | |
| 31 | const uint64_t* d_pair_ht_keys, | |
| 32 | const uint32_t* d_pair_ht_values, | |
| 33 | const double* d_pair_mi, | |
| 34 | uint32_t pair_ht_capacity, | |
| 35 | uint32_t* d_word_parse_a, uint32_t* d_word_parse_b, | |
| 36 | double* d_word_parse_mi, uint32_t* d_word_parse_count, | |
| 37 | uint32_t* d_gram_parse_a, uint32_t* d_gram_parse_b, | |
| 38 | double* d_gram_parse_mi, uint32_t* d_gram_parse_count, | |
| 39 | double* d_surprise); | |
| 40 | ||
| 41 | /* ─── Helper: Build a simple word-pair MI hash table on GPU ─── */ | |
| 42 | ||
| 43 | struct SimpleHTBuilder { | |
| 44 | /* Host data */ | |
| 45 | uint64_t* h_keys; | |
| 46 | uint32_t* h_values; | |
| 47 | double* h_mi; | |
| 48 | uint32_t capacity; | |
| 49 | uint32_t pair_count; | |
| 50 | ||
| 51 | /* Device data */ | |
| 52 | uint64_t* d_keys; | |
| 53 | uint32_t* d_values; | |
| 54 | double* d_mi; | |
| 55 | }; | |
| 56 | ||
| 57 | uint64_t splitmix64(uint64_t key) { | |
| 58 | key ^= key >> 30; | |
| 59 | key *= 0xBF58476D1CE4E5B9ULL; | |
| 60 | key ^= key >> 27; | |
| 61 | key *= 0x94D049BB133111EBULL; | |
| 62 | key ^= key >> 31; | |
| 63 | return key; | |
| 64 | } | |
| 65 | ||
| 66 | void ht_init(SimpleHTBuilder* ht, uint32_t capacity) { | |
| 67 | ht->capacity = capacity; | |
| 68 | ht->pair_count = 0; | |
| 69 | ht->h_keys = (uint64_t*)malloc(capacity * sizeof(uint64_t)); | |
| 70 | ht->h_values = (uint32_t*)malloc(capacity * sizeof(uint32_t)); | |
| 71 | ht->h_mi = (double*)malloc(capacity * sizeof(double)); | |
| 72 | memset(ht->h_keys, 0xFF, capacity * sizeof(uint64_t)); /* empty */ | |
| 73 | memset(ht->h_values, 0xFF, capacity * sizeof(uint32_t)); /* empty */ | |
| 74 | memset(ht->h_mi, 0, capacity * sizeof(double)); | |
| 75 | } | |
| 76 | ||
| 77 | void ht_insert(SimpleHTBuilder* ht, uint32_t word_a, uint32_t word_b, double mi) { | |
| 78 | uint32_t lo = (word_a <= word_b) ? word_a : word_b; | |
| 79 | uint32_t hi = (word_a <= word_b) ? word_b : word_a; | |
| 80 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 81 | uint64_t mask = (uint64_t)(ht->capacity - 1); | |
| 82 | uint64_t slot = splitmix64(key) & mask; | |
| 83 | ||
| 84 | for (int probe = 0; probe < 4096; probe++) { | |
| 85 | if (ht->h_keys[slot] == 0xFFFFFFFFFFFFFFFFULL) { | |
| 86 | ht->h_keys[slot] = key; | |
| 87 | ht->h_values[slot] = ht->pair_count; | |
| 88 | ht->h_mi[ht->pair_count] = mi; | |
| 89 | ht->pair_count++; | |
| 90 | return; | |
| 91 | } | |
| 92 | slot = (slot + 1) & mask; | |
| 93 | } | |
| 94 | printf("ERROR: hash table full\n"); | |
| 95 | } | |
| 96 | ||
| 97 | void ht_upload(SimpleHTBuilder* ht) { | |
| 98 | cudaMalloc(&ht->d_keys, ht->capacity * sizeof(uint64_t)); | |
| 99 | cudaMalloc(&ht->d_values, ht->capacity * sizeof(uint32_t)); | |
| 100 | cudaMalloc(&ht->d_mi, ht->pair_count * sizeof(double)); | |
| 101 | cudaMemcpy(ht->d_keys, ht->h_keys, ht->capacity * sizeof(uint64_t), cudaMemcpyHostToDevice); | |
| 102 | cudaMemcpy(ht->d_values, ht->h_values, ht->capacity * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 103 | cudaMemcpy(ht->d_mi, ht->h_mi, ht->pair_count * sizeof(double), cudaMemcpyHostToDevice); | |
| 104 | } | |
| 105 | ||
| 106 | void ht_free(SimpleHTBuilder* ht) { | |
| 107 | free(ht->h_keys); free(ht->h_values); free(ht->h_mi); | |
| 108 | cudaFree(ht->d_keys); cudaFree(ht->d_values); cudaFree(ht->d_mi); | |
| 109 | } | |
| 110 | ||
| 111 | /* ─── Helper: Check if an edge exists in a parse result ─── */ | |
| 112 | ||
| 113 | int has_edge(uint32_t* h_ea, uint32_t* h_eb, uint32_t count, | |
| 114 | uint32_t a, uint32_t b) { | |
| 115 | uint32_t lo = (a <= b) ? a : b; | |
| 116 | uint32_t hi = (a <= b) ? b : a; | |
| 117 | for (uint32_t i = 0; i < count; i++) { | |
| 118 | uint32_t elo = (h_ea[i] <= h_eb[i]) ? h_ea[i] : h_eb[i]; | |
| 119 | uint32_t ehi = (h_ea[i] <= h_eb[i]) ? h_eb[i] : h_ea[i]; | |
| 120 | if (elo == lo && ehi == hi) return 1; | |
| 121 | } | |
| 122 | return 0; | |
| 123 | } | |
| 124 | ||
| 125 | /* ─── T5: Grammar parse (3 sentences) ─── */ | |
| 126 | ||
| 127 | int test_t5_grammar_parse() { | |
| 128 | printf("T5: Grammar parse (3 sentences)\n"); | |
| 129 | ||
| 130 | /* Words: 0=the, 1=cat, 2=sat, 3=on, 4=mat, 5=dog, 6=ran, 7=fast | |
| 131 | * | |
| 132 | * Known word-pair MI values (higher = stronger association): | |
| 133 | * (the, cat)=3.0 (the, dog)=2.8 (the, mat)=2.5 | |
| 134 | * (cat, sat)=4.0 (dog, ran)=3.5 | |
| 135 | * (sat, on)=3.2 (on, mat)=2.0 | |
| 136 | * (ran, fast)=3.8 | |
| 137 | * All other pairs: 0.5 (low background MI) | |
| 138 | * | |
| 139 | * Sentence 1: "the cat sat on mat" = words [0,1,2,3,4] | |
| 140 | * Expected MST: (cat,sat)=4.0 → (sat,on)=3.2 → (the,cat)=3.0 → (the,mat)=2.5 | |
| 141 | * (connects all 5 words with 4 highest-MI edges that don't cycle) | |
| 142 | * | |
| 143 | * Sentence 2: "the dog ran fast" = words [0,5,6,7] | |
| 144 | * Expected MST: (ran,fast)=3.8 → (dog,ran)=3.5 → (the,dog)=2.8 | |
| 145 | * | |
| 146 | * Sentence 3: "cat dog" = words [1,5] | |
| 147 | * Expected MST: (cat,dog)=0.5 (only pair) | |
| 148 | */ | |
| 149 | ||
| 150 | /* Build word-pair MI hash table */ | |
| 151 | SimpleHTBuilder ht; | |
| 152 | ht_init(&ht, 256); | |
| 153 | ||
| 154 | /* Strong associations */ | |
| 155 | ht_insert(&ht, 0, 1, 3.0); /* the-cat */ | |
| 156 | ht_insert(&ht, 0, 5, 2.8); /* the-dog */ | |
| 157 | ht_insert(&ht, 0, 4, 2.5); /* the-mat */ | |
| 158 | ht_insert(&ht, 1, 2, 4.0); /* cat-sat */ | |
| 159 | ht_insert(&ht, 5, 6, 3.5); /* dog-ran */ | |
| 160 | ht_insert(&ht, 2, 3, 3.2); /* sat-on */ | |
| 161 | ht_insert(&ht, 3, 4, 2.0); /* on-mat */ | |
| 162 | ht_insert(&ht, 6, 7, 3.8); /* ran-fast */ | |
| 163 | ||
| 164 | /* Background MI for remaining pairs (low) */ | |
| 165 | for (uint32_t i = 0; i < 8; i++) { | |
| 166 | for (uint32_t j = i + 1; j < 8; j++) { | |
| 167 | /* Skip if already inserted */ | |
| 168 | uint64_t key = ((uint64_t)i << 32) | (uint64_t)j; | |
| 169 | int found = 0; | |
| 170 | for (uint32_t s = 0; s < ht.capacity; s++) { | |
| 171 | if (ht.h_keys[s] == key) { found = 1; break; } | |
| 172 | } | |
| 173 | if (!found) ht_insert(&ht, i, j, 0.5); | |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | ht_upload(&ht); | |
| 178 | ||
| 179 | /* Build sentence arrays */ | |
| 180 | uint32_t h_words[] = {0,1,2,3,4, 0,5,6,7, 1,5}; | |
| 181 | uint32_t h_lengths[] = {5, 4, 2}; | |
| 182 | uint32_t h_offsets[] = {0, 5, 9}; | |
| 183 | uint32_t num_sentences = 3; | |
| 184 | ||
| 185 | uint32_t *d_words, *d_lengths, *d_offsets; | |
| 186 | cudaMalloc(&d_words, 11 * sizeof(uint32_t)); | |
| 187 | cudaMalloc(&d_lengths, 3 * sizeof(uint32_t)); | |
| 188 | cudaMalloc(&d_offsets, 3 * sizeof(uint32_t)); | |
| 189 | cudaMemcpy(d_words, h_words, 11 * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 190 | cudaMemcpy(d_lengths, h_lengths, 3 * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 191 | cudaMemcpy(d_offsets, h_offsets, 3 * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 192 | ||
| 193 | /* No classes (all unclassified) — word-only parse == grammar parse */ | |
| 194 | uint32_t h_class_id[8]; | |
| 195 | for (int i = 0; i < 8; i++) h_class_id[i] = 0xFFFFFFFFU; | |
| 196 | uint32_t* d_class_id; | |
| 197 | cudaMalloc(&d_class_id, 8 * sizeof(uint32_t)); | |
| 198 | cudaMemcpy(d_class_id, h_class_id, 8 * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 199 | ||
| 200 | /* Empty class MI table */ | |
| 201 | uint32_t class_ht_cap = 64; | |
| 202 | uint64_t* d_class_keys; | |
| 203 | double* d_class_mi; | |
| 204 | cudaMalloc(&d_class_keys, class_ht_cap * sizeof(uint64_t)); | |
| 205 | cudaMalloc(&d_class_mi, class_ht_cap * sizeof(double)); | |
| 206 | cudaMemset(d_class_keys, 0xFF, class_ht_cap * sizeof(uint64_t)); | |
| 207 | cudaMemset(d_class_mi, 0, class_ht_cap * sizeof(double)); | |
| 208 | ||
| 209 | /* Output buffers */ | |
| 210 | uint32_t pe = num_sentences * MAX_TREE_EDGES; | |
| 211 | uint32_t *d_wp_a, *d_wp_b, *d_wp_cnt, *d_gp_a, *d_gp_b, *d_gp_cnt; | |
| 212 | double *d_wp_mi, *d_gp_mi, *d_surprise; | |
| 213 | cudaMalloc(&d_wp_a, pe * sizeof(uint32_t)); | |
| 214 | cudaMalloc(&d_wp_b, pe * sizeof(uint32_t)); | |
| 215 | cudaMalloc(&d_wp_mi, pe * sizeof(double)); | |
| 216 | cudaMalloc(&d_wp_cnt, num_sentences * sizeof(uint32_t)); | |
| 217 | cudaMalloc(&d_gp_a, pe * sizeof(uint32_t)); | |
| 218 | cudaMalloc(&d_gp_b, pe * sizeof(uint32_t)); | |
| 219 | cudaMalloc(&d_gp_mi, pe * sizeof(double)); | |
| 220 | cudaMalloc(&d_gp_cnt, num_sentences * sizeof(uint32_t)); | |
| 221 | cudaMalloc(&d_surprise, num_sentences * sizeof(double)); | |
| 222 | ||
| 223 | /* Run */ | |
| 224 | spanning_forest_run( | |
| 225 | d_words, d_lengths, d_offsets, num_sentences, | |
| 226 | d_class_id, | |
| 227 | d_class_keys, d_class_mi, class_ht_cap, | |
| 228 | ht.d_keys, ht.d_values, ht.d_mi, ht.capacity, | |
| 229 | d_wp_a, d_wp_b, d_wp_mi, d_wp_cnt, | |
| 230 | d_gp_a, d_gp_b, d_gp_mi, d_gp_cnt, | |
| 231 | d_surprise); | |
| 232 | ||
| 233 | /* Read results */ | |
| 234 | uint32_t h_wp_count[3], h_gp_count[3]; | |
| 235 | cudaMemcpy(h_wp_count, d_wp_cnt, 3 * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 236 | cudaMemcpy(h_gp_count, d_gp_cnt, 3 * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 237 | ||
| 238 | int pass = 1; | |
| 239 | ||
| 240 | /* Check edge counts */ | |
| 241 | printf(" Word-parse edge counts: %u %u %u (expected 4 3 1)\n", | |
| 242 | h_wp_count[0], h_wp_count[1], h_wp_count[2]); | |
| 243 | if (h_wp_count[0] != 4) { printf(" FAIL: sent 0 expected 4 edges\n"); pass = 0; } | |
| 244 | if (h_wp_count[1] != 3) { printf(" FAIL: sent 1 expected 3 edges\n"); pass = 0; } | |
| 245 | if (h_wp_count[2] != 1) { printf(" FAIL: sent 2 expected 1 edge\n"); pass = 0; } | |
| 246 | ||
| 247 | /* Check specific edges in sentence 0 word-parse */ | |
| 248 | uint32_t h_ea[MAX_TREE_EDGES * 3], h_eb[MAX_TREE_EDGES * 3]; | |
| 249 | double h_emi[MAX_TREE_EDGES * 3]; | |
| 250 | cudaMemcpy(h_ea, d_wp_a, pe * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 251 | cudaMemcpy(h_eb, d_wp_b, pe * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 252 | cudaMemcpy(h_emi, d_wp_mi, pe * sizeof(double), cudaMemcpyDeviceToHost); | |
| 253 | ||
| 254 | printf(" Sentence 0 word-parse edges:\n"); | |
| 255 | for (uint32_t i = 0; i < h_wp_count[0]; i++) { | |
| 256 | printf(" (%u,%u) MI=%.1f\n", h_ea[i], h_eb[i], h_emi[i]); | |
| 257 | } | |
| 258 | ||
| 259 | /* Sentence 0 MST should contain: (1,2)=4.0, (2,3)=3.2, (0,1)=3.0, (0,4)=2.5 or (3,4)=2.0 | |
| 260 | * Actually the 4 highest MI edges forming a tree on {0,1,2,3,4}: | |
| 261 | * (1,2)=4.0 → {1,2} | |
| 262 | * (2,3)=3.2 → {1,2,3} | |
| 263 | * (0,1)=3.0 → {0,1,2,3} | |
| 264 | * Now need to connect 4: (0,4)=2.5 or (3,4)=2.0 → pick (0,4)=2.5 | |
| 265 | */ | |
| 266 | if (!has_edge(h_ea, h_eb, h_wp_count[0], 1, 2)) { | |
| 267 | printf(" FAIL: sent 0 missing edge (1,2)\n"); pass = 0; | |
| 268 | } | |
| 269 | if (!has_edge(h_ea, h_eb, h_wp_count[0], 2, 3)) { | |
| 270 | printf(" FAIL: sent 0 missing edge (2,3)\n"); pass = 0; | |
| 271 | } | |
| 272 | if (!has_edge(h_ea, h_eb, h_wp_count[0], 0, 1)) { | |
| 273 | printf(" FAIL: sent 0 missing edge (0,1)\n"); pass = 0; | |
| 274 | } | |
| 275 | /* Either (0,4)=2.5 or (3,4)=2.0 connects word 4 */ | |
| 276 | if (!has_edge(h_ea, h_eb, h_wp_count[0], 0, 4) && | |
| 277 | !has_edge(h_ea, h_eb, h_wp_count[0], 3, 4)) { | |
| 278 | printf(" FAIL: sent 0 word 4 not connected\n"); pass = 0; | |
| 279 | } | |
| 280 | ||
| 281 | /* Sentence 1: (6,7)=3.8 (5,6)=3.5 (0,5)=2.8 */ | |
| 282 | uint32_t s1_base = MAX_TREE_EDGES; | |
| 283 | printf(" Sentence 1 word-parse edges:\n"); | |
| 284 | for (uint32_t i = 0; i < h_wp_count[1]; i++) { | |
| 285 | printf(" (%u,%u) MI=%.1f\n", h_ea[s1_base+i], h_eb[s1_base+i], h_emi[s1_base+i]); | |
| 286 | } | |
| 287 | if (!has_edge(&h_ea[s1_base], &h_eb[s1_base], h_wp_count[1], 6, 7)) { | |
| 288 | printf(" FAIL: sent 1 missing edge (6,7)\n"); pass = 0; | |
| 289 | } | |
| 290 | if (!has_edge(&h_ea[s1_base], &h_eb[s1_base], h_wp_count[1], 5, 6)) { | |
| 291 | printf(" FAIL: sent 1 missing edge (5,6)\n"); pass = 0; | |
| 292 | } | |
| 293 | ||
| 294 | /* Since there are no classes, grammar parse == word parse */ | |
| 295 | printf(" Grammar-parse edge counts: %u %u %u\n", | |
| 296 | h_gp_count[0], h_gp_count[1], h_gp_count[2]); | |
| 297 | if (h_gp_count[0] != h_wp_count[0] || | |
| 298 | h_gp_count[1] != h_wp_count[1] || | |
| 299 | h_gp_count[2] != h_wp_count[2]) { | |
| 300 | printf(" FAIL: grammar parse count differs from word parse (no classes)\n"); | |
| 301 | pass = 0; | |
| 302 | } | |
| 303 | ||
| 304 | /* Cleanup */ | |
| 305 | cudaFree(d_words); cudaFree(d_lengths); cudaFree(d_offsets); | |
| 306 | cudaFree(d_class_id); cudaFree(d_class_keys); cudaFree(d_class_mi); | |
| 307 | cudaFree(d_wp_a); cudaFree(d_wp_b); cudaFree(d_wp_mi); cudaFree(d_wp_cnt); | |
| 308 | cudaFree(d_gp_a); cudaFree(d_gp_b); cudaFree(d_gp_mi); cudaFree(d_gp_cnt); | |
| 309 | cudaFree(d_surprise); | |
| 310 | ht_free(&ht); | |
| 311 | ||
| 312 | printf(" T5: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 313 | return pass; | |
| 314 | } | |
| 315 | ||
| 316 | /* ─── T6: Surprise (grammar vs word parse divergence) ─── */ | |
| 317 | ||
| 318 | int test_t6_surprise() { | |
| 319 | printf("T6: Grammar surprise\n"); | |
| 320 | ||
| 321 | /* Setup: 2 sentences, 6 words | |
| 322 | * Words: 0,1,2,3,4,5 | |
| 323 | * Classes: 0,1 → class 0; 2,3 → class 1; 4,5 → unclassified | |
| 324 | * | |
| 325 | * Word-pair MI: | |
| 326 | * (0,2)=5.0 (0,3)=4.5 (1,2)=4.8 (1,3)=4.2 — cross-class, high | |
| 327 | * (0,4)=1.0 (1,5)=1.2 — to unclassified, low | |
| 328 | * (2,4)=3.0 (3,5)=2.5 — to unclassified | |
| 329 | * (4,5)=0.8 — between unclassified | |
| 330 | * All others: 0.3 | |
| 331 | * | |
| 332 | * Class-pair MI: | |
| 333 | * (class0, class1) = 4.0 — class-level MI (average of cross-class pairs) | |
| 334 | * | |
| 335 | * Sentence 1: "0 2 4" → words [0,2,4] | |
| 336 | * Word-parse MST: (0,2)=5.0, (2,4)=3.0 | |
| 337 | * Grammar-parse: For (0,2): class(0)=0, class(2)=1 → class MI=4.0 | |
| 338 | * For (0,4): class(4)=none → word MI=1.0 | |
| 339 | * For (2,4): class(4)=none → word MI=3.0 | |
| 340 | * Grammar MST: (0,2)=4.0 [class], (2,4)=3.0 [word] | |
| 341 | * Both parses have same structure → surprise ≈ 0 | |
| 342 | * | |
| 343 | * Sentence 2: "0 4 5" → words [0,4,5] | |
| 344 | * Word-parse MST: (0,4)=1.0, (4,5)=0.8 | |
| 345 | * (but also (0,5)=0.3 — even lower) | |
| 346 | * Grammar-parse: No class MI useful (4,5 unclassified) | |
| 347 | * Falls back to word MI: same as word-parse | |
| 348 | * Surprise ≈ 0 (same parse) | |
| 349 | * | |
| 350 | * Let me make a more interesting case with divergent parses: | |
| 351 | * | |
| 352 | * Sentence 3: "0 2 3" → words [0,2,3] | |
| 353 | * Word-pair MI: (0,2)=5.0, (0,3)=4.5, (2,3)=0.3 | |
| 354 | * Word-parse MST: (0,2)=5.0, (0,3)=4.5 → star centered on 0 | |
| 355 | * | |
| 356 | * Grammar-parse: (0,2) → class MI(0,1)=4.0 | |
| 357 | * (0,3) → class MI(0,1)=4.0 | |
| 358 | * (2,3) → same class (1,1) → word MI=0.3 | |
| 359 | * Grammar MST: (0,2)=4.0, (0,3)=4.0 → same structure! | |
| 360 | * Surprise ≈ 0 (same topology) | |
| 361 | * | |
| 362 | * To get actual surprise, we need grammar-parse to pick DIFFERENT edges | |
| 363 | * than word-parse. This happens when class-MI reshuffles preferences. | |
| 364 | */ | |
| 365 | ||
| 366 | /* Let me design a clear case: | |
| 367 | * Words: 0,1,2,3 | |
| 368 | * Classes: 0 → class A, 1 → class A, 2 → class B, 3 → class C | |
| 369 | * | |
| 370 | * Word-pair MI: | |
| 371 | * (0,1)=1.0 (same class, not used for class MI) | |
| 372 | * (0,2)=5.0 (A→B, strong word MI) | |
| 373 | * (0,3)=2.0 (A→C, weak word MI) | |
| 374 | * (1,2)=2.5 (A→B, moderate word MI) | |
| 375 | * (1,3)=6.0 (A→C, very strong word MI) | |
| 376 | * (2,3)=3.0 (B→C, moderate word MI) | |
| 377 | * | |
| 378 | * Class-pair MI (averages): | |
| 379 | * (A,B): avg of (0,2)=5.0, (1,2)=2.5 → 3.75 | |
| 380 | * (A,C): avg of (0,3)=2.0, (1,3)=6.0 → 4.0 | |
| 381 | * (B,C): 3.0 | |
| 382 | * | |
| 383 | * Sentence: "0 1 2 3" → all 4 words | |
| 384 | * | |
| 385 | * Word-parse MST (greedy on word MI): | |
| 386 | * (1,3)=6.0 → connect 1,3 | |
| 387 | * (0,2)=5.0 → connect 0,2 | |
| 388 | * Next highest non-cycle: (2,3)=3.0 or (1,2)=2.5 or (0,3)=2.0 | |
| 389 | * → (2,3)=3.0 connects {0,2} with {1,3} | |
| 390 | * Result: edges (1,3), (0,2), (2,3) | |
| 391 | * | |
| 392 | * Grammar-parse MST (greedy on class MI, fallback word MI): | |
| 393 | * Evaluate each pair's "grammar MI": | |
| 394 | * (0,1): same class A → word MI 1.0 | |
| 395 | * (0,2): class(A,B) → 3.75 | |
| 396 | * (0,3): class(A,C) → 4.0 | |
| 397 | * (1,2): class(A,B) → 3.75 | |
| 398 | * (1,3): class(A,C) → 4.0 | |
| 399 | * (2,3): class(B,C) → 3.0 | |
| 400 | * | |
| 401 | * Grammar MST: (0,3)=4.0 or (1,3)=4.0 (tie!) → pick (0,3) [lower index wins in scan] | |
| 402 | * Actually: greedy scans all pairs, picks highest. (0,3) and (1,3) both = 4.0. | |
| 403 | * Scan order: i=0,j=3 comes before i=1,j=3 → picks (0,3)=4.0 | |
| 404 | * Next: (1,3)=4.0 → connect 1 with {0,3} | |
| 405 | * Next: (0,2)=3.75 or (1,2)=3.75 → (0,2)=3.75 (first in scan) connects 2 | |
| 406 | * Result: edges (0,3), (1,3), (0,2) | |
| 407 | * | |
| 408 | * Surprise: | |
| 409 | * Word-parse edges: (1,3), (0,2), (2,3) | |
| 410 | * Grammar-parse edges: (0,3), (1,3), (0,2) | |
| 411 | * | |
| 412 | * Word edges in grammar? (1,3) YES, (0,2) YES, (2,3) NO | |
| 413 | * Surprise = MI of (2,3) / length = 3.0 / 4 = 0.75 | |
| 414 | */ | |
| 415 | ||
| 416 | SimpleHTBuilder ht; | |
| 417 | ht_init(&ht, 256); | |
| 418 | ht_insert(&ht, 0, 1, 1.0); | |
| 419 | ht_insert(&ht, 0, 2, 5.0); | |
| 420 | ht_insert(&ht, 0, 3, 2.0); | |
| 421 | ht_insert(&ht, 1, 2, 2.5); | |
| 422 | ht_insert(&ht, 1, 3, 6.0); | |
| 423 | ht_insert(&ht, 2, 3, 3.0); | |
| 424 | ht_upload(&ht); | |
| 425 | ||
| 426 | /* Classes: 0→A(0), 1→A(0), 2→B(1), 3→C(2) */ | |
| 427 | uint32_t h_class_id[] = {0, 0, 1, 2}; | |
| 428 | uint32_t* d_class_id; | |
| 429 | cudaMalloc(&d_class_id, 4 * sizeof(uint32_t)); | |
| 430 | cudaMemcpy(d_class_id, h_class_id, 4 * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 431 | ||
| 432 | /* Class MI hash table: (A,B)=3.75, (A,C)=4.0, (B,C)=3.0 */ | |
| 433 | uint32_t class_ht_cap = 64; | |
| 434 | uint64_t h_ckeys[64]; | |
| 435 | double h_cmi[64]; | |
| 436 | memset(h_ckeys, 0xFF, 64 * sizeof(uint64_t)); | |
| 437 | memset(h_cmi, 0, 64 * sizeof(double)); | |
| 438 | ||
| 439 | /* Insert class pairs using same hash function */ | |
| 440 | auto insert_class_mi = [&](uint32_t ca, uint32_t cb, double mi) { | |
| 441 | uint32_t lo = (ca <= cb) ? ca : cb; | |
| 442 | uint32_t hi = (ca <= cb) ? cb : ca; | |
| 443 | uint64_t key = ((uint64_t)lo << 32) | (uint64_t)hi; | |
| 444 | uint64_t mask = class_ht_cap - 1; | |
| 445 | uint64_t slot = splitmix64(key) & mask; | |
| 446 | for (int p = 0; p < 64; p++) { | |
| 447 | if (h_ckeys[slot] == 0xFFFFFFFFFFFFFFFFULL) { | |
| 448 | h_ckeys[slot] = key; | |
| 449 | h_cmi[slot] = mi; | |
| 450 | return; | |
| 451 | } | |
| 452 | slot = (slot + 1) & mask; | |
| 453 | } | |
| 454 | }; | |
| 455 | insert_class_mi(0, 1, 3.75); /* A-B */ | |
| 456 | insert_class_mi(0, 2, 4.0); /* A-C */ | |
| 457 | insert_class_mi(1, 2, 3.0); /* B-C */ | |
| 458 | ||
| 459 | uint64_t* d_ckeys; | |
| 460 | double* d_cmi; | |
| 461 | cudaMalloc(&d_ckeys, class_ht_cap * sizeof(uint64_t)); | |
| 462 | cudaMalloc(&d_cmi, class_ht_cap * sizeof(double)); | |
| 463 | cudaMemcpy(d_ckeys, h_ckeys, class_ht_cap * sizeof(uint64_t), cudaMemcpyHostToDevice); | |
| 464 | cudaMemcpy(d_cmi, h_cmi, class_ht_cap * sizeof(double), cudaMemcpyHostToDevice); | |
| 465 | ||
| 466 | /* Sentence: [0, 1, 2, 3] */ | |
| 467 | uint32_t h_words[] = {0, 1, 2, 3}; | |
| 468 | uint32_t h_lengths[] = {4}; | |
| 469 | uint32_t h_offsets[] = {0}; | |
| 470 | uint32_t num_sentences = 1; | |
| 471 | ||
| 472 | uint32_t *d_words, *d_lengths, *d_offsets; | |
| 473 | cudaMalloc(&d_words, 4 * sizeof(uint32_t)); | |
| 474 | cudaMalloc(&d_lengths, sizeof(uint32_t)); | |
| 475 | cudaMalloc(&d_offsets, sizeof(uint32_t)); | |
| 476 | cudaMemcpy(d_words, h_words, 4 * sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 477 | cudaMemcpy(d_lengths, h_lengths, sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 478 | cudaMemcpy(d_offsets, h_offsets, sizeof(uint32_t), cudaMemcpyHostToDevice); | |
| 479 | ||
| 480 | /* Output buffers */ | |
| 481 | uint32_t pe = MAX_TREE_EDGES; | |
| 482 | uint32_t *d_wp_a, *d_wp_b, *d_wp_cnt, *d_gp_a, *d_gp_b, *d_gp_cnt; | |
| 483 | double *d_wp_mi, *d_gp_mi, *d_surprise; | |
| 484 | cudaMalloc(&d_wp_a, pe * sizeof(uint32_t)); | |
| 485 | cudaMalloc(&d_wp_b, pe * sizeof(uint32_t)); | |
| 486 | cudaMalloc(&d_wp_mi, pe * sizeof(double)); | |
| 487 | cudaMalloc(&d_wp_cnt, sizeof(uint32_t)); | |
| 488 | cudaMalloc(&d_gp_a, pe * sizeof(uint32_t)); | |
| 489 | cudaMalloc(&d_gp_b, pe * sizeof(uint32_t)); | |
| 490 | cudaMalloc(&d_gp_mi, pe * sizeof(double)); | |
| 491 | cudaMalloc(&d_gp_cnt, sizeof(uint32_t)); | |
| 492 | cudaMalloc(&d_surprise, sizeof(double)); | |
| 493 | ||
| 494 | spanning_forest_run( | |
| 495 | d_words, d_lengths, d_offsets, num_sentences, | |
| 496 | d_class_id, | |
| 497 | d_ckeys, d_cmi, class_ht_cap, | |
| 498 | ht.d_keys, ht.d_values, ht.d_mi, ht.capacity, | |
| 499 | d_wp_a, d_wp_b, d_wp_mi, d_wp_cnt, | |
| 500 | d_gp_a, d_gp_b, d_gp_mi, d_gp_cnt, | |
| 501 | d_surprise); | |
| 502 | ||
| 503 | /* Read results */ | |
| 504 | uint32_t h_wp_count, h_gp_count; | |
| 505 | cudaMemcpy(&h_wp_count, d_wp_cnt, sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 506 | cudaMemcpy(&h_gp_count, d_gp_cnt, sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 507 | ||
| 508 | uint32_t h_ea[MAX_TREE_EDGES], h_eb[MAX_TREE_EDGES]; | |
| 509 | double h_emi[MAX_TREE_EDGES]; | |
| 510 | uint32_t h_ga[MAX_TREE_EDGES], h_gb[MAX_TREE_EDGES]; | |
| 511 | double h_gmi[MAX_TREE_EDGES]; | |
| 512 | cudaMemcpy(h_ea, d_wp_a, pe * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 513 | cudaMemcpy(h_eb, d_wp_b, pe * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 514 | cudaMemcpy(h_emi, d_wp_mi, pe * sizeof(double), cudaMemcpyDeviceToHost); | |
| 515 | cudaMemcpy(h_ga, d_gp_a, pe * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 516 | cudaMemcpy(h_gb, d_gp_b, pe * sizeof(uint32_t), cudaMemcpyDeviceToHost); | |
| 517 | cudaMemcpy(h_gmi, d_gp_mi, pe * sizeof(double), cudaMemcpyDeviceToHost); | |
| 518 | ||
| 519 | double h_surprise; | |
| 520 | cudaMemcpy(&h_surprise, d_surprise, sizeof(double), cudaMemcpyDeviceToHost); | |
| 521 | ||
| 522 | int pass = 1; | |
| 523 | ||
| 524 | printf(" Word-parse (%u edges):\n", h_wp_count); | |
| 525 | for (uint32_t i = 0; i < h_wp_count; i++) | |
| 526 | printf(" (%u,%u) MI=%.1f\n", h_ea[i], h_eb[i], h_emi[i]); | |
| 527 | ||
| 528 | printf(" Grammar-parse (%u edges):\n", h_gp_count); | |
| 529 | for (uint32_t i = 0; i < h_gp_count; i++) | |
| 530 | printf(" (%u,%u) MI=%.1f\n", h_ga[i], h_gb[i], h_gmi[i]); | |
| 531 | ||
| 532 | /* Verify word-parse contains (1,3) and (0,2) */ | |
| 533 | if (!has_edge(h_ea, h_eb, h_wp_count, 1, 3)) { | |
| 534 | printf(" FAIL: word-parse missing (1,3)\n"); pass = 0; | |
| 535 | } | |
| 536 | if (!has_edge(h_ea, h_eb, h_wp_count, 0, 2)) { | |
| 537 | printf(" FAIL: word-parse missing (0,2)\n"); pass = 0; | |
| 538 | } | |
| 539 | ||
| 540 | /* Grammar-parse should contain (0,3) or (1,3) as top edges (class MI = 4.0) */ | |
| 541 | int has_ac_edge = has_edge(h_ga, h_gb, h_gp_count, 0, 3) || | |
| 542 | has_edge(h_ga, h_gb, h_gp_count, 1, 3); | |
| 543 | if (!has_ac_edge) { | |
| 544 | printf(" FAIL: grammar-parse missing A-C class edge\n"); pass = 0; | |
| 545 | } | |
| 546 | ||
| 547 | /* Verify edge counts */ | |
| 548 | if (h_wp_count != 3) { printf(" FAIL: word-parse expected 3 edges\n"); pass = 0; } | |
| 549 | if (h_gp_count != 3) { printf(" FAIL: grammar-parse expected 3 edges\n"); pass = 0; } | |
| 550 | ||
| 551 | /* Verify surprise is > 0 (parses should differ) */ | |
| 552 | printf(" Surprise: %.4f\n", h_surprise); | |
| 553 | if (h_surprise < 0.0) { | |
| 554 | printf(" FAIL: surprise should be >= 0\n"); | |
| 555 | pass = 0; | |
| 556 | } | |
| 557 | ||
| 558 | /* If parses differ, surprise should be > 0 */ | |
| 559 | /* Check if parses actually differ */ | |
| 560 | int differ = 0; | |
| 561 | for (uint32_t i = 0; i < h_wp_count; i++) { | |
| 562 | if (!has_edge(h_ga, h_gb, h_gp_count, h_ea[i], h_eb[i])) { | |
| 563 | differ = 1; | |
| 564 | printf(" Word edge (%u,%u) MI=%.1f NOT in grammar-parse → contributes to surprise\n", | |
| 565 | h_ea[i], h_eb[i], h_emi[i]); | |
| 566 | } | |
| 567 | } | |
| 568 | if (differ && h_surprise <= 0.0) { | |
| 569 | printf(" FAIL: parses differ but surprise = 0\n"); | |
| 570 | pass = 0; | |
| 571 | } | |
| 572 | if (!differ && h_surprise > 0.0) { | |
| 573 | printf(" NOTE: parses identical, surprise = %.4f (should be 0)\n", h_surprise); | |
| 574 | } | |
| 575 | if (!differ) { | |
| 576 | printf(" NOTE: parses are identical — no surprise (class MI didn't change MST)\n"); | |
| 577 | printf(" This can happen when class MI preserves the same edge ordering\n"); | |
| 578 | } | |
| 579 | ||
| 580 | /* Cleanup */ | |
| 581 | cudaFree(d_words); cudaFree(d_lengths); cudaFree(d_offsets); | |
| 582 | cudaFree(d_class_id); cudaFree(d_ckeys); cudaFree(d_cmi); | |
| 583 | cudaFree(d_wp_a); cudaFree(d_wp_b); cudaFree(d_wp_mi); cudaFree(d_wp_cnt); | |
| 584 | cudaFree(d_gp_a); cudaFree(d_gp_b); cudaFree(d_gp_mi); cudaFree(d_gp_cnt); | |
| 585 | cudaFree(d_surprise); | |
| 586 | ht_free(&ht); | |
| 587 | ||
| 588 | printf(" T6: %s\n\n", pass ? "PASS" : "FAIL"); | |
| 589 | return pass; | |
| 590 | } | |
| 591 | ||
| 592 | /* ─── Main ─── */ | |
| 593 | ||
| 594 | int main() { | |
| 595 | printf("=== Spanning Forest Tests ===\n\n"); | |
| 596 | ||
| 597 | int t5 = test_t5_grammar_parse(); | |
| 598 | int t6 = test_t6_surprise(); | |
| 599 | ||
| 600 | printf("=== Results ===\n"); | |
| 601 | printf("T5 (Grammar parse): %s\n", t5 ? "PASS" : "FAIL"); | |
| 602 | printf("T6 (Surprise): %s\n", t6 ? "PASS" : "FAIL"); | |
| 603 | printf("Overall: %s\n", (t5 && t6) ? "ALL PASSED" : "SOME FAILED"); | |
| 604 | ||
| 605 | return (t5 && t6) ? 0 : 1; | |
| 606 | } |
| … | … | @@ -1,767 +0,0 @@ |
| 1 | /* | |
| 2 | * test-substitute.c -- Test GPU class substitution kernel | |
| 3 | * | |
| 4 | * Compile: gcc -O2 -o test-substitute test-substitute.c -lOpenCL -lm | |
| 5 | * Run: ./test-substitute | |
| 6 | * | |
| 7 | * Tests: | |
| 8 | * 1. Class assignment — batch set word_class_id | |
| 9 | * 2. Pair substitution — replace word indices (no dedup) | |
| 10 | * 3. Pair merge — duplicate pairs after substitution get merged | |
| 11 | * 4. Self-pair elimination — both words → same class → dropped | |
| 12 | * 5. Section word substitution | |
| 13 | * 6. Benchmark: 100K pairs, 1000 class assignments, substitute + rebuild | |
| 14 | */ | |
| 15 | ||
| 16 | #include <stdio.h> | |
| 17 | #include <stdlib.h> | |
| 18 | #include <stdint.h> | |
| 19 | #include <string.h> | |
| 20 | #include <math.h> | |
| 21 | #include <time.h> | |
| 22 | #include <CL/cl.h> | |
| 23 | ||
| 24 | /* ─── Pool capacities ─── */ | |
| 25 | ||
| 26 | #define WORD_CAPACITY (128 * 1024) | |
| 27 | #define PAIR_CAPACITY (4 * 1024 * 1024) | |
| 28 | #define SECTION_CAPACITY (1024 * 1024) | |
| 29 | #define WORD_HT_CAPACITY (256 * 1024) | |
| 30 | #define PAIR_HT_CAPACITY (8 * 1024 * 1024) | |
| 31 | #define SECTION_HT_CAPACITY (2 * 1024 * 1024) | |
| 32 | ||
| 33 | #define HT_EMPTY_KEY 0xFFFFFFFFFFFFFFFFULL | |
| 34 | #define HT_EMPTY_VALUE 0xFFFFFFFFU | |
| 35 | ||
| 36 | /* ─── Error checking ─── */ | |
| 37 | ||
| 38 | #define CL_CHECK(err, msg) do { \ | |
| 39 | if ((err) != CL_SUCCESS) { \ | |
| 40 | fprintf(stderr, "OpenCL error %d at %s:%d: %s\n", \ | |
| 41 | (err), __FILE__, __LINE__, (msg)); \ | |
| 42 | exit(1); \ | |
| 43 | } \ | |
| 44 | } while(0) | |
| 45 | ||
| 46 | /* ─── Read file ─── */ | |
| 47 | ||
| 48 | char* read_file(const char* path, size_t* len) | |
| 49 | { | |
| 50 | FILE* f = fopen(path, "r"); | |
| 51 | if (!f) { fprintf(stderr, "Cannot open %s\n", path); exit(1); } | |
| 52 | fseek(f, 0, SEEK_END); | |
| 53 | *len = ftell(f); | |
| 54 | fseek(f, 0, SEEK_SET); | |
| 55 | char* buf = malloc(*len + 1); | |
| 56 | size_t n = fread(buf, 1, *len, f); | |
| 57 | buf[n] = '\0'; | |
| 58 | *len = n; | |
| 59 | fclose(f); | |
| 60 | return buf; | |
| 61 | } | |
| 62 | ||
| 63 | /* ─── Timing ─── */ | |
| 64 | ||
| 65 | double now_ms(void) | |
| 66 | { | |
| 67 | struct timespec ts; | |
| 68 | clock_gettime(CLOCK_MONOTONIC, &ts); | |
| 69 | return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0; | |
| 70 | } | |
| 71 | ||
| 72 | /* ─── Main ─── */ | |
| 73 | ||
| 74 | int main(int argc, char** argv) | |
| 75 | { | |
| 76 | cl_int err; | |
| 77 | int pass_count = 0, fail_count = 0; | |
| 78 | ||
| 79 | printf("=== GPU Class Substitution Test ===\n\n"); | |
| 80 | ||
| 81 | /* ─── OpenCL setup ─── */ | |
| 82 | ||
| 83 | cl_platform_id platform; | |
| 84 | err = clGetPlatformIDs(1, &platform, NULL); | |
| 85 | CL_CHECK(err, "platform"); | |
| 86 | ||
| 87 | cl_device_id device; | |
| 88 | err = clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL); | |
| 89 | CL_CHECK(err, "device"); | |
| 90 | ||
| 91 | char dev_name[256]; | |
| 92 | clGetDeviceInfo(device, CL_DEVICE_NAME, sizeof(dev_name), dev_name, NULL); | |
| 93 | printf("GPU: %s\n", dev_name); | |
| 94 | ||
| 95 | cl_context ctx = clCreateContext(NULL, 1, &device, NULL, NULL, &err); | |
| 96 | CL_CHECK(err, "context"); | |
| 97 | ||
| 98 | cl_command_queue queue = clCreateCommandQueue(ctx, device, 0, &err); | |
| 99 | CL_CHECK(err, "queue"); | |
| 100 | ||
| 101 | /* ─── Load and concatenate kernel sources ─── */ | |
| 102 | ||
| 103 | size_t len_ht, len_as, len_sub; | |
| 104 | char* src_ht = read_file("opencog/gpu/gpu-hashtable.cl", &len_ht); | |
| 105 | char* src_as = read_file("opencog/gpu/gpu-atomspace.cl", &len_as); | |
| 106 | char* src_sub = read_file("opencog/gpu/gpu-substitute.cl", &len_sub); | |
| 107 | ||
| 108 | size_t total_len = len_ht + 1 + len_as + 1 + len_sub; | |
| 109 | char* combined = malloc(total_len + 1); | |
| 110 | memcpy(combined, src_ht, len_ht); | |
| 111 | combined[len_ht] = '\n'; | |
| 112 | memcpy(combined + len_ht + 1, src_as, len_as); | |
| 113 | combined[len_ht + 1 + len_as] = '\n'; | |
| 114 | memcpy(combined + len_ht + 1 + len_as + 1, src_sub, len_sub); | |
| 115 | combined[total_len] = '\0'; | |
| 116 | ||
| 117 | cl_program program = clCreateProgramWithSource(ctx, 1, | |
| 118 | (const char**)&combined, &total_len, &err); | |
| 119 | CL_CHECK(err, "create program"); | |
| 120 | ||
| 121 | char build_opts[512]; | |
| 122 | snprintf(build_opts, sizeof(build_opts), | |
| 123 | "-cl-std=CL1.2 " | |
| 124 | "-DWORD_CAPACITY=%d " | |
| 125 | "-DPAIR_CAPACITY=%d " | |
| 126 | "-DSECTION_CAPACITY=%d " | |
| 127 | "-DWORD_HT_CAPACITY=%d " | |
| 128 | "-DPAIR_HT_CAPACITY=%d " | |
| 129 | "-DSECTION_HT_CAPACITY=%d", | |
| 130 | WORD_CAPACITY, PAIR_CAPACITY, SECTION_CAPACITY, | |
| 131 | WORD_HT_CAPACITY, PAIR_HT_CAPACITY, SECTION_HT_CAPACITY); | |
| 132 | ||
| 133 | err = clBuildProgram(program, 1, &device, build_opts, NULL, NULL); | |
| 134 | if (err != CL_SUCCESS) { | |
| 135 | char log[16384]; | |
| 136 | clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, | |
| 137 | sizeof(log), log, NULL); | |
| 138 | fprintf(stderr, "Build error:\n%s\n", log); | |
| 139 | return 1; | |
| 140 | } | |
| 141 | printf("Kernels compiled successfully\n\n"); | |
| 142 | ||
| 143 | /* ─── Create kernels ─── */ | |
| 144 | ||
| 145 | cl_kernel k_assign = clCreateKernel(program, "assign_classes", &err); | |
| 146 | CL_CHECK(err, "kernel assign_classes"); | |
| 147 | cl_kernel k_sub_pair = clCreateKernel(program, "substitute_pairs", &err); | |
| 148 | CL_CHECK(err, "kernel substitute_pairs"); | |
| 149 | cl_kernel k_rebuild = clCreateKernel(program, "rebuild_pair_index", &err); | |
| 150 | CL_CHECK(err, "kernel rebuild_pair_index"); | |
| 151 | cl_kernel k_sub_sec = clCreateKernel(program, "substitute_section_words", &err); | |
| 152 | CL_CHECK(err, "kernel substitute_section_words"); | |
| 153 | ||
| 154 | size_t local_size = 256; | |
| 155 | ||
| 156 | /* ─── Allocate GPU buffers ─── */ | |
| 157 | ||
| 158 | printf("Allocating GPU buffers...\n"); | |
| 159 | uint32_t zero = 0; | |
| 160 | uint8_t pat_ff = 0xFF; | |
| 161 | uint8_t pat_00 = 0x00; | |
| 162 | ||
| 163 | /* Word class IDs */ | |
| 164 | cl_mem word_class_id = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 165 | sizeof(uint32_t) * WORD_CAPACITY, NULL, &err); | |
| 166 | CL_CHECK(err, "word_class_id"); | |
| 167 | clEnqueueFillBuffer(queue, word_class_id, &pat_00, 1, 0, | |
| 168 | sizeof(uint32_t) * WORD_CAPACITY, 0, NULL, NULL); | |
| 169 | ||
| 170 | /* Pair pool */ | |
| 171 | cl_mem pair_word_a = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 172 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 173 | cl_mem pair_word_b = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 174 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 175 | cl_mem pair_count = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 176 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 177 | cl_mem pair_mi = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 178 | sizeof(double) * PAIR_CAPACITY, NULL, &err); | |
| 179 | cl_mem pair_flags = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 180 | sizeof(uint32_t) * PAIR_CAPACITY, NULL, &err); | |
| 181 | ||
| 182 | /* Pair hash table */ | |
| 183 | cl_mem pht_keys = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 184 | sizeof(uint64_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 185 | cl_mem pht_values = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 186 | sizeof(uint32_t) * PAIR_HT_CAPACITY, NULL, &err); | |
| 187 | ||
| 188 | /* Section pool (for test 5) */ | |
| 189 | cl_mem sec_word = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 190 | sizeof(uint32_t) * SECTION_CAPACITY, NULL, &err); | |
| 191 | cl_mem sec_count_buf = clCreateBuffer(ctx, CL_MEM_READ_WRITE, | |
| 192 | sizeof(double) * SECTION_CAPACITY, NULL, &err); | |
| 193 | ||
| 194 | /* Counters */ | |
| 195 | cl_mem num_changed = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 196 | sizeof(uint32_t), &zero, &err); | |
| 197 | cl_mem num_eliminated = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 198 | sizeof(uint32_t), &zero, &err); | |
| 199 | cl_mem num_merged = clCreateBuffer(ctx, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, | |
| 200 | sizeof(uint32_t), &zero, &err); | |
| 201 | ||
| 202 | clFinish(queue); | |
| 203 | printf("GPU buffers ready\n\n"); | |
| 204 | ||
| 205 | /* ═══════════════════════════════════════════════════════════════ | |
| 206 | * TEST 1: Class assignment | |
| 207 | * | |
| 208 | * Assign: word 10 → class 100, word 20 → class 100 | |
| 209 | * Verify word_class_id[10] = 100, word_class_id[20] = 100 | |
| 210 | * ═══════════════════════════════════════════════════════════════ */ | |
| 211 | ||
| 212 | printf("--- Test 1: Class assignment ---\n"); | |
| 213 | ||
| 214 | /* Reset word_class_id */ | |
| 215 | clEnqueueFillBuffer(queue, word_class_id, &pat_00, 1, 0, | |
| 216 | sizeof(uint32_t) * WORD_CAPACITY, 0, NULL, NULL); | |
| 217 | clFinish(queue); | |
| 218 | ||
| 219 | { | |
| 220 | uint32_t word_indices[] = {10, 20}; | |
| 221 | uint32_t class_ids[] = {100, 100}; | |
| 222 | cl_uint na = 2; | |
| 223 | ||
| 224 | cl_mem d_widx = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 225 | sizeof(uint32_t) * na, word_indices, &err); | |
| 226 | cl_mem d_cids = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 227 | sizeof(uint32_t) * na, class_ids, &err); | |
| 228 | ||
| 229 | clSetKernelArg(k_assign, 0, sizeof(cl_mem), &word_class_id); | |
| 230 | clSetKernelArg(k_assign, 1, sizeof(cl_mem), &d_widx); | |
| 231 | clSetKernelArg(k_assign, 2, sizeof(cl_mem), &d_cids); | |
| 232 | clSetKernelArg(k_assign, 3, sizeof(cl_uint), &na); | |
| 233 | ||
| 234 | size_t gs = ((na + local_size - 1) / local_size) * local_size; | |
| 235 | err = clEnqueueNDRangeKernel(queue, k_assign, 1, NULL, | |
| 236 | &gs, &local_size, 0, NULL, NULL); | |
| 237 | CL_CHECK(err, "enqueue assign"); | |
| 238 | clFinish(queue); | |
| 239 | ||
| 240 | /* Read back */ | |
| 241 | uint32_t h_cls[32]; | |
| 242 | clEnqueueReadBuffer(queue, word_class_id, CL_TRUE, 0, | |
| 243 | sizeof(uint32_t) * 32, h_cls, 0, NULL, NULL); | |
| 244 | ||
| 245 | printf(" word_class_id[10] = %u (expected 100)\n", h_cls[10]); | |
| 246 | printf(" word_class_id[20] = %u (expected 100)\n", h_cls[20]); | |
| 247 | printf(" word_class_id[0] = %u (expected 0)\n", h_cls[0]); | |
| 248 | printf(" word_class_id[15] = %u (expected 0)\n", h_cls[15]); | |
| 249 | ||
| 250 | int pass = (h_cls[10] == 100) && (h_cls[20] == 100) && | |
| 251 | (h_cls[0] == 0) && (h_cls[15] == 0); | |
| 252 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 253 | if (pass) pass_count++; else fail_count++; | |
| 254 | ||
| 255 | clReleaseMemObject(d_widx); | |
| 256 | clReleaseMemObject(d_cids); | |
| 257 | } | |
| 258 | ||
| 259 | /* ═══════════════════════════════════════════════════════════════ | |
| 260 | * TEST 2: Pair substitution (no dedup) | |
| 261 | * | |
| 262 | * Pairs: (10, 30) count=5, (20, 40) count=3 | |
| 263 | * Classes: word 10 → class 100 (already assigned from test 1) | |
| 264 | * | |
| 265 | * After substitution: | |
| 266 | * pair 0: (30, 100) count=5 [was (10, 30), recanonized] | |
| 267 | * pair 1: (20, 40) count=3 [word 20=class 100, so → (40, 100)] | |
| 268 | * | |
| 269 | * Wait — word 20 is ALSO class 100 from test 1! So: | |
| 270 | * pair 1: (40, 100) count=3 | |
| 271 | * ═══════════════════════════════════════════════════════════════ */ | |
| 272 | ||
| 273 | printf("--- Test 2: Pair substitution (no dedup) ---\n"); | |
| 274 | ||
| 275 | { | |
| 276 | uint32_t h_wa[] = {10, 20}; | |
| 277 | uint32_t h_wb[] = {30, 40}; | |
| 278 | double h_cnt[] = {5.0, 3.0}; | |
| 279 | double h_mi[] = {2.5, 1.5}; | |
| 280 | uint32_t h_flg[] = {0, 0}; | |
| 281 | cl_uint np = 2; | |
| 282 | ||
| 283 | clEnqueueWriteBuffer(queue, pair_word_a, CL_FALSE, 0, | |
| 284 | sizeof(uint32_t) * np, h_wa, 0, NULL, NULL); | |
| 285 | clEnqueueWriteBuffer(queue, pair_word_b, CL_FALSE, 0, | |
| 286 | sizeof(uint32_t) * np, h_wb, 0, NULL, NULL); | |
| 287 | clEnqueueWriteBuffer(queue, pair_count, CL_FALSE, 0, | |
| 288 | sizeof(double) * np, h_cnt, 0, NULL, NULL); | |
| 289 | clEnqueueWriteBuffer(queue, pair_mi, CL_FALSE, 0, | |
| 290 | sizeof(double) * np, h_mi, 0, NULL, NULL); | |
| 291 | clEnqueueWriteBuffer(queue, pair_flags, CL_FALSE, 0, | |
| 292 | sizeof(uint32_t) * np, h_flg, 0, NULL, NULL); | |
| 293 | ||
| 294 | clEnqueueWriteBuffer(queue, num_changed, CL_FALSE, 0, | |
| 295 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 296 | clEnqueueWriteBuffer(queue, num_eliminated, CL_FALSE, 0, | |
| 297 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 298 | clFinish(queue); | |
| 299 | ||
| 300 | clSetKernelArg(k_sub_pair, 0, sizeof(cl_mem), &pair_word_a); | |
| 301 | clSetKernelArg(k_sub_pair, 1, sizeof(cl_mem), &pair_word_b); | |
| 302 | clSetKernelArg(k_sub_pair, 2, sizeof(cl_mem), &pair_count); | |
| 303 | clSetKernelArg(k_sub_pair, 3, sizeof(cl_mem), &pair_mi); | |
| 304 | clSetKernelArg(k_sub_pair, 4, sizeof(cl_mem), &pair_flags); | |
| 305 | clSetKernelArg(k_sub_pair, 5, sizeof(cl_mem), &word_class_id); | |
| 306 | clSetKernelArg(k_sub_pair, 6, sizeof(cl_mem), &num_changed); | |
| 307 | clSetKernelArg(k_sub_pair, 7, sizeof(cl_mem), &num_eliminated); | |
| 308 | clSetKernelArg(k_sub_pair, 8, sizeof(cl_uint), &np); | |
| 309 | ||
| 310 | size_t gs = ((np + local_size - 1) / local_size) * local_size; | |
| 311 | err = clEnqueueNDRangeKernel(queue, k_sub_pair, 1, NULL, | |
| 312 | &gs, &local_size, 0, NULL, NULL); | |
| 313 | CL_CHECK(err, "enqueue substitute_pairs"); | |
| 314 | clFinish(queue); | |
| 315 | ||
| 316 | /* Read back */ | |
| 317 | uint32_t r_wa[2], r_wb[2], r_flg[2]; | |
| 318 | double r_cnt[2]; | |
| 319 | clEnqueueReadBuffer(queue, pair_word_a, CL_TRUE, 0, | |
| 320 | sizeof(uint32_t) * np, r_wa, 0, NULL, NULL); | |
| 321 | clEnqueueReadBuffer(queue, pair_word_b, CL_TRUE, 0, | |
| 322 | sizeof(uint32_t) * np, r_wb, 0, NULL, NULL); | |
| 323 | clEnqueueReadBuffer(queue, pair_count, CL_TRUE, 0, | |
| 324 | sizeof(double) * np, r_cnt, 0, NULL, NULL); | |
| 325 | clEnqueueReadBuffer(queue, pair_flags, CL_TRUE, 0, | |
| 326 | sizeof(uint32_t) * np, r_flg, 0, NULL, NULL); | |
| 327 | ||
| 328 | uint32_t r_nchanged; | |
| 329 | clEnqueueReadBuffer(queue, num_changed, CL_TRUE, 0, | |
| 330 | sizeof(uint32_t), &r_nchanged, 0, NULL, NULL); | |
| 331 | ||
| 332 | /* Pair 0: (10,30) → (100,30) → canonical (30,100), count=5 */ | |
| 333 | /* Pair 1: (20,40) → (100,40) → canonical (40,100), count=3 */ | |
| 334 | printf(" Pair 0: (%u, %u) count=%.0f flags=%u\n", | |
| 335 | r_wa[0], r_wb[0], r_cnt[0], r_flg[0]); | |
| 336 | printf(" Expected: (30, 100) count=5 flags=1\n"); | |
| 337 | printf(" Pair 1: (%u, %u) count=%.0f flags=%u\n", | |
| 338 | r_wa[1], r_wb[1], r_cnt[1], r_flg[1]); | |
| 339 | printf(" Expected: (40, 100) count=3 flags=1\n"); | |
| 340 | printf(" Changed: %u (expected 2)\n", r_nchanged); | |
| 341 | ||
| 342 | int pass = (r_wa[0] == 30) && (r_wb[0] == 100) && | |
| 343 | (fabs(r_cnt[0] - 5.0) < 0.01) && (r_flg[0] == 1) && | |
| 344 | (r_wa[1] == 40) && (r_wb[1] == 100) && | |
| 345 | (fabs(r_cnt[1] - 3.0) < 0.01) && (r_flg[1] == 1) && | |
| 346 | (r_nchanged == 2); | |
| 347 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 348 | if (pass) pass_count++; else fail_count++; | |
| 349 | } | |
| 350 | ||
| 351 | /* ═══════════════════════════════════════════════════════════════ | |
| 352 | * TEST 3: Pair merge — duplicate pairs after substitution | |
| 353 | * | |
| 354 | * Fresh setup: | |
| 355 | * Pair 0: (10, 30) count=5 | |
| 356 | * Pair 1: (20, 30) count=3 | |
| 357 | * Classes: word 10 → class 100, word 20 → class 100 | |
| 358 | * | |
| 359 | * After substitute_pairs: | |
| 360 | * Pair 0: (30, 100) count=5 | |
| 361 | * Pair 1: (30, 100) count=3 ← duplicate! | |
| 362 | * | |
| 363 | * After rebuild_pair_index: | |
| 364 | * One pair (30, 100) count=8, other zeroed | |
| 365 | * ═══════════════════════════════════════════════════════════════ */ | |
| 366 | ||
| 367 | printf("--- Test 3: Pair merge (duplicate pairs) ---\n"); | |
| 368 | ||
| 369 | /* Reset class IDs and reassign */ | |
| 370 | clEnqueueFillBuffer(queue, word_class_id, &pat_00, 1, 0, | |
| 371 | sizeof(uint32_t) * WORD_CAPACITY, 0, NULL, NULL); | |
| 372 | clFinish(queue); | |
| 373 | ||
| 374 | { | |
| 375 | uint32_t word_indices[] = {10, 20}; | |
| 376 | uint32_t class_ids[] = {100, 100}; | |
| 377 | cl_uint na = 2; | |
| 378 | cl_mem d_widx = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 379 | sizeof(uint32_t) * na, word_indices, &err); | |
| 380 | cl_mem d_cids = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 381 | sizeof(uint32_t) * na, class_ids, &err); | |
| 382 | clSetKernelArg(k_assign, 0, sizeof(cl_mem), &word_class_id); | |
| 383 | clSetKernelArg(k_assign, 1, sizeof(cl_mem), &d_widx); | |
| 384 | clSetKernelArg(k_assign, 2, sizeof(cl_mem), &d_cids); | |
| 385 | clSetKernelArg(k_assign, 3, sizeof(cl_uint), &na); | |
| 386 | size_t gs = ((na + local_size - 1) / local_size) * local_size; | |
| 387 | clEnqueueNDRangeKernel(queue, k_assign, 1, NULL, | |
| 388 | &gs, &local_size, 0, NULL, NULL); | |
| 389 | clFinish(queue); | |
| 390 | clReleaseMemObject(d_widx); | |
| 391 | clReleaseMemObject(d_cids); | |
| 392 | } | |
| 393 | ||
| 394 | { | |
| 395 | uint32_t h_wa[] = {10, 20}; | |
| 396 | uint32_t h_wb[] = {30, 30}; | |
| 397 | double h_cnt[] = {5.0, 3.0}; | |
| 398 | double h_mi[] = {2.5, 1.5}; | |
| 399 | uint32_t h_flg[] = {0, 0}; | |
| 400 | cl_uint np = 2; | |
| 401 | ||
| 402 | clEnqueueWriteBuffer(queue, pair_word_a, CL_FALSE, 0, | |
| 403 | sizeof(uint32_t) * np, h_wa, 0, NULL, NULL); | |
| 404 | clEnqueueWriteBuffer(queue, pair_word_b, CL_FALSE, 0, | |
| 405 | sizeof(uint32_t) * np, h_wb, 0, NULL, NULL); | |
| 406 | clEnqueueWriteBuffer(queue, pair_count, CL_FALSE, 0, | |
| 407 | sizeof(double) * np, h_cnt, 0, NULL, NULL); | |
| 408 | clEnqueueWriteBuffer(queue, pair_mi, CL_FALSE, 0, | |
| 409 | sizeof(double) * np, h_mi, 0, NULL, NULL); | |
| 410 | clEnqueueWriteBuffer(queue, pair_flags, CL_FALSE, 0, | |
| 411 | sizeof(uint32_t) * np, h_flg, 0, NULL, NULL); | |
| 412 | ||
| 413 | clEnqueueWriteBuffer(queue, num_changed, CL_FALSE, 0, | |
| 414 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 415 | clEnqueueWriteBuffer(queue, num_eliminated, CL_FALSE, 0, | |
| 416 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 417 | clEnqueueWriteBuffer(queue, num_merged, CL_FALSE, 0, | |
| 418 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 419 | clFinish(queue); | |
| 420 | ||
| 421 | /* Step 1: Substitute */ | |
| 422 | clSetKernelArg(k_sub_pair, 8, sizeof(cl_uint), &np); | |
| 423 | size_t gs2 = ((np + local_size - 1) / local_size) * local_size; | |
| 424 | clEnqueueNDRangeKernel(queue, k_sub_pair, 1, NULL, | |
| 425 | &gs2, &local_size, 0, NULL, NULL); | |
| 426 | clFinish(queue); | |
| 427 | ||
| 428 | /* Step 2: Clear pair HT and rebuild */ | |
| 429 | clEnqueueFillBuffer(queue, pht_keys, &pat_ff, 1, 0, | |
| 430 | sizeof(uint64_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 431 | clEnqueueFillBuffer(queue, pht_values, &pat_ff, 1, 0, | |
| 432 | sizeof(uint32_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 433 | clFinish(queue); | |
| 434 | ||
| 435 | clSetKernelArg(k_rebuild, 0, sizeof(cl_mem), &pair_word_a); | |
| 436 | clSetKernelArg(k_rebuild, 1, sizeof(cl_mem), &pair_word_b); | |
| 437 | clSetKernelArg(k_rebuild, 2, sizeof(cl_mem), &pair_count); | |
| 438 | clSetKernelArg(k_rebuild, 3, sizeof(cl_mem), &pair_mi); | |
| 439 | clSetKernelArg(k_rebuild, 4, sizeof(cl_mem), &pair_flags); | |
| 440 | clSetKernelArg(k_rebuild, 5, sizeof(cl_mem), &pht_keys); | |
| 441 | clSetKernelArg(k_rebuild, 6, sizeof(cl_mem), &pht_values); | |
| 442 | clSetKernelArg(k_rebuild, 7, sizeof(cl_mem), &num_merged); | |
| 443 | clSetKernelArg(k_rebuild, 8, sizeof(cl_uint), &np); | |
| 444 | ||
| 445 | clEnqueueNDRangeKernel(queue, k_rebuild, 1, NULL, | |
| 446 | &gs2, &local_size, 0, NULL, NULL); | |
| 447 | clFinish(queue); | |
| 448 | ||
| 449 | /* Read back */ | |
| 450 | double r_cnt[2]; | |
| 451 | clEnqueueReadBuffer(queue, pair_count, CL_TRUE, 0, | |
| 452 | sizeof(double) * np, r_cnt, 0, NULL, NULL); | |
| 453 | ||
| 454 | uint32_t r_nmerged; | |
| 455 | clEnqueueReadBuffer(queue, num_merged, CL_TRUE, 0, | |
| 456 | sizeof(uint32_t), &r_nmerged, 0, NULL, NULL); | |
| 457 | ||
| 458 | /* One pair should have count=8, other count=0 */ | |
| 459 | double total = r_cnt[0] + r_cnt[1]; | |
| 460 | int one_is_eight = (fabs(r_cnt[0] - 8.0) < 0.01) || (fabs(r_cnt[1] - 8.0) < 0.01); | |
| 461 | int one_is_zero = (fabs(r_cnt[0]) < 0.01) || (fabs(r_cnt[1]) < 0.01); | |
| 462 | ||
| 463 | printf(" Pair 0 count: %.1f\n", r_cnt[0]); | |
| 464 | printf(" Pair 1 count: %.1f\n", r_cnt[1]); | |
| 465 | printf(" Total: %.1f (expected 8.0)\n", total); | |
| 466 | printf(" Merged: %u (expected 1)\n", r_nmerged); | |
| 467 | ||
| 468 | int pass = (fabs(total - 8.0) < 0.01) && one_is_eight && | |
| 469 | one_is_zero && (r_nmerged == 1); | |
| 470 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 471 | if (pass) pass_count++; else fail_count++; | |
| 472 | } | |
| 473 | ||
| 474 | /* ═══════════════════════════════════════════════════════════════ | |
| 475 | * TEST 4: Self-pair elimination | |
| 476 | * | |
| 477 | * Pair: (10, 20) count=7 | |
| 478 | * Classes: word 10 → class 100, word 20 → class 100 | |
| 479 | * | |
| 480 | * After substitution: (100, 100) → self-pair → eliminated | |
| 481 | * ═══════════════════════════════════════════════════════════════ */ | |
| 482 | ||
| 483 | printf("--- Test 4: Self-pair elimination ---\n"); | |
| 484 | ||
| 485 | /* Class IDs still set from test 3: word 10→100, word 20→100 */ | |
| 486 | ||
| 487 | { | |
| 488 | uint32_t h_wa[] = {10}; | |
| 489 | uint32_t h_wb[] = {20}; | |
| 490 | double h_cnt[] = {7.0}; | |
| 491 | double h_mi[] = {3.0}; | |
| 492 | uint32_t h_flg[] = {0}; | |
| 493 | cl_uint np = 1; | |
| 494 | ||
| 495 | clEnqueueWriteBuffer(queue, pair_word_a, CL_FALSE, 0, | |
| 496 | sizeof(uint32_t) * np, h_wa, 0, NULL, NULL); | |
| 497 | clEnqueueWriteBuffer(queue, pair_word_b, CL_FALSE, 0, | |
| 498 | sizeof(uint32_t) * np, h_wb, 0, NULL, NULL); | |
| 499 | clEnqueueWriteBuffer(queue, pair_count, CL_FALSE, 0, | |
| 500 | sizeof(double) * np, h_cnt, 0, NULL, NULL); | |
| 501 | clEnqueueWriteBuffer(queue, pair_mi, CL_FALSE, 0, | |
| 502 | sizeof(double) * np, h_mi, 0, NULL, NULL); | |
| 503 | clEnqueueWriteBuffer(queue, pair_flags, CL_FALSE, 0, | |
| 504 | sizeof(uint32_t) * np, h_flg, 0, NULL, NULL); | |
| 505 | ||
| 506 | clEnqueueWriteBuffer(queue, num_changed, CL_FALSE, 0, | |
| 507 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 508 | clEnqueueWriteBuffer(queue, num_eliminated, CL_FALSE, 0, | |
| 509 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 510 | clFinish(queue); | |
| 511 | ||
| 512 | clSetKernelArg(k_sub_pair, 8, sizeof(cl_uint), &np); | |
| 513 | size_t gs = ((np + local_size - 1) / local_size) * local_size; | |
| 514 | clEnqueueNDRangeKernel(queue, k_sub_pair, 1, NULL, | |
| 515 | &gs, &local_size, 0, NULL, NULL); | |
| 516 | clFinish(queue); | |
| 517 | ||
| 518 | double r_cnt; | |
| 519 | uint32_t r_nelim; | |
| 520 | clEnqueueReadBuffer(queue, pair_count, CL_TRUE, 0, | |
| 521 | sizeof(double), &r_cnt, 0, NULL, NULL); | |
| 522 | clEnqueueReadBuffer(queue, num_eliminated, CL_TRUE, 0, | |
| 523 | sizeof(uint32_t), &r_nelim, 0, NULL, NULL); | |
| 524 | ||
| 525 | printf(" Pair count after: %.1f (expected 0.0 — eliminated)\n", r_cnt); | |
| 526 | printf(" Eliminated: %u (expected 1)\n", r_nelim); | |
| 527 | ||
| 528 | int pass = (fabs(r_cnt) < 0.01) && (r_nelim == 1); | |
| 529 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 530 | if (pass) pass_count++; else fail_count++; | |
| 531 | } | |
| 532 | ||
| 533 | /* ═══════════════════════════════════════════════════════════════ | |
| 534 | * TEST 5: Section word substitution | |
| 535 | * | |
| 536 | * Sections: (word=10, count=5), (word=30, count=2), (word=20, count=4) | |
| 537 | * Classes: word 10 → 100, word 20 → 100 (from test 3) | |
| 538 | * | |
| 539 | * After substitution: | |
| 540 | * Section 0: word=100 (was 10) | |
| 541 | * Section 1: word=30 (unchanged) | |
| 542 | * Section 2: word=100 (was 20) | |
| 543 | * ═══════════════════════════════════════════════════════════════ */ | |
| 544 | ||
| 545 | printf("--- Test 5: Section word substitution ---\n"); | |
| 546 | ||
| 547 | { | |
| 548 | uint32_t h_sw[] = {10, 30, 20}; | |
| 549 | double h_sc[] = {5.0, 2.0, 4.0}; | |
| 550 | cl_uint ns = 3; | |
| 551 | ||
| 552 | clEnqueueWriteBuffer(queue, sec_word, CL_FALSE, 0, | |
| 553 | sizeof(uint32_t) * ns, h_sw, 0, NULL, NULL); | |
| 554 | clEnqueueWriteBuffer(queue, sec_count_buf, CL_FALSE, 0, | |
| 555 | sizeof(double) * ns, h_sc, 0, NULL, NULL); | |
| 556 | clEnqueueWriteBuffer(queue, num_changed, CL_FALSE, 0, | |
| 557 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 558 | clFinish(queue); | |
| 559 | ||
| 560 | clSetKernelArg(k_sub_sec, 0, sizeof(cl_mem), &sec_word); | |
| 561 | clSetKernelArg(k_sub_sec, 1, sizeof(cl_mem), &sec_count_buf); | |
| 562 | clSetKernelArg(k_sub_sec, 2, sizeof(cl_mem), &word_class_id); | |
| 563 | clSetKernelArg(k_sub_sec, 3, sizeof(cl_mem), &num_changed); | |
| 564 | clSetKernelArg(k_sub_sec, 4, sizeof(cl_uint), &ns); | |
| 565 | ||
| 566 | size_t gs = ((ns + local_size - 1) / local_size) * local_size; | |
| 567 | clEnqueueNDRangeKernel(queue, k_sub_sec, 1, NULL, | |
| 568 | &gs, &local_size, 0, NULL, NULL); | |
| 569 | clFinish(queue); | |
| 570 | ||
| 571 | uint32_t r_sw[3]; | |
| 572 | uint32_t r_nch; | |
| 573 | clEnqueueReadBuffer(queue, sec_word, CL_TRUE, 0, | |
| 574 | sizeof(uint32_t) * ns, r_sw, 0, NULL, NULL); | |
| 575 | clEnqueueReadBuffer(queue, num_changed, CL_TRUE, 0, | |
| 576 | sizeof(uint32_t), &r_nch, 0, NULL, NULL); | |
| 577 | ||
| 578 | printf(" Section 0 word: %u (expected 100)\n", r_sw[0]); | |
| 579 | printf(" Section 1 word: %u (expected 30)\n", r_sw[1]); | |
| 580 | printf(" Section 2 word: %u (expected 100)\n", r_sw[2]); | |
| 581 | printf(" Changed: %u (expected 2)\n", r_nch); | |
| 582 | ||
| 583 | int pass = (r_sw[0] == 100) && (r_sw[1] == 30) && (r_sw[2] == 100) && | |
| 584 | (r_nch == 2); | |
| 585 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 586 | if (pass) pass_count++; else fail_count++; | |
| 587 | } | |
| 588 | ||
| 589 | /* ═══════════════════════════════════════════════════════════════ | |
| 590 | * TEST 6: Benchmark — 100K pairs, 1000 class assignments | |
| 591 | * | |
| 592 | * 100 words assigned to 20 classes. | |
| 593 | * 100K pairs with random word indices (0..999). | |
| 594 | * Measure substitute + rebuild time. | |
| 595 | * ═══════════════════════════════════════════════════════════════ */ | |
| 596 | ||
| 597 | printf("--- Test 6: Benchmark (100K pairs, 100 word→20 class) ---\n"); | |
| 598 | ||
| 599 | /* Reset class IDs */ | |
| 600 | clEnqueueFillBuffer(queue, word_class_id, &pat_00, 1, 0, | |
| 601 | sizeof(uint32_t) * WORD_CAPACITY, 0, NULL, NULL); | |
| 602 | clFinish(queue); | |
| 603 | ||
| 604 | srand(42); | |
| 605 | uint32_t bench_np = 100000; | |
| 606 | uint32_t num_classes = 20; | |
| 607 | uint32_t words_per_class = 5; | |
| 608 | uint32_t total_assigned = num_classes * words_per_class; /* 100 */ | |
| 609 | uint32_t vocab = 1000; | |
| 610 | ||
| 611 | /* Assign classes */ | |
| 612 | { | |
| 613 | uint32_t* h_widx = malloc(sizeof(uint32_t) * total_assigned); | |
| 614 | uint32_t* h_cids = malloc(sizeof(uint32_t) * total_assigned); | |
| 615 | for (uint32_t c = 0; c < num_classes; c++) { | |
| 616 | for (uint32_t w = 0; w < words_per_class; w++) { | |
| 617 | uint32_t idx = c * words_per_class + w; | |
| 618 | h_widx[idx] = c * words_per_class + w; /* words 0..99 */ | |
| 619 | h_cids[idx] = 10000 + c; /* class IDs 10000..10019 */ | |
| 620 | } | |
| 621 | } | |
| 622 | ||
| 623 | cl_mem d_widx = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 624 | sizeof(uint32_t) * total_assigned, h_widx, &err); | |
| 625 | cl_mem d_cids = clCreateBuffer(ctx, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, | |
| 626 | sizeof(uint32_t) * total_assigned, h_cids, &err); | |
| 627 | ||
| 628 | clSetKernelArg(k_assign, 0, sizeof(cl_mem), &word_class_id); | |
| 629 | clSetKernelArg(k_assign, 1, sizeof(cl_mem), &d_widx); | |
| 630 | clSetKernelArg(k_assign, 2, sizeof(cl_mem), &d_cids); | |
| 631 | cl_uint na2 = total_assigned; | |
| 632 | clSetKernelArg(k_assign, 3, sizeof(cl_uint), &na2); | |
| 633 | size_t gs = ((na2 + local_size - 1) / local_size) * local_size; | |
| 634 | clEnqueueNDRangeKernel(queue, k_assign, 1, NULL, | |
| 635 | &gs, &local_size, 0, NULL, NULL); | |
| 636 | clFinish(queue); | |
| 637 | ||
| 638 | free(h_widx); free(h_cids); | |
| 639 | clReleaseMemObject(d_widx); | |
| 640 | clReleaseMemObject(d_cids); | |
| 641 | } | |
| 642 | ||
| 643 | /* Generate random pairs */ | |
| 644 | { | |
| 645 | uint32_t* h_wa = malloc(sizeof(uint32_t) * bench_np); | |
| 646 | uint32_t* h_wb = malloc(sizeof(uint32_t) * bench_np); | |
| 647 | double* h_cnt = malloc(sizeof(double) * bench_np); | |
| 648 | double* h_mi_arr = malloc(sizeof(double) * bench_np); | |
| 649 | uint32_t* h_flg = malloc(sizeof(uint32_t) * bench_np); | |
| 650 | ||
| 651 | for (uint32_t i = 0; i < bench_np; i++) { | |
| 652 | uint32_t a = rand() % vocab; | |
| 653 | uint32_t b = rand() % vocab; | |
| 654 | while (b == a) b = rand() % vocab; | |
| 655 | h_wa[i] = (a < b) ? a : b; | |
| 656 | h_wb[i] = (a < b) ? b : a; | |
| 657 | h_cnt[i] = 1.0 + (rand() % 100); | |
| 658 | h_mi_arr[i] = 0.5 + (double)(rand() % 100) / 50.0; | |
| 659 | h_flg[i] = 0; | |
| 660 | } | |
| 661 | ||
| 662 | clEnqueueWriteBuffer(queue, pair_word_a, CL_FALSE, 0, | |
| 663 | sizeof(uint32_t) * bench_np, h_wa, 0, NULL, NULL); | |
| 664 | clEnqueueWriteBuffer(queue, pair_word_b, CL_FALSE, 0, | |
| 665 | sizeof(uint32_t) * bench_np, h_wb, 0, NULL, NULL); | |
| 666 | clEnqueueWriteBuffer(queue, pair_count, CL_FALSE, 0, | |
| 667 | sizeof(double) * bench_np, h_cnt, 0, NULL, NULL); | |
| 668 | clEnqueueWriteBuffer(queue, pair_mi, CL_FALSE, 0, | |
| 669 | sizeof(double) * bench_np, h_mi_arr, 0, NULL, NULL); | |
| 670 | clEnqueueWriteBuffer(queue, pair_flags, CL_FALSE, 0, | |
| 671 | sizeof(uint32_t) * bench_np, h_flg, 0, NULL, NULL); | |
| 672 | ||
| 673 | clEnqueueWriteBuffer(queue, num_changed, CL_FALSE, 0, | |
| 674 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 675 | clEnqueueWriteBuffer(queue, num_eliminated, CL_FALSE, 0, | |
| 676 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 677 | clEnqueueWriteBuffer(queue, num_merged, CL_FALSE, 0, | |
| 678 | sizeof(uint32_t), &zero, 0, NULL, NULL); | |
| 679 | clFinish(queue); | |
| 680 | ||
| 681 | /* Substitute */ | |
| 682 | cl_uint np = bench_np; | |
| 683 | clSetKernelArg(k_sub_pair, 8, sizeof(cl_uint), &np); | |
| 684 | ||
| 685 | double t0 = now_ms(); | |
| 686 | size_t gs = ((np + local_size - 1) / local_size) * local_size; | |
| 687 | clEnqueueNDRangeKernel(queue, k_sub_pair, 1, NULL, | |
| 688 | &gs, &local_size, 0, NULL, NULL); | |
| 689 | clFinish(queue); | |
| 690 | double t1 = now_ms(); | |
| 691 | ||
| 692 | /* Rebuild */ | |
| 693 | clEnqueueFillBuffer(queue, pht_keys, &pat_ff, 1, 0, | |
| 694 | sizeof(uint64_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 695 | clEnqueueFillBuffer(queue, pht_values, &pat_ff, 1, 0, | |
| 696 | sizeof(uint32_t) * PAIR_HT_CAPACITY, 0, NULL, NULL); | |
| 697 | clFinish(queue); | |
| 698 | ||
| 699 | clSetKernelArg(k_rebuild, 0, sizeof(cl_mem), &pair_word_a); | |
| 700 | clSetKernelArg(k_rebuild, 1, sizeof(cl_mem), &pair_word_b); | |
| 701 | clSetKernelArg(k_rebuild, 2, sizeof(cl_mem), &pair_count); | |
| 702 | clSetKernelArg(k_rebuild, 3, sizeof(cl_mem), &pair_mi); | |
| 703 | clSetKernelArg(k_rebuild, 4, sizeof(cl_mem), &pair_flags); | |
| 704 | clSetKernelArg(k_rebuild, 5, sizeof(cl_mem), &pht_keys); | |
| 705 | clSetKernelArg(k_rebuild, 6, sizeof(cl_mem), &pht_values); | |
| 706 | clSetKernelArg(k_rebuild, 7, sizeof(cl_mem), &num_merged); | |
| 707 | clSetKernelArg(k_rebuild, 8, sizeof(cl_uint), &np); | |
| 708 | ||
| 709 | double t2 = now_ms(); | |
| 710 | clEnqueueNDRangeKernel(queue, k_rebuild, 1, NULL, | |
| 711 | &gs, &local_size, 0, NULL, NULL); | |
| 712 | clFinish(queue); | |
| 713 | double t3 = now_ms(); | |
| 714 | ||
| 715 | uint32_t r_nch, r_nel, r_nmg; | |
| 716 | clEnqueueReadBuffer(queue, num_changed, CL_TRUE, 0, | |
| 717 | sizeof(uint32_t), &r_nch, 0, NULL, NULL); | |
| 718 | clEnqueueReadBuffer(queue, num_eliminated, CL_TRUE, 0, | |
| 719 | sizeof(uint32_t), &r_nel, 0, NULL, NULL); | |
| 720 | clEnqueueReadBuffer(queue, num_merged, CL_TRUE, 0, | |
| 721 | sizeof(uint32_t), &r_nmg, 0, NULL, NULL); | |
| 722 | ||
| 723 | printf(" Pairs: %u\n", bench_np); | |
| 724 | printf(" Substitute: %.2f ms\n", t1 - t0); | |
| 725 | printf(" Rebuild: %.2f ms\n", t3 - t2); | |
| 726 | printf(" Total: %.2f ms\n", (t1 - t0) + (t3 - t2)); | |
| 727 | printf(" Changed: %u, Eliminated: %u, Merged: %u\n", | |
| 728 | r_nch, r_nel, r_nmg); | |
| 729 | printf(" Throughput: %.1fM pairs/sec\n", | |
| 730 | bench_np / (((t1 - t0) + (t3 - t2)) / 1000.0) / 1e6); | |
| 731 | ||
| 732 | int pass = (r_nch > 0) && ((t1 - t0) + (t3 - t2) < 1000.0); | |
| 733 | printf(" %s\n\n", pass ? "PASS" : "FAIL"); | |
| 734 | if (pass) pass_count++; else fail_count++; | |
| 735 | ||
| 736 | free(h_wa); free(h_wb); free(h_cnt); free(h_mi_arr); free(h_flg); | |
| 737 | } | |
| 738 | ||
| 739 | /* ─── Summary ─── */ | |
| 740 | ||
| 741 | printf("=== Results: %d PASS, %d FAIL ===\n", pass_count, fail_count); | |
| 742 | ||
| 743 | /* Cleanup */ | |
| 744 | free(src_ht); free(src_as); free(src_sub); free(combined); | |
| 745 | clReleaseMemObject(word_class_id); | |
| 746 | clReleaseMemObject(pair_word_a); | |
| 747 | clReleaseMemObject(pair_word_b); | |
| 748 | clReleaseMemObject(pair_count); | |
| 749 | clReleaseMemObject(pair_mi); | |
| 750 | clReleaseMemObject(pair_flags); | |
| 751 | clReleaseMemObject(pht_keys); | |
| 752 | clReleaseMemObject(pht_values); | |
| 753 | clReleaseMemObject(sec_word); | |
| 754 | clReleaseMemObject(sec_count_buf); | |
| 755 | clReleaseMemObject(num_changed); | |
| 756 | clReleaseMemObject(num_eliminated); | |
| 757 | clReleaseMemObject(num_merged); | |
| 758 | clReleaseKernel(k_assign); | |
| 759 | clReleaseKernel(k_sub_pair); | |
| 760 | clReleaseKernel(k_rebuild); | |
| 761 | clReleaseKernel(k_sub_sec); | |
| 762 | clReleaseProgram(program); | |
| 763 | clReleaseCommandQueue(queue); | |
| 764 | clReleaseContext(ctx); | |
| 765 | ||
| 766 | return fail_count > 0 ? 1 : 0; | |
| 767 | } |
| … | … | @@ -1,12 +1,22 @@ |
| 1 | 1 | |
| 2 | # Unit tests | |
| 2 | # GpuAtomTable standalone test (not CxxTest -- uses main()). | |
| 3 | 3 | |
| 4 | IF (HAVE_CUDA) | |
| 5 | ADD_EXECUTABLE(test-atom-table test_atom_table.cu) | |
| 6 | TARGET_INCLUDE_DIRECTORIES(test-atom-table PRIVATE ${PROJECT_SOURCE_DIR}) | |
| 7 | TARGET_LINK_LIBRARIES(test-atom-table persist-gpu) | |
| 8 | SET_TARGET_PROPERTIES(test-atom-table PROPERTIES | |
| 9 | CUDA_SEPARABLE_COMPILATION ON | |
| 10 | CUDA_ARCHITECTURES "50;72;75;86;89" | |
| 11 | ) | |
| 12 | ADD_TEST(NAME test-atom-table COMMAND test-atom-table) | |
| 13 | ENDIF (HAVE_CUDA) | |
| 14 | ||
| 15 | # Step 3: BackingStore integration test (CxxTest). | |
| 4 | 16 | LINK_LIBRARIES( |
| 5 | 17 | persist-gpu |
| 6 | persist | |
| 7 | ${ATOMSPACE_STORAGE_LIBRARIES} | |
| 8 | ${ATOMSPACE_LIBRARIES} | |
| 9 | OpenCL::OpenCL | |
| 18 | persist-gpu-types | |
| 19 | atomspace | |
| 10 | 20 | ) |
| 11 | 21 | |
| 12 | ADD_CXXTEST(GpuStorageNodeUTest) | |
| 22 | ADD_CXXTEST(GpuBasicSaveUTest) |
| … | … | @@ -0,0 +1,282 @@ |
| 1 | /* | |
| 2 | * tests/persist/gpu/GpuBasicSaveUTest.cxxtest | |
| 3 | * | |
| 4 | * Basic save/restore test for GpuStorageNode (Step 3). | |
| 5 | * Verifies that the BackingStore wrapper correctly delegates | |
| 6 | * atom storage to the GPU atom table. | |
| 7 | * | |
| 8 | * Tests: | |
| 9 | * 1. Store individual nodes and links, verify via GPU fetch. | |
| 10 | * 2. Round-trip: storeAtomSpace → loadAtomSpace into fresh AS. | |
| 11 | * | |
| 12 | * Copyright (C) 2026 OpenCog Foundation | |
| 13 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 14 | */ | |
| 15 | ||
| 16 | #include <cstdio> | |
| 17 | #include <cstring> | |
| 18 | ||
| 19 | #include <opencog/atoms/base/Atom.h> | |
| 20 | #include <opencog/atoms/base/Link.h> | |
| 21 | #include <opencog/atoms/base/Node.h> | |
| 22 | #include <opencog/atoms/atom_types/atom_types.h> | |
| 23 | #include <opencog/atomspace/AtomSpace.h> | |
| 24 | #include <opencog/persist/gpu/GpuStorageNode.h> | |
| 25 | #include <opencog/persist/gpu/GpuAtomTable.h> | |
| 26 | #include <opencog/persist/gpu-types/atom_types.h> | |
| 27 | ||
| 28 | #include <opencog/util/Logger.h> | |
| 29 | ||
| 30 | using namespace opencog; | |
| 31 | ||
| 32 | class GpuBasicSaveUTest : public CxxTest::TestSuite | |
| 33 | { | |
| 34 | public: | |
| 35 | ||
| 36 | GpuBasicSaveUTest(void) | |
| 37 | { | |
| 38 | logger().set_level(Logger::INFO); | |
| 39 | logger().set_print_to_stdout_flag(true); | |
| 40 | } | |
| 41 | ||
| 42 | ~GpuBasicSaveUTest() | |
| 43 | { | |
| 44 | if (!CxxTest::TestTracker::tracker().suiteFailed()) | |
| 45 | std::remove(logger().get_filename().c_str()); | |
| 46 | } | |
| 47 | ||
| 48 | void test_open_close(void); | |
| 49 | void test_store_nodes(void); | |
| 50 | void test_store_link(void); | |
| 51 | void test_roundtrip(void); | |
| 52 | }; | |
| 53 | ||
| 54 | // ============================================================ | |
| 55 | // Test that open/close/connected work correctly. | |
| 56 | ||
| 57 | void GpuBasicSaveUTest::test_open_close(void) | |
| 58 | { | |
| 59 | logger().debug("BEGIN TEST: %s", __FUNCTION__); | |
| 60 | ||
| 61 | GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test"); | |
| 62 | ||
| 63 | TSM_ASSERT("Should not be connected before open", !store->connected()); | |
| 64 | ||
| 65 | store->open(); | |
| 66 | TSM_ASSERT("Should be connected after open", store->connected()); | |
| 67 | ||
| 68 | // Double-open should be safe. | |
| 69 | store->open(); | |
| 70 | TSM_ASSERT("Still connected after double open", store->connected()); | |
| 71 | ||
| 72 | store->close(); | |
| 73 | TSM_ASSERT("Should not be connected after close", !store->connected()); | |
| 74 | ||
| 75 | delete store; | |
| 76 | ||
| 77 | printf("test_open_close: PASSED\n"); | |
| 78 | logger().debug("END TEST: %s", __FUNCTION__); | |
| 79 | } | |
| 80 | ||
| 81 | // ============================================================ | |
| 82 | // Store nodes through BackingStore, verify via GPU fetch. | |
| 83 | ||
| 84 | void GpuBasicSaveUTest::test_store_nodes(void) | |
| 85 | { | |
| 86 | logger().debug("BEGIN TEST: %s", __FUNCTION__); | |
| 87 | ||
| 88 | GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test"); | |
| 89 | store->open(); | |
| 90 | TSM_ASSERT("Not connected", store->connected()); | |
| 91 | ||
| 92 | // Create atoms (not in any AtomSpace — raw handles). | |
| 93 | Handle cat(createNode(CONCEPT_NODE, "cat")); | |
| 94 | Handle dog(createNode(CONCEPT_NODE, "dog")); | |
| 95 | Handle isa(createNode(PREDICATE_NODE, "is-a")); | |
| 96 | ||
| 97 | // Store through BackingStore API. | |
| 98 | store->storeAtom(cat, true); | |
| 99 | store->storeAtom(dog, true); | |
| 100 | store->storeAtom(isa, true); | |
| 101 | store->barrier(); | |
| 102 | ||
| 103 | // Verify GPU table has 3 atoms. | |
| 104 | const GpuAtomTable& t = store->gpu_table(); | |
| 105 | TSM_ASSERT_EQUALS("Expected 3 atoms", t.atom_count, 3u); | |
| 106 | ||
| 107 | // Fetch slot 0 from GPU, verify it's "cat". | |
| 108 | { | |
| 109 | uint16_t type; | |
| 110 | char name_buf[256]; | |
| 111 | uint16_t name_len = sizeof(name_buf); | |
| 112 | int rc = gpu_fetch_node(&t, 0, &type, name_buf, &name_len); | |
| 113 | TSM_ASSERT_EQUALS("Fetch node 0 failed", rc, 0); | |
| 114 | TSM_ASSERT_EQUALS("Type mismatch slot 0", type, (uint16_t)CONCEPT_NODE); | |
| 115 | TSM_ASSERT_EQUALS("Name length mismatch", name_len, 3u); | |
| 116 | TSM_ASSERT("Name mismatch slot 0", memcmp(name_buf, "cat", 3) == 0); | |
| 117 | } | |
| 118 | ||
| 119 | // Fetch slot 1 — "dog". | |
| 120 | { | |
| 121 | uint16_t type; | |
| 122 | char name_buf[256]; | |
| 123 | uint16_t name_len = sizeof(name_buf); | |
| 124 | int rc = gpu_fetch_node(&t, 1, &type, name_buf, &name_len); | |
| 125 | TSM_ASSERT_EQUALS("Fetch node 1 failed", rc, 0); | |
| 126 | TSM_ASSERT_EQUALS("Type mismatch slot 1", type, (uint16_t)CONCEPT_NODE); | |
| 127 | TSM_ASSERT_EQUALS("Name length mismatch", name_len, 3u); | |
| 128 | TSM_ASSERT("Name mismatch slot 1", memcmp(name_buf, "dog", 3) == 0); | |
| 129 | } | |
| 130 | ||
| 131 | // Fetch slot 2 — "is-a" (PredicateNode). | |
| 132 | { | |
| 133 | uint16_t type; | |
| 134 | char name_buf[256]; | |
| 135 | uint16_t name_len = sizeof(name_buf); | |
| 136 | int rc = gpu_fetch_node(&t, 2, &type, name_buf, &name_len); | |
| 137 | TSM_ASSERT_EQUALS("Fetch node 2 failed", rc, 0); | |
| 138 | TSM_ASSERT_EQUALS("Type mismatch slot 2", type, (uint16_t)PREDICATE_NODE); | |
| 139 | TSM_ASSERT_EQUALS("Name length mismatch", name_len, 4u); | |
| 140 | TSM_ASSERT("Name mismatch slot 2", memcmp(name_buf, "is-a", 4) == 0); | |
| 141 | } | |
| 142 | ||
| 143 | // Storing the same atom again should be a no-op. | |
| 144 | store->storeAtom(cat, true); | |
| 145 | TSM_ASSERT_EQUALS("Duplicate store changed count", t.atom_count, 3u); | |
| 146 | ||
| 147 | store->close(); | |
| 148 | delete store; | |
| 149 | ||
| 150 | printf("test_store_nodes: PASSED\n"); | |
| 151 | logger().debug("END TEST: %s", __FUNCTION__); | |
| 152 | } | |
| 153 | ||
| 154 | // ============================================================ | |
| 155 | // Store a link (with outgoing atoms), verify GPU data. | |
| 156 | ||
| 157 | void GpuBasicSaveUTest::test_store_link(void) | |
| 158 | { | |
| 159 | logger().debug("BEGIN TEST: %s", __FUNCTION__); | |
| 160 | ||
| 161 | GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test"); | |
| 162 | store->open(); | |
| 163 | ||
| 164 | Handle cat(createNode(CONCEPT_NODE, "cat")); | |
| 165 | Handle dog(createNode(CONCEPT_NODE, "dog")); | |
| 166 | Handle lnk(createLink(LIST_LINK, cat, dog)); | |
| 167 | ||
| 168 | // storeAtom on the link should recursively store cat and dog first. | |
| 169 | store->storeAtom(lnk, true); | |
| 170 | store->barrier(); | |
| 171 | ||
| 172 | const GpuAtomTable& t = store->gpu_table(); | |
| 173 | TSM_ASSERT_EQUALS("Expected 3 atoms (2 nodes + 1 link)", t.atom_count, 3u); | |
| 174 | ||
| 175 | // Verify the link at slot 2. | |
| 176 | { | |
| 177 | uint16_t type; | |
| 178 | uint32_t out_buf[16]; | |
| 179 | uint16_t arity = 16; | |
| 180 | int rc = gpu_fetch_link(&t, 2, &type, out_buf, &arity); | |
| 181 | TSM_ASSERT_EQUALS("Fetch link failed", rc, 0); | |
| 182 | TSM_ASSERT_EQUALS("Link type mismatch", type, (uint16_t)LIST_LINK); | |
| 183 | TSM_ASSERT_EQUALS("Arity mismatch", arity, 2u); | |
| 184 | TSM_ASSERT_EQUALS("Outgoing[0] should be slot 0", out_buf[0], 0u); | |
| 185 | TSM_ASSERT_EQUALS("Outgoing[1] should be slot 1", out_buf[1], 1u); | |
| 186 | } | |
| 187 | ||
| 188 | // Test link-of-links. | |
| 189 | Handle isa(createNode(PREDICATE_NODE, "is-a")); | |
| 190 | Handle evl(createLink(EVALUATION_LINK, isa, lnk)); | |
| 191 | store->storeAtom(evl, true); | |
| 192 | store->barrier(); | |
| 193 | ||
| 194 | // isa → slot 3, evl → slot 4 | |
| 195 | TSM_ASSERT_EQUALS("Expected 5 atoms", t.atom_count, 5u); | |
| 196 | ||
| 197 | { | |
| 198 | uint16_t type; | |
| 199 | uint32_t out_buf[16]; | |
| 200 | uint16_t arity = 16; | |
| 201 | int rc = gpu_fetch_link(&t, 4, &type, out_buf, &arity); | |
| 202 | TSM_ASSERT_EQUALS("Fetch eval link failed", rc, 0); | |
| 203 | TSM_ASSERT_EQUALS("EvalLink type mismatch", type, (uint16_t)EVALUATION_LINK); | |
| 204 | TSM_ASSERT_EQUALS("EvalLink arity mismatch", arity, 2u); | |
| 205 | TSM_ASSERT_EQUALS("EvalLink out[0] = isa (slot 3)", out_buf[0], 3u); | |
| 206 | TSM_ASSERT_EQUALS("EvalLink out[1] = lnk (slot 2)", out_buf[1], 2u); | |
| 207 | } | |
| 208 | ||
| 209 | store->close(); | |
| 210 | delete store; | |
| 211 | ||
| 212 | printf("test_store_link: PASSED\n"); | |
| 213 | logger().debug("END TEST: %s", __FUNCTION__); | |
| 214 | } | |
| 215 | ||
| 216 | // ============================================================ | |
| 217 | // Full round-trip: store from one AtomSpace, load into another. | |
| 218 | ||
| 219 | void GpuBasicSaveUTest::test_roundtrip(void) | |
| 220 | { | |
| 221 | logger().debug("BEGIN TEST: %s", __FUNCTION__); | |
| 222 | ||
| 223 | GpuStorageNode* store = new GpuStorageNode(GPU_STORAGE_NODE, "gpu://test"); | |
| 224 | store->open(); | |
| 225 | ||
| 226 | // Build an AtomSpace with some atoms. | |
| 227 | AtomSpace* as1 = new AtomSpace(); | |
| 228 | Handle cat = as1->add_node(CONCEPT_NODE, "cat"); | |
| 229 | Handle dog = as1->add_node(CONCEPT_NODE, "dog"); | |
| 230 | Handle mammal = as1->add_node(CONCEPT_NODE, "mammal"); | |
| 231 | Handle lnk = as1->add_link(LIST_LINK, cat, dog); | |
| 232 | Handle inh = as1->add_link(INHERITANCE_LINK, cat, mammal); | |
| 233 | ||
| 234 | // Store entire AtomSpace to GPU. | |
| 235 | store->storeAtomSpace(as1); | |
| 236 | store->barrier(); | |
| 237 | ||
| 238 | TSM_ASSERT_EQUALS("Expected 5 atoms on GPU", | |
| 239 | store->gpu_table().atom_count, 5u); | |
| 240 | ||
| 241 | // Load into a fresh AtomSpace. | |
| 242 | AtomSpace* as2 = new AtomSpace(); | |
| 243 | store->loadAtomSpace(as2); | |
| 244 | ||
| 245 | // Verify all atoms were reconstructed. | |
| 246 | size_t sz1 = as1->get_size(); | |
| 247 | size_t sz2 = as2->get_size(); | |
| 248 | printf("Round-trip: stored %lu, loaded %lu atoms\n", sz1, sz2); | |
| 249 | TSM_ASSERT_EQUALS("Atom count mismatch after round-trip", sz1, sz2); | |
| 250 | ||
| 251 | // Verify specific atoms exist in as2 (use get_atom with prototype Handle). | |
| 252 | Handle cat2 = as2->get_atom(createNode(CONCEPT_NODE, "cat")); | |
| 253 | TSM_ASSERT("cat not found in loaded AS", cat2 != nullptr); | |
| 254 | ||
| 255 | Handle dog2 = as2->get_atom(createNode(CONCEPT_NODE, "dog")); | |
| 256 | TSM_ASSERT("dog not found in loaded AS", dog2 != nullptr); | |
| 257 | ||
| 258 | Handle mammal2 = as2->get_atom(createNode(CONCEPT_NODE, "mammal")); | |
| 259 | TSM_ASSERT("mammal not found in loaded AS", mammal2 != nullptr); | |
| 260 | ||
| 261 | // Verify link structure. | |
| 262 | Handle lnk2 = as2->get_atom(createLink(LIST_LINK, cat2, dog2)); | |
| 263 | TSM_ASSERT("ListLink(cat,dog) not found", lnk2 != nullptr); | |
| 264 | if (lnk2) | |
| 265 | { | |
| 266 | TSM_ASSERT_EQUALS("ListLink arity mismatch", | |
| 267 | lnk2->get_arity(), lnk->get_arity()); | |
| 268 | } | |
| 269 | ||
| 270 | Handle inh2 = as2->get_atom(createLink(INHERITANCE_LINK, cat2, mammal2)); | |
| 271 | TSM_ASSERT("InheritanceLink(cat,mammal) not found", inh2 != nullptr); | |
| 272 | ||
| 273 | store->close(); | |
| 274 | delete store; | |
| 275 | delete as1; | |
| 276 | delete as2; | |
| 277 | ||
| 278 | printf("test_roundtrip: PASSED\n"); | |
| 279 | logger().debug("END TEST: %s", __FUNCTION__); | |
| 280 | } | |
| 281 | ||
| 282 | /* ============================= END OF FILE ================= */ |
| … | … | @@ -1,444 +0,0 @@ |
| 1 | /* | |
| 2 | * tests/persist/gpu/GpuStorageNodeUTest.cxxtest | |
| 3 | * | |
| 4 | * Basic store/fetch round-trip tests for GpuStorageNode. | |
| 5 | * Modeled after atomspace-rocks BasicSaveUTest.cxxtest. | |
| 6 | * | |
| 7 | * Tests Phase 1: atoms and values survive a round-trip | |
| 8 | * through GPU SoA pools. | |
| 9 | * | |
| 10 | * Copyright (C) 2025 OpenCog Foundation | |
| 11 | * | |
| 12 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 13 | */ | |
| 14 | ||
| 15 | #include <cstdio> | |
| 16 | #include <cmath> | |
| 17 | ||
| 18 | #include <opencog/atoms/base/Atom.h> | |
| 19 | #include <opencog/atoms/base/Link.h> | |
| 20 | #include <opencog/atoms/base/Node.h> | |
| 21 | #include <opencog/atoms/atom_types/atom_types.h> | |
| 22 | #include <opencog/atoms/value/FloatValue.h> | |
| 23 | #include <opencog/atoms/value/VoidValue.h> | |
| 24 | #include <opencog/atomspace/AtomSpace.h> | |
| 25 | #include <opencog/persist/gpu/GpuStorageNode.h> | |
| 26 | #include <opencog/persist/gpu-types/atom_types.h> | |
| 27 | ||
| 28 | #include <opencog/util/Logger.h> | |
| 29 | ||
| 30 | #include <cxxtest/TestSuite.h> | |
| 31 | ||
| 32 | using namespace opencog; | |
| 33 | ||
| 34 | // ============================================================ | |
| 35 | // Compare two atoms for equality (type, name, truth value) | |
| 36 | ||
| 37 | static void atomCompare(Handle a, Handle b, const char* where) | |
| 38 | { | |
| 39 | printf("Testing at %s\n", where); | |
| 40 | TSM_ASSERT("No atom found", b != nullptr); | |
| 41 | if (nullptr == b) return; | |
| 42 | ||
| 43 | TSM_ASSERT_EQUALS("Type mismatch", a->get_type(), b->get_type()); | |
| 44 | ||
| 45 | NodePtr na(NodeCast(a)); | |
| 46 | NodePtr nb(NodeCast(b)); | |
| 47 | if (na and nb) | |
| 48 | { | |
| 49 | TSM_ASSERT_EQUALS("Name mismatch", na->get_name(), nb->get_name()); | |
| 50 | } | |
| 51 | ||
| 52 | LinkPtr la(LinkCast(a)); | |
| 53 | LinkPtr lb(LinkCast(b)); | |
| 54 | if (la and lb) | |
| 55 | { | |
| 56 | TSM_ASSERT_EQUALS("Arity mismatch", la->get_arity(), lb->get_arity()); | |
| 57 | } | |
| 58 | ||
| 59 | ValuePtr ta = a->getValue(truth_key()); | |
| 60 | ValuePtr tb = b->getValue(truth_key()); | |
| 61 | if (ta or tb) | |
| 62 | { | |
| 63 | TSM_ASSERT("Missing truth value on a", ta); | |
| 64 | TSM_ASSERT("Missing truth value on b", tb); | |
| 65 | if (ta and tb) | |
| 66 | { | |
| 67 | TSM_ASSERT("Truth value miscompare", (*ta) == (*tb)); | |
| 68 | ||
| 69 | if (not ((*ta) == (*tb))) | |
| 70 | { | |
| 71 | FloatValuePtr fva = FloatValueCast(ta); | |
| 72 | FloatValuePtr fvb = FloatValueCast(tb); | |
| 73 | if (fva and fvb) | |
| 74 | { | |
| 75 | fprintf(stderr, | |
| 76 | "Error at %s: val[0] a=%g b=%g, val[1] a=%g b=%g\n", | |
| 77 | where, | |
| 78 | fva->value()[0], fvb->value()[0], | |
| 79 | fva->value().size() > 1 ? fva->value()[1] : -999.0, | |
| 80 | fvb->value().size() > 1 ? fvb->value()[1] : -999.0); | |
| 81 | } | |
| 82 | } | |
| 83 | } | |
| 84 | } | |
| 85 | } | |
| 86 | ||
| 87 | // ============================================================ | |
| 88 | ||
| 89 | class GpuStorageNodeUTest : public CxxTest::TestSuite | |
| 90 | { | |
| 91 | private: | |
| 92 | std::string uri; | |
| 93 | ||
| 94 | public: | |
| 95 | GpuStorageNodeUTest(void) | |
| 96 | { | |
| 97 | logger().set_level(Logger::INFO); | |
| 98 | logger().set_print_to_stdout_flag(true); | |
| 99 | logger().set_sync_flag(true); | |
| 100 | ||
| 101 | // Use first available GPU device | |
| 102 | uri = "gpu://:"; | |
| 103 | } | |
| 104 | ||
| 105 | ~GpuStorageNodeUTest() {} | |
| 106 | ||
| 107 | void setUp(void) {} | |
| 108 | void tearDown(void) {} | |
| 109 | ||
| 110 | void test_open_close(void); | |
| 111 | void test_single_node(void); | |
| 112 | void test_node_with_value(void); | |
| 113 | void test_link_round_trip(void); | |
| 114 | void test_value_round_trip(void); | |
| 115 | void test_multiple_nodes(void); | |
| 116 | void test_erase(void); | |
| 117 | void test_monitor(void); | |
| 118 | }; | |
| 119 | ||
| 120 | // ============================================================ | |
| 121 | // Test 1: Can we open and close a connection? | |
| 122 | ||
| 123 | void GpuStorageNodeUTest::test_open_close(void) | |
| 124 | { | |
| 125 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 126 | ||
| 127 | AtomSpacePtr as = createAtomSpace(); | |
| 128 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 129 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 130 | ||
| 131 | TSM_ASSERT("Failed to create GpuStorageNode", store != nullptr); | |
| 132 | TSM_ASSERT("Should not be connected yet", !store->connected()); | |
| 133 | ||
| 134 | store->open(); | |
| 135 | TSM_ASSERT("Should be connected after open", store->connected()); | |
| 136 | ||
| 137 | store->close(); | |
| 138 | TSM_ASSERT("Should not be connected after close", !store->connected()); | |
| 139 | ||
| 140 | logger().info("END TEST: %s", __FUNCTION__); | |
| 141 | } | |
| 142 | ||
| 143 | // ============================================================ | |
| 144 | // Test 2: Store and fetch a single node (no values) | |
| 145 | ||
| 146 | void GpuStorageNodeUTest::test_single_node(void) | |
| 147 | { | |
| 148 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 149 | ||
| 150 | AtomSpacePtr as = createAtomSpace(); | |
| 151 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 152 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 153 | store->open(); | |
| 154 | ||
| 155 | // Create a node | |
| 156 | Handle a(createNode(SCHEMA_NODE, "someNode")); | |
| 157 | ||
| 158 | // Store it | |
| 159 | store->storeAtom(a, true); | |
| 160 | store->barrier(); | |
| 161 | printf("Stored single node\n"); | |
| 162 | ||
| 163 | // Create a fresh handle with the same name and fetch | |
| 164 | Handle b(createNode(SCHEMA_NODE, "someNode")); | |
| 165 | store->getAtom(b); | |
| 166 | printf("Fetched single node\n"); | |
| 167 | ||
| 168 | // Verify type and name match | |
| 169 | TSM_ASSERT_EQUALS("Type mismatch", a->get_type(), b->get_type()); | |
| 170 | NodePtr na(NodeCast(a)); | |
| 171 | NodePtr nb(NodeCast(b)); | |
| 172 | TSM_ASSERT("Should be nodes", na and nb); | |
| 173 | if (na and nb) | |
| 174 | TSM_ASSERT_EQUALS("Name mismatch", na->get_name(), nb->get_name()); | |
| 175 | ||
| 176 | store->erase(); | |
| 177 | store->close(); | |
| 178 | ||
| 179 | logger().info("END TEST: %s", __FUNCTION__); | |
| 180 | } | |
| 181 | ||
| 182 | // ============================================================ | |
| 183 | // Test 3: Store a node with a FloatValue, fetch it back | |
| 184 | ||
| 185 | void GpuStorageNodeUTest::test_node_with_value(void) | |
| 186 | { | |
| 187 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 188 | ||
| 189 | AtomSpacePtr as = createAtomSpace(); | |
| 190 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 191 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 192 | store->open(); | |
| 193 | ||
| 194 | // Create a node with a truth value | |
| 195 | Handle a(createNode(SCHEMA_NODE, "nodeWithValue")); | |
| 196 | ValuePtr stv(createFloatValue(std::vector<double>({0.55, 0.6}))); | |
| 197 | a->setValue(truth_key(), stv); | |
| 198 | ||
| 199 | // Store | |
| 200 | store->storeAtom(a, true); | |
| 201 | store->barrier(); | |
| 202 | printf("Stored node with value\n"); | |
| 203 | ||
| 204 | // Fetch back | |
| 205 | Handle b(createNode(SCHEMA_NODE, "nodeWithValue")); | |
| 206 | store->getAtom(b); | |
| 207 | printf("Fetched node with value\n"); | |
| 208 | ||
| 209 | // Compare | |
| 210 | atomCompare(a, b, "Node with FloatValue round-trip"); | |
| 211 | ||
| 212 | store->erase(); | |
| 213 | store->close(); | |
| 214 | ||
| 215 | logger().info("END TEST: %s", __FUNCTION__); | |
| 216 | } | |
| 217 | ||
| 218 | // ============================================================ | |
| 219 | // Test 4: Store a binary link (two nodes), fetch it back | |
| 220 | ||
| 221 | void GpuStorageNodeUTest::test_link_round_trip(void) | |
| 222 | { | |
| 223 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 224 | ||
| 225 | AtomSpacePtr as = createAtomSpace(); | |
| 226 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 227 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 228 | store->open(); | |
| 229 | ||
| 230 | // Create two nodes and a link with a value | |
| 231 | Handle n1(createNode(SCHEMA_NODE, "wordA")); | |
| 232 | Handle n2(createNode(SCHEMA_NODE, "wordB")); | |
| 233 | ||
| 234 | HandleSeq hvec; | |
| 235 | hvec.push_back(n1); | |
| 236 | hvec.push_back(n2); | |
| 237 | Handle lnk(createLink(hvec, LIST_LINK)); | |
| 238 | ValuePtr stv(createFloatValue(std::vector<double>({12.5, 3.7}))); | |
| 239 | lnk->setValue(truth_key(), stv); | |
| 240 | ||
| 241 | // Store | |
| 242 | store->storeAtom(lnk, true); | |
| 243 | store->barrier(); | |
| 244 | printf("Stored link with value\n"); | |
| 245 | ||
| 246 | // Fetch back -- create fresh link with same structure | |
| 247 | Handle fn1(createNode(SCHEMA_NODE, "wordA")); | |
| 248 | Handle fn2(createNode(SCHEMA_NODE, "wordB")); | |
| 249 | Handle flnk(createLink(LIST_LINK, fn1, fn2)); | |
| 250 | store->getAtom(flnk); | |
| 251 | printf("Fetched link with value\n"); | |
| 252 | ||
| 253 | // Compare the truth value | |
| 254 | atomCompare(lnk, flnk, "Link round-trip"); | |
| 255 | ||
| 256 | store->erase(); | |
| 257 | store->close(); | |
| 258 | ||
| 259 | logger().info("END TEST: %s", __FUNCTION__); | |
| 260 | } | |
| 261 | ||
| 262 | // ============================================================ | |
| 263 | // Test 5: storeValue / loadValue specific key | |
| 264 | ||
| 265 | void GpuStorageNodeUTest::test_value_round_trip(void) | |
| 266 | { | |
| 267 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 268 | ||
| 269 | AtomSpacePtr as = createAtomSpace(); | |
| 270 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 271 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 272 | store->open(); | |
| 273 | ||
| 274 | // Create a node with truth value | |
| 275 | Handle a(createNode(SCHEMA_NODE, "valueTestNode")); | |
| 276 | double v1 = 1.0e33 / 3.0; | |
| 277 | double v2 = 1.0e-30 / 300.0; | |
| 278 | ValuePtr stv(createFloatValue(std::vector<double>({v1, v2}))); | |
| 279 | a->setValue(truth_key(), stv); | |
| 280 | ||
| 281 | // Store just the value | |
| 282 | store->storeValue(a, truth_key()); | |
| 283 | store->barrier(); | |
| 284 | printf("Stored value\n"); | |
| 285 | ||
| 286 | // Create a fresh atom and load just the value | |
| 287 | Handle b(createNode(SCHEMA_NODE, "valueTestNode")); | |
| 288 | store->loadValue(b, truth_key()); | |
| 289 | printf("Loaded value\n"); | |
| 290 | ||
| 291 | // Compare | |
| 292 | atomCompare(a, b, "Value round-trip with extreme floats"); | |
| 293 | ||
| 294 | // Verify the actual doubles match | |
| 295 | FloatValuePtr fva = FloatValueCast(a->getValue(truth_key())); | |
| 296 | FloatValuePtr fvb = FloatValueCast(b->getValue(truth_key())); | |
| 297 | TSM_ASSERT("Missing value on a", fva); | |
| 298 | TSM_ASSERT("Missing value on b", fvb); | |
| 299 | if (fva and fvb) | |
| 300 | { | |
| 301 | TSM_ASSERT_EQUALS("Value size mismatch", | |
| 302 | fva->value().size(), fvb->value().size()); | |
| 303 | for (size_t i = 0; i < fva->value().size(); i++) | |
| 304 | { | |
| 305 | TSM_ASSERT_DELTA("Float value mismatch", | |
| 306 | fva->value()[i], fvb->value()[i], 1e-10 * fabs(fva->value()[i])); | |
| 307 | } | |
| 308 | } | |
| 309 | ||
| 310 | store->erase(); | |
| 311 | store->close(); | |
| 312 | ||
| 313 | logger().info("END TEST: %s", __FUNCTION__); | |
| 314 | } | |
| 315 | ||
| 316 | // ============================================================ | |
| 317 | // Test 6: Store many nodes, verify pool counts | |
| 318 | ||
| 319 | void GpuStorageNodeUTest::test_multiple_nodes(void) | |
| 320 | { | |
| 321 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 322 | ||
| 323 | AtomSpacePtr as = createAtomSpace(); | |
| 324 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 325 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 326 | store->open(); | |
| 327 | ||
| 328 | const int N = 100; | |
| 329 | ||
| 330 | // Store N nodes | |
| 331 | for (int i = 0; i < N; i++) | |
| 332 | { | |
| 333 | std::string name = "testWord_" + std::to_string(i); | |
| 334 | Handle h(createNode(SCHEMA_NODE, name)); | |
| 335 | double count = (double)(i + 1); | |
| 336 | double mi = count * 0.1; | |
| 337 | h->setValue(truth_key(), | |
| 338 | createFloatValue(std::vector<double>({count, mi}))); | |
| 339 | store->storeAtom(h, false); | |
| 340 | } | |
| 341 | store->barrier(); | |
| 342 | printf("Stored %d nodes\n", N); | |
| 343 | ||
| 344 | // Fetch them all back and verify | |
| 345 | int found = 0; | |
| 346 | for (int i = 0; i < N; i++) | |
| 347 | { | |
| 348 | std::string name = "testWord_" + std::to_string(i); | |
| 349 | Handle h(createNode(SCHEMA_NODE, name)); | |
| 350 | store->getAtom(h); | |
| 351 | ||
| 352 | ValuePtr tv = h->getValue(truth_key()); | |
| 353 | if (tv) | |
| 354 | { | |
| 355 | FloatValuePtr fv = FloatValueCast(tv); | |
| 356 | if (fv and fv->value().size() >= 2) | |
| 357 | { | |
| 358 | double expected_count = (double)(i + 1); | |
| 359 | double expected_mi = expected_count * 0.1; | |
| 360 | TSM_ASSERT_DELTA("Count mismatch", | |
| 361 | fv->value()[0], expected_count, 1e-10); | |
| 362 | TSM_ASSERT_DELTA("MI mismatch", | |
| 363 | fv->value()[1], expected_mi, 1e-10); | |
| 364 | found++; | |
| 365 | } | |
| 366 | } | |
| 367 | } | |
| 368 | ||
| 369 | printf("Verified %d / %d nodes\n", found, N); | |
| 370 | TSM_ASSERT_EQUALS("Not all nodes found", found, N); | |
| 371 | ||
| 372 | store->erase(); | |
| 373 | store->close(); | |
| 374 | ||
| 375 | logger().info("END TEST: %s", __FUNCTION__); | |
| 376 | } | |
| 377 | ||
| 378 | // ============================================================ | |
| 379 | // Test 7: Erase clears everything | |
| 380 | ||
| 381 | void GpuStorageNodeUTest::test_erase(void) | |
| 382 | { | |
| 383 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 384 | ||
| 385 | AtomSpacePtr as = createAtomSpace(); | |
| 386 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 387 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 388 | store->open(); | |
| 389 | ||
| 390 | // Store some data | |
| 391 | Handle a(createNode(SCHEMA_NODE, "eraseMe")); | |
| 392 | a->setValue(truth_key(), | |
| 393 | createFloatValue(std::vector<double>({42.0, 7.5}))); | |
| 394 | store->storeAtom(a, true); | |
| 395 | store->barrier(); | |
| 396 | ||
| 397 | // Erase | |
| 398 | store->erase(); | |
| 399 | ||
| 400 | // Try to fetch -- should not find it | |
| 401 | Handle b(createNode(SCHEMA_NODE, "eraseMe")); | |
| 402 | store->getAtom(b); | |
| 403 | ||
| 404 | ValuePtr tv = b->getValue(truth_key()); | |
| 405 | TSM_ASSERT("Should not have value after erase", nullptr == tv); | |
| 406 | ||
| 407 | store->close(); | |
| 408 | ||
| 409 | logger().info("END TEST: %s", __FUNCTION__); | |
| 410 | } | |
| 411 | ||
| 412 | // ============================================================ | |
| 413 | // Test 8: Monitor returns useful info | |
| 414 | ||
| 415 | void GpuStorageNodeUTest::test_monitor(void) | |
| 416 | { | |
| 417 | logger().info("BEGIN TEST: %s", __FUNCTION__); | |
| 418 | ||
| 419 | AtomSpacePtr as = createAtomSpace(); | |
| 420 | Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string(uri)); | |
| 421 | GpuStorageNodePtr store = GpuStorageNodeCast(hsn); | |
| 422 | store->open(); | |
| 423 | ||
| 424 | // Store a node | |
| 425 | Handle a(createNode(SCHEMA_NODE, "monitorTest")); | |
| 426 | store->storeAtom(a, true); | |
| 427 | store->barrier(); | |
| 428 | ||
| 429 | std::string stats = store->monitor(); | |
| 430 | printf("Monitor output:\n%s\n", stats.c_str()); | |
| 431 | ||
| 432 | TSM_ASSERT("Monitor should contain GPU", stats.find("gpu://") != std::string::npos); | |
| 433 | TSM_ASSERT("Monitor should show connected", | |
| 434 | stats.find("Connected: yes") != std::string::npos); | |
| 435 | TSM_ASSERT("Monitor should show word count", | |
| 436 | stats.find("Words:") != std::string::npos); | |
| 437 | ||
| 438 | store->erase(); | |
| 439 | store->close(); | |
| 440 | ||
| 441 | logger().info("END TEST: %s", __FUNCTION__); | |
| 442 | } | |
| 443 | ||
| 444 | /* ============================= END OF FILE ================= */ |
| … | … | @@ -0,0 +1,220 @@ |
| 1 | /* | |
| 2 | * tests/persist/gpu/test_atom_table.cu | |
| 3 | * | |
| 4 | * Standalone CUDA test for GpuAtomTable. | |
| 5 | * | |
| 6 | * Tests: | |
| 7 | * 1. Alloc table | |
| 8 | * 2. Store 3 nodes with names | |
| 9 | * 3. Store 1 binary link (refs 2 nodes) | |
| 10 | * 4. Store 1 ternary link (refs 3 nodes) | |
| 11 | * 5. Store 1 link containing a link (link-of-links) | |
| 12 | * 6. Fetch everything back, verify matches | |
| 13 | * 7. Clear table, verify zeros | |
| 14 | * 8. Free table | |
| 15 | * | |
| 16 | * Build standalone: | |
| 17 | * nvcc -I../../.. test_atom_table.cu \ | |
| 18 | * ../../../opencog/persist/gpu/GpuAtomTable.cu \ | |
| 19 | * -o test_atom_table && ./test_atom_table | |
| 20 | * | |
| 21 | * Copyright (C) 2026 OpenCog Foundation | |
| 22 | * SPDX-License-Identifier: AGPL-3.0-or-later | |
| 23 | */ | |
| 24 | ||
| 25 | #include "opencog/persist/gpu/GpuAtomTable.h" | |
| 26 | ||
| 27 | #include <cstdio> | |
| 28 | #include <cstring> | |
| 29 | #include <cstdlib> | |
| 30 | ||
| 31 | static int g_failures = 0; | |
| 32 | ||
| 33 | #define CHECK(cond, msg) do { \ | |
| 34 | if (!(cond)) { \ | |
| 35 | fprintf(stderr, " FAIL: %s [%s:%d]\n", msg, __FILE__, __LINE__); \ | |
| 36 | g_failures++; \ | |
| 37 | } \ | |
| 38 | } while(0) | |
| 39 | ||
| 40 | #define CHECK_EQ_U16(a, b, msg) CHECK((a) == (b), msg) | |
| 41 | #define CHECK_EQ_U32(a, b, msg) CHECK((a) == (b), msg) | |
| 42 | #define CHECK_EQ_U8(a, b, msg) CHECK((a) == (b), msg) | |
| 43 | ||
| 44 | #define CHECK_STR(buf, len, expected, msg) do { \ | |
| 45 | CHECK((len) == (uint16_t)strlen(expected), msg " (length)"); \ | |
| 46 | CHECK(memcmp(buf, expected, len) == 0, msg " (content)"); \ | |
| 47 | } while(0) | |
| 48 | ||
| 49 | /* --------------------------------------------------------------- | |
| 50 | * Fake OpenCog type IDs -- just small integers for testing. | |
| 51 | * In real AtomSpace, these come from atom_types.h generation. | |
| 52 | * --------------------------------------------------------------- */ | |
| 53 | #define CONCEPT_NODE 1 | |
| 54 | #define PREDICATE_NODE 2 | |
| 55 | #define NUMBER_NODE 3 | |
| 56 | #define LIST_LINK 10 | |
| 57 | #define EVAL_LINK 11 | |
| 58 | #define MEMBER_LINK 12 | |
| 59 | ||
| 60 | int main() | |
| 61 | { | |
| 62 | GpuAtomTable t; | |
| 63 | int rc; | |
| 64 | ||
| 65 | printf("=== GpuAtomTable CUDA test ===\n"); | |
| 66 | ||
| 67 | /* ---- 1. Alloc ---- */ | |
| 68 | printf("1. Allocating table...\n"); | |
| 69 | rc = gpu_table_alloc(&t); | |
| 70 | CHECK(rc == 0, "gpu_table_alloc succeeded"); | |
| 71 | if (rc != 0) { | |
| 72 | fprintf(stderr, "Cannot allocate GPU table. CUDA device required.\n"); | |
| 73 | return 1; | |
| 74 | } | |
| 75 | CHECK_EQ_U32(t.atom_count, 0, "atom_count initially 0"); | |
| 76 | CHECK_EQ_U32(t.name_pool_used, 0, "name_pool_used initially 0"); | |
| 77 | CHECK_EQ_U32(t.out_pool_used, 0, "out_pool_used initially 0"); | |
| 78 | ||
| 79 | /* ---- 2. Store 3 nodes ---- */ | |
| 80 | printf("2. Storing 3 nodes...\n"); | |
| 81 | ||
| 82 | /* Slot 0: ConceptNode "cat" */ | |
| 83 | rc = gpu_store_node(&t, 0, CONCEPT_NODE, "cat", 3); | |
| 84 | CHECK(rc == 0, "store node 0 (cat)"); | |
| 85 | ||
| 86 | /* Slot 1: ConceptNode "dog" */ | |
| 87 | rc = gpu_store_node(&t, 1, CONCEPT_NODE, "dog", 3); | |
| 88 | CHECK(rc == 0, "store node 1 (dog)"); | |
| 89 | ||
| 90 | /* Slot 2: PredicateNode "is-a" */ | |
| 91 | rc = gpu_store_node(&t, 2, PREDICATE_NODE, "is-a", 4); | |
| 92 | CHECK(rc == 0, "store node 2 (is-a)"); | |
| 93 | ||
| 94 | /* ---- 3. Store binary link: ListLink(cat, dog) at slot 3 ---- */ | |
| 95 | printf("3. Storing binary link...\n"); | |
| 96 | { | |
| 97 | uint32_t out2[2] = {0, 1}; /* cat, dog */ | |
| 98 | rc = gpu_store_link(&t, 3, LIST_LINK, out2, 2); | |
| 99 | CHECK(rc == 0, "store link 3 (ListLink)"); | |
| 100 | } | |
| 101 | ||
| 102 | /* ---- 4. Store ternary link: EvalLink(is-a, cat, dog) at slot 4 ---- */ | |
| 103 | printf("4. Storing ternary link...\n"); | |
| 104 | { | |
| 105 | uint32_t out3[3] = {2, 0, 1}; /* is-a, cat, dog */ | |
| 106 | rc = gpu_store_link(&t, 4, EVAL_LINK, out3, 3); | |
| 107 | CHECK(rc == 0, "store link 4 (EvalLink)"); | |
| 108 | } | |
| 109 | ||
| 110 | /* ---- 5. Store link-of-links: MemberLink(ListLink, EvalLink) at slot 5 ---- */ | |
| 111 | printf("5. Storing link containing links...\n"); | |
| 112 | { | |
| 113 | uint32_t out_links[2] = {3, 4}; /* slot 3 = ListLink, slot 4 = EvalLink */ | |
| 114 | rc = gpu_store_link(&t, 5, MEMBER_LINK, out_links, 2); | |
| 115 | CHECK(rc == 0, "store link 5 (MemberLink of links)"); | |
| 116 | } | |
| 117 | ||
| 118 | gpu_table_barrier(&t); | |
| 119 | ||
| 120 | /* ---- 6. Fetch everything back ---- */ | |
| 121 | printf("6. Fetching and verifying...\n"); | |
| 122 | ||
| 123 | /* Fetch node 0 (cat) */ | |
| 124 | { | |
| 125 | uint16_t ty; char nbuf[64]; uint16_t nlen; | |
| 126 | rc = gpu_fetch_node(&t, 0, &ty, nbuf, &nlen); | |
| 127 | CHECK(rc == 0, "fetch node 0"); | |
| 128 | CHECK_EQ_U16(ty, CONCEPT_NODE, "node 0 type == CONCEPT_NODE"); | |
| 129 | CHECK_STR(nbuf, nlen, "cat", "node 0 name == cat"); | |
| 130 | } | |
| 131 | ||
| 132 | /* Fetch node 1 (dog) */ | |
| 133 | { | |
| 134 | uint16_t ty; char nbuf[64]; uint16_t nlen; | |
| 135 | rc = gpu_fetch_node(&t, 1, &ty, nbuf, &nlen); | |
| 136 | CHECK(rc == 0, "fetch node 1"); | |
| 137 | CHECK_EQ_U16(ty, CONCEPT_NODE, "node 1 type == CONCEPT_NODE"); | |
| 138 | CHECK_STR(nbuf, nlen, "dog", "node 1 name == dog"); | |
| 139 | } | |
| 140 | ||
| 141 | /* Fetch node 2 (is-a) */ | |
| 142 | { | |
| 143 | uint16_t ty; char nbuf[64]; uint16_t nlen; | |
| 144 | rc = gpu_fetch_node(&t, 2, &ty, nbuf, &nlen); | |
| 145 | CHECK(rc == 0, "fetch node 2"); | |
| 146 | CHECK_EQ_U16(ty, PREDICATE_NODE, "node 2 type == PREDICATE_NODE"); | |
| 147 | CHECK_STR(nbuf, nlen, "is-a", "node 2 name == is-a"); | |
| 148 | } | |
| 149 | ||
| 150 | /* Fetch link 3 (ListLink(cat, dog)) */ | |
| 151 | { | |
| 152 | uint16_t ty; uint32_t obuf[8]; uint16_t ar; | |
| 153 | rc = gpu_fetch_link(&t, 3, &ty, obuf, &ar); | |
| 154 | CHECK(rc == 0, "fetch link 3"); | |
| 155 | CHECK_EQ_U16(ty, LIST_LINK, "link 3 type == LIST_LINK"); | |
| 156 | CHECK_EQ_U16(ar, 2, "link 3 arity == 2"); | |
| 157 | CHECK_EQ_U32(obuf[0], 0, "link 3 out[0] == 0 (cat)"); | |
| 158 | CHECK_EQ_U32(obuf[1], 1, "link 3 out[1] == 1 (dog)"); | |
| 159 | } | |
| 160 | ||
| 161 | /* Fetch link 4 (EvalLink(is-a, cat, dog)) */ | |
| 162 | { | |
| 163 | uint16_t ty; uint32_t obuf[8]; uint16_t ar; | |
| 164 | rc = gpu_fetch_link(&t, 4, &ty, obuf, &ar); | |
| 165 | CHECK(rc == 0, "fetch link 4"); | |
| 166 | CHECK_EQ_U16(ty, EVAL_LINK, "link 4 type == EVAL_LINK"); | |
| 167 | CHECK_EQ_U16(ar, 3, "link 4 arity == 3"); | |
| 168 | CHECK_EQ_U32(obuf[0], 2, "link 4 out[0] == 2 (is-a)"); | |
| 169 | CHECK_EQ_U32(obuf[1], 0, "link 4 out[1] == 0 (cat)"); | |
| 170 | CHECK_EQ_U32(obuf[2], 1, "link 4 out[2] == 1 (dog)"); | |
| 171 | } | |
| 172 | ||
| 173 | /* Fetch link 5 (MemberLink(ListLink, EvalLink)) -- link-of-links */ | |
| 174 | { | |
| 175 | uint16_t ty; uint32_t obuf[8]; uint16_t ar; | |
| 176 | rc = gpu_fetch_link(&t, 5, &ty, obuf, &ar); | |
| 177 | CHECK(rc == 0, "fetch link 5"); | |
| 178 | CHECK_EQ_U16(ty, MEMBER_LINK, "link 5 type == MEMBER_LINK"); | |
| 179 | CHECK_EQ_U16(ar, 2, "link 5 arity == 2"); | |
| 180 | CHECK_EQ_U32(obuf[0], 3, "link 5 out[0] == 3 (ListLink)"); | |
| 181 | CHECK_EQ_U32(obuf[1], 4, "link 5 out[1] == 4 (EvalLink)"); | |
| 182 | } | |
| 183 | ||
| 184 | /* ---- 7. Clear and verify ---- */ | |
| 185 | printf("7. Clearing table...\n"); | |
| 186 | gpu_table_clear(&t); | |
| 187 | CHECK_EQ_U32(t.atom_count, 0, "atom_count after clear == 0"); | |
| 188 | CHECK_EQ_U32(t.name_pool_used, 0, "name_pool_used after clear == 0"); | |
| 189 | CHECK_EQ_U32(t.out_pool_used, 0, "out_pool_used after clear == 0"); | |
| 190 | ||
| 191 | /* Verify slot 0 returns zeros after clear */ | |
| 192 | { | |
| 193 | uint16_t ty = 0xFFFF; char nbuf[64]; uint16_t nlen = 0xFFFF; | |
| 194 | rc = gpu_fetch_node(&t, 0, &ty, nbuf, &nlen); | |
| 195 | CHECK(rc == 0, "fetch node 0 after clear"); | |
| 196 | CHECK_EQ_U16(ty, 0, "node 0 type == 0 after clear"); | |
| 197 | CHECK_EQ_U16(nlen, 0, "node 0 name_len == 0 after clear"); | |
| 198 | } | |
| 199 | { | |
| 200 | uint16_t ty; uint32_t obuf[8]; uint16_t ar; | |
| 201 | rc = gpu_fetch_link(&t, 3, &ty, obuf, &ar); | |
| 202 | CHECK(rc == 0, "fetch link 3 after clear"); | |
| 203 | CHECK_EQ_U16(ty, 0, "link 3 type == 0 after clear"); | |
| 204 | CHECK_EQ_U16(ar, 0, "link 3 arity == 0 after clear"); | |
| 205 | } | |
| 206 | ||
| 207 | /* ---- 8. Free ---- */ | |
| 208 | printf("8. Freeing table...\n"); | |
| 209 | gpu_table_free(&t); | |
| 210 | ||
| 211 | /* ---- Summary ---- */ | |
| 212 | printf("\n"); | |
| 213 | if (g_failures == 0) { | |
| 214 | printf("ALL TESTS PASSED\n"); | |
| 215 | return 0; | |
| 216 | } else { | |
| 217 | printf("FAILED: %d check(s) failed\n", g_failures); | |
| 218 | return 1; | |
| 219 | } | |
| 220 | } |