OverviewFilesCommitsIssuesDocsReleasesPeers

atomspace-gpu commits / bb0ec4e

Restructure repo: move GPU AtomSpace kernels to opencog/gpu/
The GPU AtomSpace (SoA pools, hash tables, MI, cosine, clustering, learning loop) now lives in opencog/gpu/ as standalone CUDA/OpenCL with zero OpenCog dependencies. GpuStorageNode bridge stays in opencog/persist/gpu/. Added 13 kernel tests, 6 benchmarks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bb0ec4e9de9a1a85aaa6511884e128b817e8249c
by plankatron <fleeky@gmail.com> · 2026-02-10T02:51:42.000Z

Changed files (47) +16400 -816

.gitignore +12 -0
……@@ -1 +1,13 @@
11build/
2*.o
3*.so
4*.a
5*.pyc
6__pycache__/
7cscope.files
8cscope.out
9tags
10TAGS
11*.swp
12*.swo
13*~
CMakeLists.txt +5 -4
……@@ -1,5 +1,5 @@
11#
2# Master AtomSpace GPU Storage Backend CMake file.
2# Master AtomSpace GPU CMake file.
33#
44# General organization:
55# -- check for different compilers, OS'es
……@@ -139,6 +139,7 @@ INCLUDE(OpenCogCython)
139139
140140ADD_SUBDIRECTORY(cmake)
141141ADD_SUBDIRECTORY(opencog)
142ADD_SUBDIRECTORY(benchmarks)
142143
143144INCLUDE(OpenCogTestOptions)
144145OPENCOG_SETUP_TESTING()
……@@ -166,7 +167,7 @@ SET(CPACK_GENERATOR "DEB")
166167SET(CPACK_PACKAGE_CONTACT "opencog@googlegroups.com")
167168SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
168169SET(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}/packages")
169SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "The AtomSpace GPU Storage Backend (CUDA + OpenCL)")
170SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "The AtomSpace GPU (CUDA + OpenCL)")
170171SET(CPACK_PACKAGE_NAME "atomspace-gpu-dev")
171172SET(CPACK_PACKAGE_VENDOR "opencog.org")
172173SET(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}-${UTC_DATE}")
……@@ -196,8 +197,8 @@ FIND_PACKAGE(Doxygen)
196197# ===================================================================
197198# Show a summary of what we found, what we will do.
198199
199SUMMARY_ADD("CUDA Backend" "CUDA GPU StorageNode" HAVE_CUDA)
200SUMMARY_ADD("OpenCL Backend" "OpenCL GPU StorageNode" HAVE_OPENCL)
200SUMMARY_ADD("CUDA Backend" "CUDA GPU AtomSpace" HAVE_CUDA)
201SUMMARY_ADD("OpenCL Backend" "OpenCL GPU AtomSpace" HAVE_OPENCL)
201202SUMMARY_ADD("Doxygen" "Code documentation" DOXYGEN_FOUND)
202203SUMMARY_ADD("Unit tests" "Unit tests" CXXTEST_FOUND)
203204
README.md +40 -89
……@@ -1,60 +1,48 @@
11
2AtomSpace GPU Storage Backend
2AtomSpace GPU
33=====================================
44
5A GPU-backed [StorageNode](https://wiki.opencog.org/w/StorageNode)
6for the [OpenCog AtomSpace](https://github.com/opencog/atomspace).
7Allows atoms and values to be stored in GPU VRAM for high-throughput
8GPU-resident computation.
5An [AtomSpace](https://github.com/opencog/atomspace) on the GPU.
96
10Same API as [RocksStorageNode](https://github.com/opencog/atomspace-rocks)
11(disk) and [CogStorageNode](https://github.com/opencog/atomspace-cog)
12(network), but backed by GPU SoA (Structure-of-Arrays) pools.
7Stores atoms as flattened Structure-of-Arrays (SoA) in VRAM so thousands
8of GPU threads can operate on them simultaneously. Kernels compute MI,
9cosine similarity, clustering, connected components, and the full
10learning loop (count → MI → cluster → grammar) directly on the GPU.
11
12[GpuStorageNode](https://wiki.opencog.org/w/StorageNode) is the bridge
13that sends atoms from the CPU to the AtomSpace on the GPU and fetches
14results back. Same API as
15[RocksStorageNode](https://github.com/opencog/atomspace-rocks) (disk)
16and [CogStorageNode](https://github.com/opencog/atomspace-cog) (network).
1317
1418Supports **CUDA** (preferred for NVIDIA GPUs) and **OpenCL** (fallback
15for AMD/Intel). At least one must be available at build time. Backend
16is selected at runtime based on URI and hardware availability.
19for AMD/Intel). At least one must be available at build time.
1720
1821Architecture
1922------------
2023```
21CPU AtomSpace
22 |
23 +-- RocksStorageNode -> disk (RocksDB)
24 +-- CogStorageNode -> network (CogServer)
25 +-- GpuStorageNode -> GPU VRAM (CUDA or OpenCL)
26 |
27 +-- GpuBackend (abstract interface)
28 | |
29 | +-- CudaBackend (preferred, NVIDIA)
30 | +-- OpenCLBackend (fallback, AMD/Intel/NVIDIA)
31 |
32 +-- WordPool 128K nodes (name_hash, count, marginal, class_id)
33 +-- PairPool 4M binary links (word_a, word_b, count, mi, flags)
34 +-- SectionPool 1M sections (word, disjunct_hash, count)
24CPU (control plane) GPU (the AtomSpace)
25 Scheme / C++ code SoA pools + kernels
26 │ │
27 ├── storeAtom() ──────────→ │ receives atom
28 ├── getAtom() ←────────── │ returns results
29 └── runQuery() ──────────→ │ runs computation
3530```
3631
37Each pool uses a lock-free GPU hash table for O(1) lookup and a bump
38allocator for slot allocation. CUDA backend uses device memory with
39managed staging buffers; OpenCL backend uses cl::Buffer with JIT
40kernel compilation.
32The repo has two layers with different dependencies:
4133
42URI Format
43----------
44```
45gpu://: first available device (CUDA preferred)
46gpu://NVIDIA:RTX NVIDIA device containing "RTX" (auto backend)
47gpu://cuda:: force CUDA backend, any device
48gpu://opencl:: force OpenCL backend, any device
49gpu://opencl:Intel: OpenCL on first Intel device
50```
34- **`opencog/gpu/`** -- The GPU AtomSpace itself. Fully standalone: pure
35 CUDA and OpenCL with zero OpenCog dependencies. Can be built and used
36 independently for any GPU computing project.
37- **`opencog/persist/gpu/`** -- The GpuStorageNode bridge (~350 lines).
38 Implements the StorageNode API so the CPU AtomSpace can send atoms to
39 the GPU. Requires [CogUtil](https://github.com/opencog/cogutil),
40 [AtomSpace](https://github.com/opencog/atomspace), and
41 [AtomSpace-Storage](https://github.com/opencog/atomspace-storage).
5142
5243Building
5344--------
54Prerequisites: [CogUtil](https://github.com/opencog/cogutil),
55[AtomSpace](https://github.com/opencog/atomspace),
56[AtomSpace-Storage](https://github.com/opencog/atomspace-storage),
57and at least one of:
45Requires at least one of:
5846- **CUDA Toolkit** (11.0+) for NVIDIA GPUs
5947- **OpenCL** (1.2+ runtime and headers) for AMD/Intel/NVIDIA GPUs
6048
……@@ -66,81 +54,44 @@ make test
6654sudo make install
6755```
6856
69CMake will auto-detect available backends. To force a specific backend:
70```bash
71cmake .. -DHAVE_CUDA=OFF # OpenCL only
72cmake .. -DHAVE_OPENCL=OFF # CUDA only (not recommended)
73```
74
75Usage (C++)
76-----------
57Usage
58-----
7759```cpp
7860#include <opencog/persist/gpu/GpuStorageNode.h>
7961
80AtomSpacePtr as = createAtomSpace();
8162Handle hsn = as->add_node(GPU_STORAGE_NODE, std::string("gpu://:"));
8263GpuStorageNodePtr store = GpuStorageNodeCast(hsn);
64store->open();
8365
84store->open(); // Selects CUDA if available, else OpenCL
85
86// Store a node with values
87Handle a(createNode(SCHEMA_NODE, "someWord"));
66Handle a(createNode(CONCEPT_NODE, "someWord"));
8867a->setValue(truth_key(), createFloatValue({42.0, 3.7}));
8968store->storeAtom(a, true);
9069store->barrier();
9170
92// Fetch it back
93Handle b(createNode(SCHEMA_NODE, "someWord"));
94store->getAtom(b);
95// b now has the same FloatValue
71Handle b(createNode(CONCEPT_NODE, "someWord"));
72store->getAtom(b); // b now has the same FloatValue
9673
9774store->close();
9875```
9976
100Usage (Scheme)
101--------------
77Or from Scheme:
10278```scheme
10379(use-modules (opencog) (opencog persist) (opencog persist-gpu))
104
10580(define gpu (GpuStorageNode "gpu://:"))
10681(cog-open gpu)
107
108(define a (Concept "someWord"))
109(cog-set-value! a (Predicate "*-TruthValueKey-*")
110 (FloatValue 42.0 3.7))
111(store-atom a)
112(barrier gpu)
113
114; Fetch back
115(define b (Concept "someWord"))
116(fetch-atom b)
117
82(store-atom (Concept "someWord"))
83(fetch-atom (Concept "someWord"))
11884(cog-close gpu)
11985```
12086
121GPU Memory Usage
122----------------
123| Pool | Capacity | Hash Table | GPU Memory |
124|----------|----------|------------|------------|
125| Words | 128K | 256K | ~4 MB |
126| Pairs | 4M | 8M | ~160 MB |
127| Sections | 1M | 2M | ~40 MB |
128| **Total** | | | **~204 MB** |
129
13087Phases
13188------
132- **Phase 1** (complete): Store/fetch round-trip. 8/8 tests pass.
89- **Phase 1** (complete): Store/fetch round-trip. 13/13 tests pass.
13390- **Phase 2** (planned): `runQuery()` -- execute Atomese expressions on GPU.
13491- **Phase 3** (planned): JIT compilation -- fused kernels from AST.
13592
13693Known Limitations
13794-----------------
13895- GPU storage is **volatile** -- data is lost when the connection closes.
139 Use RocksStorageNode for persistence.
140- Only `truth_key()` values are stored. Other value keys are silently
141 ignored (GPU has fixed SoA layout).
142- Only binary links map to the pair pool. Higher-arity links store
143 their outgoing nodes but not link-level values.
96- Only `truth_key()` values are stored (GPU has fixed SoA layout).
14497- CUDA backend requires Compute Capability 5.0+ (Maxwell or newer).
145- OpenCL backend requires `enqueueWriteBuffer` with `CL_TRUE` (blocking)
146 when the host buffer goes out of scope immediately.
benchmarks/CMakeLists.txt +33 -0
……@@ -0,0 +1,33 @@
1#
2# GPU AtomSpace benchmarks.
3#
4
5IF (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()
25ENDIF (HAVE_CUDA)
26
27IF (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)
33ENDIF (HAVE_OPENCL)
benchmarks/bench-corpus-ck.cu +527 -0
……@@ -0,0 +1,527 @@
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
32extern "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
53static 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
61typedef struct {
62 uint32_t* words;
63 uint32_t length;
64} Sentence;
65
66typedef struct {
67 Sentence* sentences;
68 uint32_t num_sentences;
69 uint32_t num_words;
70} Corpus;
71
72static 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
99static 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
107typedef struct {
108 char word[64];
109 uint32_t id;
110} VocabEntry;
111
112static VocabEntry* vocab_table = NULL;
113static uint32_t vocab_count = 0;
114
115static 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
130static 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
142int 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}
benchmarks/bench-corpus-feeder.c +231 -0
……@@ -0,0 +1,231 @@
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
35typedef struct {
36 char word[MAX_WORD_LEN];
37 uint32_t id;
38} VocabEntry;
39
40static VocabEntry vocab[MAX_VOCAB];
41static uint32_t vocab_size = 0;
42
43/* djb2 hash */
44static 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
53static 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
77typedef struct {
78 uint32_t words[MAX_SENT_LEN];
79 uint32_t length;
80} Sentence;
81
82static Sentence sentences[MAX_SENTENCES];
83static uint32_t num_sentences = 0;
84
85/* Detect Gutenberg header/footer lines */
86static 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 */
95static 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 */
147static 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
191int 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}
benchmarks/bench-corpus.cu +432 -0
……@@ -0,0 +1,432 @@
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
33extern "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
48static 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
56typedef struct {
57 uint32_t* words;
58 uint32_t length;
59} Sentence;
60
61typedef struct {
62 Sentence* sentences;
63 uint32_t num_sentences;
64 uint32_t num_words; /* vocabulary size */
65} Corpus;
66
67static 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
95static 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
104typedef struct {
105 char word[64];
106 uint32_t id;
107} VocabEntry;
108
109static 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
124static 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
135int 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}
benchmarks/bench-kernels.cu +711 -0
……@@ -0,0 +1,711 @@
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
30extern "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
82struct BenchStats {
83 double min_ms, max_ms, mean_ms, stddev_ms;
84};
85
86static 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
103static 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 */
113static 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 */
132static 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 */
149static 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
183static 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
284static 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
389static 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
550static 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
670int 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}
benchmarks/bench-mi-comparison.cu +325 -0
……@@ -0,0 +1,325 @@
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
25namespace cg = cooperative_groups;
26
27/* ─── Forward declarations ─── */
28
29extern "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
37static 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
46static 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
54static 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
106struct 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
116static 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
272int 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}
benchmarks/bench-pipeline.cu +420 -0
……@@ -0,0 +1,420 @@
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
31extern "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
46static 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
55static 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 */
62static 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
90static 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
97static 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
110struct 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
124static 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
247static 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
309static 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
358int 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}
opencog/CMakeLists.txt +1 -0
……@@ -2,6 +2,7 @@
22# This declaration must happen before subdirectory traversal.
33DECLARE_GUILE_CONFIG_TARGET(SCM_CONFIG "opencog gpu-config" "OPENCOG_TEST")
44
5ADD_SUBDIRECTORY (gpu)
56ADD_SUBDIRECTORY (persist)
67
78# ------------------------------------------------------------------
opencog/gpu/CMakeLists.txt +52 -0
……@@ -0,0 +1,52 @@
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) --
10SET(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
20INSTALL(FILES ${OPENCL_KERNEL_FILES}
21 DESTINATION "share/opencog/opencl/gpu"
22)
23
24# -- CUDA kernels (compiled into shared library) --
25IF (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 )
47ENDIF (HAVE_CUDA)
48
49# -- Header files --
50INSTALL(FILES gpu-learning-types.h
51 DESTINATION "include/opencog/gpu"
52)
opencog/gpu/gpu-atomspace.cl +407 -0
……@@ -0,0 +1,407 @@
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 */
27void 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 */
155inline 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 */
250inline 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}
opencog/gpu/gpu-compartment-kernel.cu +771 -0
……@@ -0,0 +1,771 @@
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
32namespace cg = cooperative_groups;
33
34/* ─── Hash function (same splitmix64 as gpu-learning-loop.cu) ─── */
35
36__device__ __forceinline__
37uint64_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__
49void 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__
71uint32_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__
105void 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__
126void 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__
208void 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__
237void 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__
276void 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__
369double 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
597extern "C" {
598
599/* Initialize compartment-aware learning state */
600LearningState* 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 */
633CompartmentState* 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) */
656SentenceRing* 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) */
664void 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 */
682int 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 */
724void ck_wait() {
725 cudaDeviceSynchronize();
726}
727
728/* Read class assignments */
729void 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 */
735void 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) */
743int ck_get_num_sms() {
744 int num_sms = 0;
745 cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0);
746 return num_sms;
747}
748
749/* Memory report */
750void 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" */
opencog/gpu/gpu-connected-components.cu +398 -0
……@@ -0,0 +1,398 @@
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
38struct 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
46struct CCLabels {
47 uint32_t* label; /* label[word_idx] = component root */
48 uint32_t num_words; /* active word count */
49};
50
51struct 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. */
226extern "C"
227int 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). */
333extern "C"
334float 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}
opencog/gpu/gpu-connector-rewrite.cu +404 -0
……@@ -0,0 +1,404 @@
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__
31uint64_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__
43uint64_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__
52void 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__
242double 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) */
270extern "C"
271void 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 */
283extern "C"
284void 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) */
348extern "C"
349double 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 */
389extern "C"
390uint32_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}
opencog/gpu/gpu-cosine.cl +340 -0
……@@ -0,0 +1,340 @@
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}
opencog/gpu/gpu-counting.cl +296 -0
……@@ -0,0 +1,296 @@
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}
opencog/gpu/gpu-divergence.cu +284 -0
……@@ -0,0 +1,284 @@
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__
35uint64_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__
47uint64_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
231extern "C"
232void 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}
opencog/gpu/gpu-hashtable.cl +303 -0
……@@ -0,0 +1,303 @@
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 */
40inline 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}
opencog/gpu/gpu-learning-bridge.cu +215 -0
……@@ -0,0 +1,215 @@
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
22extern "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
39static LearningState* g_state = NULL;
40static SentenceRing* g_ring = NULL;
41static int* g_done_flag = NULL;
42static int* g_pause_flag = NULL;
43static uint32_t* g_stats_iteration = NULL;
44static uint32_t* g_stats_pairs = NULL;
45static uint32_t* g_stats_classes = NULL;
46static double* g_stats_entropy = NULL;
47static int g_launched = 0;
48
49extern "C" {
50
51/* Initialize the GPU learning system */
52int 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) */
98int 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) */
123void 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) */
129void gpu_cuda_pause() {
130 if (g_pause_flag) *g_pause_flag = 1;
131}
132
133void gpu_cuda_resume() {
134 if (g_pause_flag) *g_pause_flag = 0;
135}
136
137/* Poll current status (non-blocking) */
138int 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 */
157void gpu_cuda_stop() {
158 if (g_done_flag) *g_done_flag = 1;
159}
160
161/* Wait for kernel to finish and read class assignments */
162int 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) */
176int 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 */
185void 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" */
opencog/gpu/gpu-learning-loop.cu +526 -0
……@@ -0,0 +1,526 @@
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
27namespace cg = cooperative_groups;
28
29/* ─── Hash function ─── */
30
31__device__ __forceinline__
32uint64_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__
44void 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__
63uint32_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__
105void 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__
127void 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__
166void 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__
263double 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
411extern "C" {
412
413/* Initialize learning state */
414LearningState* 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 */
442SentenceRing* 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) */
450void 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 */
468int 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 */
509void ll_wait() {
510 cudaDeviceSynchronize();
511}
512
513/* Read class assignments (from unified memory — no copy needed) */
514void 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 */
521void ll_shutdown(LearningState* state, SentenceRing* ring) {
522 cudaFree(state);
523 cudaFree(ring);
524}
525
526} /* extern "C" */
opencog/gpu/gpu-learning-types.h +138 -0
……@@ -0,0 +1,138 @@
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
45struct 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
105struct 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 */
126struct SentenceSlot {
127 uint32_t words[LL_MAX_SENTENCE_LEN];
128 uint32_t length;
129 uint32_t ready;
130};
131
132struct 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 */
opencog/gpu/gpu-mi.cl +224 -0
……@@ -0,0 +1,224 @@
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}
opencog/gpu/gpu-sections.cl +317 -0
……@@ -0,0 +1,317 @@
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
35inline ulong fnv1a_init(void)
36{
37 return 0xcbf29ce484222325UL;
38}
39
40inline ulong fnv1a_mix(ulong hash, ulong val)
41{
42 hash ^= val;
43 hash *= 0x100000001b3UL;
44 return hash;
45}
46
47inline 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
71inline 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. */
opencog/gpu/gpu-spanning-forest.cu +506 -0
……@@ -0,0 +1,506 @@
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__
42uint64_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__
54uint64_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__
63double 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__
90double 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__
117double 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 */
447extern "C"
448void 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}
opencog/gpu/gpu-substitute.cl +232 -0
……@@ -0,0 +1,232 @@
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}
opencog/persist/gpu/CMakeLists.txt +0 -8
……@@ -61,11 +61,3 @@ INSTALL (FILES ${GPU_INSTALL_HEADERS}
6161 DESTINATION "include/opencog/persist/gpu"
6262)
6363
64# Install OpenCL kernel files (needed at runtime for JIT compilation)
65IF (HAVE_OPENCL)
66 INSTALL (FILES
67 gpu-hashtable.cl
68 gpu-atomspace.cl
69 DESTINATION "share/opencog/opencl/gpu"
70 )
71ENDIF (HAVE_OPENCL)
opencog/persist/gpu/OpenCLBackend.cc +3 -3
……@@ -85,9 +85,9 @@ void OpenCLBackend::compile_kernels()
8585 "/usr/share/opencog/opencl/gpu/",
8686 };
8787
88 search_paths.push_back("opencog/persist/gpu/");
89 search_paths.push_back("../opencog/persist/gpu/");
90 search_paths.push_back("../../opencog/persist/gpu/");
88 search_paths.push_back("opencog/gpu/");
89 search_paths.push_back("../opencog/gpu/");
90 search_paths.push_back("../../opencog/gpu/");
9191
9292 for (const auto& prefix : search_paths)
9393 {
opencog/persist/gpu/gpu-atomspace.cl +0 -407
……@@ -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 */
27void 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 */
155inline 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 */
250inline 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}
opencog/persist/gpu/gpu-hashtable.cl +0 -303
……@@ -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 */
40inline 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}
opencog/persist/gpu/gpu-pool-defs.h +7 -1
……@@ -12,7 +12,13 @@
1212#ifndef _OPENCOG_GPU_POOL_DEFS_H
1313#define _OPENCOG_GPU_POOL_DEFS_H
1414
15// Pool capacity defaults (can be overridden at compile time)
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
1622#ifndef GPU_WORD_CAPACITY
1723#define GPU_WORD_CAPACITY 131072 // 128K words
1824#endif
tests/CMakeLists.txt +4 -1
……@@ -4,7 +4,10 @@ INCLUDE(AddCxxtest)
44# All tests should load the atomspace scm from the build dir
55SET(GUILE_LOAD_PATH "${PROJECT_BINARY_DIR}/opencog/scm")
66
7# Perform tests in component-dependency order.
7# GPU AtomSpace kernel tests (standalone, no CxxTest needed)
8ADD_SUBDIRECTORY (gpu)
9
10# StorageNode tests (require CxxTest)
811IF (CXXTEST_FOUND)
912 ADD_SUBDIRECTORY (persist)
1013ENDIF (CXXTEST_FOUND)
tests/gpu/CMakeLists.txt +54 -0
……@@ -0,0 +1,54 @@
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 --
8IF (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()
28ENDIF (HAVE_OPENCL)
29
30# -- CUDA kernel tests --
31IF (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()
54ENDIF (HAVE_CUDA)
tests/gpu/test-atomspace.c +739 -0
……@@ -0,0 +1,739 @@
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
51char* 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
68uint64_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
81double 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
90int 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}
tests/gpu/test-compartment-kernel.cu +639 -0
……@@ -0,0 +1,639 @@
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 */
19extern "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
46static 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
52static 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 */
62struct 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
71static 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
88static 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 */
98static 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
116static 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
171static 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
227static 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
299static 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
429static 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
507static 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
600int 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}
tests/gpu/test-connected-components.cu +444 -0
……@@ -0,0 +1,444 @@
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 */
20extern "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
42extern "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
67struct 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
83void 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
100void 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
116int 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
255int 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
432int 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}
tests/gpu/test-connector-rewrite.cu +320 -0
……@@ -0,0 +1,320 @@
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 */
20extern "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
28extern "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
32extern "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
37int 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
182int 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
308int 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}
tests/gpu/test-cosine.c +954 -0
……@@ -0,0 +1,954 @@
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
53char* 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
70double 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
79char* 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
97void 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
141void 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
162void 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
180void 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
227int 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}
tests/gpu/test-counting.c +720 -0
……@@ -0,0 +1,720 @@
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
47char* 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
64double 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
73void 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
104int 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}
tests/gpu/test-divergence.cu +224 -0
……@@ -0,0 +1,224 @@
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
25extern "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
47int 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
214int 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}
tests/gpu/test-hashtable.c +532 -0
……@@ -0,0 +1,532 @@
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
46char* 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
62static uint64_t rng_state = 0x12345678DEADBEEFULL;
63
64uint64_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
75double 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
84int 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}
tests/gpu/test-learning-loop.cu +393 -0
……@@ -0,0 +1,393 @@
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
23extern "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 */
43void 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
55int 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
184int 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
270int 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
379int 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}
tests/gpu/test-mi.c +961 -0
……@@ -0,0 +1,961 @@
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
51char* 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
68double 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
77double 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
86void 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
116int 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}
tests/gpu/test-sections.c +883 -0
……@@ -0,0 +1,883 @@
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
48char* 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
65double 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
74static uint64_t cpu_fnv1a_init(void) { return 0xcbf29ce484222325ULL; }
75
76static uint64_t cpu_fnv1a_mix(uint64_t hash, uint64_t val)
77{
78 hash ^= val;
79 hash *= 0x100000001b3ULL;
80 return hash;
81}
82
83static 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) */
95static 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
104void 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
128int 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}
tests/gpu/test-spanning-forest.cu +606 -0
……@@ -0,0 +1,606 @@
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 */
22extern "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
43struct 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
57uint64_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
66void 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
77void 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
97void 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
106void 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
113int 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
127int 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
318int 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
594int 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}
tests/gpu/test-substitute.c +767 -0
……@@ -0,0 +1,767 @@
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
48char* 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
65double 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
74int 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}