| … | … | @@ -0,0 +1,84 @@ |
| 1 | # GPU AtomSpace Query Architecture |
| 2 | |
| 3 | Status: IMPLEMENTED + verified (2026-06-25), branch `gpu-backing-store`. |
| 4 | Scope: how a `GpuStorageNode`-backed AtomSpace stores atoms on the GPU, serves |
| 5 | graph traversal from the GPU, and answers queries — with results verified |
| 6 | byte-identical to the in-RAM opencog pattern matcher (`cog-execute!`). |
| 7 | |
| 8 | ## Layers (bottom to top) |
| 9 | |
| 10 | ### 1. GpuAtomTable — atoms in GPU RAM (Linas Steps 2/3) |
| 11 | - AoS: one 12-byte `GpuAtom` per slot `{type, is_node, data_offset, data_len}`. |
| 12 | - Variable-length side pools: `name_pool` (node names), `out_pool` (link outgoing |
| 13 | slot-ids). N-ary links; links may reference links (link-of-links). |
| 14 | - C API: `gpu_store_node/link`, `gpu_fetch_node/link`, alloc/free/clear/barrier. |
| 15 | - No ContentHash / flags / Values (deferred — Linas Step 4 is the Values |
| 16 | slice-table, still open). |
| 17 | |
| 18 | ### 2. Incoming-set index (CSR) — the traversal primitive |
| 19 | The pattern matcher's hot operation is "what links reference this atom?" (the |
| 20 | incoming set). Built on demand from the stored links: |
| 21 | - `gpu_table_build_incoming(t)` — count → host exclusive-scan → scatter, producing |
| 22 | CSR `in_off[CAPACITY+1]` + `in_pool[]` (device). Two CUDA kernels |
| 23 | (`k_count_incoming`, `k_scatter_incoming`). |
| 24 | - `gpu_fetch_incoming(t, slot, buf, max, *count)` — incoming link-slots of an atom. |
| 25 | - `gpu_intersect_incoming(t, a, b, ...)` — GPU-resident join: links referencing |
| 26 | BOTH a and b (one block, thread-strided membership test). |
| 27 | - Total incoming entries == total outgoing entries → `in_pool` sized to `out_pool`. |
| 28 | |
| 29 | ### 3. GpuStorageNode — BackingStore integration |
| 30 | Implements the real opencog `BackingStore`/`StorageNode` contract: |
| 31 | - `storeAtom` (recursive, outgoing-first), `loadAtomSpace` (two-pass nodes→links), |
| 32 | `storeAtomSpace`, `barrier`. TLB maps Handle↔slot (slot = UUID). |
| 33 | - `fetchIncomingSet`/`fetchIncomingByType` — served from the GPU incoming index: |
| 34 | Handle→slot → `gpu_fetch_incoming` → reconstruct each link (`gpu_fetch_link` + |
| 35 | TLB slot→Handle) into the target AtomSpace, type-filtered. Lazy |
| 36 | `ensure_incoming()`; `do_store_atom` invalidates the index. |
| 37 | - `storeValue`/`loadValue` are no-ops (no Value persistence yet) so the default |
| 38 | `runQuery` caching path does not throw. |
| 39 | |
| 40 | ### 4. runQuery — GPU-resident matcher + fallback |
| 41 | `runQuery(query, key, meta, fresh)`: |
| 42 | 1. `gpu_run_meet(query, key)` — GPU fast-path (below). If handled, result placed |
| 43 | at `key` as a UnisetValue; return. |
| 44 | 2. else `BackingStore::runQuery(...)` — the default crawl, which still pulls |
| 45 | incoming sets from the GPU via `fetchIncomingByType` (so it is GPU-served). |
| 46 | |
| 47 | #### GPU fast-path supported class |
| 48 | `(Meet <vardecl> body)` with **exactly one variable**, body = a single clause or |
| 49 | an `AndLink` of clauses, each clause one of: |
| 50 | - (a) direct binary link `(T $X c)` / `(T c $X)` — any binary LinkType (e.g. |
| 51 | InheritanceLink, MemberLink); |
| 52 | - (b) Eval relation `(Evaluation (Predicate P) (List $X c))` / `(List c $X)`. |
| 53 | |
| 54 | Per clause, candidate var-bindings are computed via the GPU incoming index |
| 55 | (shape (a): one hop; shape (b): two hops, List then Eval, filtered by predicate). |
| 56 | The answer is the **intersection** of the per-clause candidate sets. Result atoms |
| 57 | are resolved via the TLB and returned as a UnisetValue at `key`. |
| 58 | |
| 59 | Anything else (multi-variable joins, nesting, globs, negation, …) returns false |
| 60 | → default path. Correctness is always preserved by the fallback. |
| 61 | |
| 62 | ## Verification (tests/persist/gpu/) |
| 63 | - `test_incoming.cu` — incoming-set + `gpu_intersect_incoming` exact on a |
| 64 | hand-built graph incl. link-of-link. |
| 65 | - `GpuBasicSaveUTest` (12 cases): store/load/roundtrip; `fetchIncomingByType`; |
| 66 | **gold check** (GPU query == in-RAM `cog-execute!`, identical sets); V1 crisp |
| 67 | multi-hop; V4 neurosymbolic 3-fact composition; agent loop (transitive closure |
| 68 | persisted on GPU); `runQuery` GPU override; **general multi-clause** runQuery |
| 69 | (is-a mammal AND lives-in water → {whale,dolphin,seal}). |
| 70 | - Scale (standalone, gpu-atomese-test): 1M atoms, ~90M two-clause intersections/s. |
| 71 | |
| 72 | ## Known limits / next steps |
| 73 | 1. **Multi-variable joins** (chains like R($X,$Y) ∧ S($Y,$Z)) fall back to the |
| 74 | default engine. Next: a backtracking / worst-case-optimal-join GPU matcher. |
| 75 | 2. **Intersection is host-side** of GPU-fetched candidate sets; the heavy incoming |
| 76 | traversal is GPU. A full CUDA-kernel multiway intersection (cf. the standalone |
| 77 | 1M-scale proto) is the perf upgrade — needs per-relation CSR on device. |
| 78 | 3. **Values slice-table** (Linas Step 4) unimplemented → no Value persistence; |
| 79 | `storeValue`/`loadValue` are no-ops. This is the genuinely-open design question |
| 80 | (what algorithm drives the slice-table). |
| 81 | 4. RT cores are deliberately NOT used: BVH traversal is metric/spatial; the query |
| 82 | primitive (neighbor-list set-intersection) is control-flow-divergent and a |
| 83 | verified poor fit for RT (it only suits an embedding-based candidate pre-filter). |
| 84 | The matcher runs on CUDA cores; tensor cores are reserved for Values/SpGEMM. |