// proto-C-word-agents.cu
// ---------------------------------------------------------------------------
// PROTOTYPE C: word-as-agent + instancing, vs the cosine baseline.
//
// Reframe (HARDWARE-KB.md §H): instead of an outside observer computing a
// global cosine matrix between word-pairs, each WORD is an AGENT whose
// world-model is its set of co-occurrence neighbours. Two word-agents are
// "similar" if they MATE well = their neighbour sets overlap (set-intersection,
// the Prototype-A primitive). One GPU block = one agent (1 mini-engine = 1
// agent). Each word-TYPE is a single agent; its many occurrences in the corpus
// are cheap instances that all feed the one shared neighbour-set (instancing).
//
// Question answered: does the agent (overlap) view recover the same word groups
// as cosine? Verified on a synthetic corpus with KNOWN planted classes.
//
// Build: nvcc -O2 -arch=sm_75 -o proto-C proto-C-word-agents.cu
// Run: ./proto-C
// ---------------------------------------------------------------------------
#include <cstdio>
#include <cstdint>
#include <cmath>
#include <vector>
#include <algorithm>
#include <random>
#include <cuda_runtime.h>
#define CUDA_OK(c) do{cudaError_t e=(c); if(e!=cudaSuccess){\
printf("CUDA ERR %s:%d %s\n",__FILE__,__LINE__,cudaGetErrorString(e));return -1;}}while(0)
// Per-word neighbour CSR: row_off[W+1], col_idx[] (sorted neighbour word-ids),
// cnt[] (co-occurrence count, parallel to col_idx), norm[W] (sqrt sum cnt^2).
// One BLOCK per source agent `a`. Threads stride candidate agents `b`,
// serially merge the two sorted neighbour lists -> intersection size (for
// Jaccard) and count-dot (for cosine). Writes both similarities for (a,b).
__global__ void agent_similarity(
const uint32_t* __restrict__ row_off,
const uint32_t* __restrict__ col_idx,
const float* __restrict__ cnt,
const float* __restrict__ norm,
int W,
float* __restrict__ jac, // [W*W]
float* __restrict__ cos) // [W*W]
{
int a = blockIdx.x; if(a>=W) return;
uint32_t aS=row_off[a], aE=row_off[a+1]; int lenA=aE-aS;
for(int b=threadIdx.x; b<W; b+=blockDim.x){
uint32_t bS=row_off[b], bE=row_off[b+1]; int lenB=bE-bS;
int i=aS, j=bS, inter=0; float dot=0.f;
while(i<(int)aE && j<(int)bE){
uint32_t va=col_idx[i], vb=col_idx[j];
if(va==vb){ inter++; dot+=cnt[i]*cnt[j]; i++; j++; }
else if(va<vb) i++; else j++;
}
int uni = lenA+lenB-inter;
jac[(size_t)a*W+b] = uni? (float)inter/(float)uni : 0.f;
float den = norm[a]*norm[b];
cos[(size_t)a*W+b] = den>0.f? dot/den : 0.f;
}
}
// top-k peers of word a by a similarity matrix (excludes self), host-side.
static std::vector<int> topk(const std::vector<float>& sim,int W,int a,int k){
std::vector<int> idx; for(int b=0;b<W;b++) if(b!=a) idx.push_back(b);
std::partial_sort(idx.begin(), idx.begin()+std::min(k,(int)idx.size()), idx.end(),
[&](int x,int y){ return sim[(size_t)a*W+x] > sim[(size_t)a*W+y]; });
idx.resize(std::min(k,(int)idx.size())); return idx;
}
int main(){
printf("=== Prototype C: word-as-agent vs cosine ===\n\n");
// ---- Synthetic "book" with PLANTED classes -------------------------
// C classes; each class has M content words + K context words.
// A sentence picks a class, emits content+context words of that class,
// with small cross-class noise. Co-occurrence => content words in the
// same class share context neighbours => should cluster together.
const int C=4, M=8, K=6; // 4 classes
const int CONTENT=C*M, CONTEXT=C*K, W=CONTENT+CONTEXT; // 32+24 = 56 words
auto content_id=[&](int c,int m){ return c*M+m; };
auto context_id=[&](int c,int k){ return CONTENT + c*K+k; };
auto class_of =[&](int w){ return w<CONTENT ? w/M : (w-CONTENT)/K; };
std::mt19937 rng(7);
const int SENTENCES=4000;
// token stream (demonstrates instancing: many tokens -> W type-agents)
std::vector<std::vector<int>> sents;
long tokens=0;
for(int s=0;s<SENTENCES;s++){
int c = rng()%C;
std::vector<int> snt;
int len = 6 + rng()%6;
for(int t=0;t<len;t++){
int r = rng()%100;
if(r<8){ // 8% cross-class noise: a word from a random class
int cc=rng()%C; snt.push_back( (rng()%2)? content_id(cc,rng()%M):context_id(cc,rng()%K)); }
else if(r<54) snt.push_back(content_id(c, rng()%M));
else snt.push_back(context_id(c, rng()%K));
}
tokens+=snt.size(); sents.push_back(std::move(snt));
}
printf("Instancing: %ld tokens in %d sentences -> %d word-type agents\n\n", tokens, SENTENCES, W);
// ---- Co-occurrence (window +/-2) -> per-word neighbour counts -------
std::vector<std::vector<float>> cooc(W, std::vector<float>(W,0.f));
const int WIN=2;
for(auto& snt:sents) for(int i=0;i<(int)snt.size();i++)
for(int j=std::max(0,i-WIN); j<=std::min((int)snt.size()-1,i+WIN); j++)
if(i!=j) cooc[snt[i]][snt[j]] += 1.f;
// CSR (sorted neighbour ids + counts) + norms
std::vector<uint32_t> row_off(W+1,0), col_idx; std::vector<float> cnt, norm(W,0.f);
for(int w=0;w<W;w++){
std::vector<std::pair<uint32_t,float>> nb;
double ss=0;
for(int u=0;u<W;u++) if(cooc[w][u]>0){ nb.push_back({(uint32_t)u,cooc[w][u]}); ss+=cooc[w][u]*cooc[w][u]; }
std::sort(nb.begin(),nb.end());
row_off[w+1]=row_off[w]+nb.size();
for(auto&p:nb){ col_idx.push_back(p.first); cnt.push_back(p.second); }
norm[w]=(float)std::sqrt(ss);
}
// saturation evidence: how full are the neighbour SETS? (if ~1.0, sets carry no signal)
double avgdeg=0; for(int w=0;w<W;w++) avgdeg += (double)(row_off[w+1]-row_off[w]);
avgdeg/=W;
printf("Neighbour-set saturation: avg |set| = %.1f of %d words (%.0f%% full)\n\n",
avgdeg, W, 100*avgdeg/(W-1));
// ---- GPU: agent (jaccard) + cosine similarity, one block per agent --
uint32_t *d_row,*d_col; float *d_cnt,*d_norm,*d_jac,*d_cos;
CUDA_OK(cudaMalloc(&d_row,row_off.size()*4));
CUDA_OK(cudaMalloc(&d_col,col_idx.size()*4));
CUDA_OK(cudaMalloc(&d_cnt,cnt.size()*4));
CUDA_OK(cudaMalloc(&d_norm,W*4));
CUDA_OK(cudaMalloc(&d_jac,(size_t)W*W*4));
CUDA_OK(cudaMalloc(&d_cos,(size_t)W*W*4));
CUDA_OK(cudaMemcpy(d_row,row_off.data(),row_off.size()*4,cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(d_col,col_idx.data(),col_idx.size()*4,cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(d_cnt,cnt.data(),cnt.size()*4,cudaMemcpyHostToDevice));
CUDA_OK(cudaMemcpy(d_norm,norm.data(),W*4,cudaMemcpyHostToDevice));
agent_similarity<<<W,128>>>(d_row,d_col,d_cnt,d_norm,W,d_jac,d_cos);
CUDA_OK(cudaDeviceSynchronize());
std::vector<float> jac((size_t)W*W), cos((size_t)W*W);
CUDA_OK(cudaMemcpy(jac.data(),d_jac,(size_t)W*W*4,cudaMemcpyDeviceToHost));
CUDA_OK(cudaMemcpy(cos.data(),d_cos,(size_t)W*W*4,cudaMemcpyDeviceToHost));
// ---- Verify: do agents cluster into the planted classes? -----------
const int kk=5; int fails=0;
// (1) agent top-k purity: fraction of top-k peers in the same class
double jac_pure=0, cos_pure=0; int counted=0;
for(int a=0;a<W;a++){
auto tj=topk(jac,W,a,kk), tc=topk(cos,W,a,kk);
int jp=0,cp=0; for(int b:tj) if(class_of(b)==class_of(a)) jp++;
for(int b:tc) if(class_of(b)==class_of(a)) cp++;
jac_pure+=(double)jp/kk; cos_pure+=(double)cp/kk; counted++;
}
jac_pure/=counted; cos_pure/=counted;
printf("Top-%d same-class purity: AGENT(overlap) %.1f%% COSINE %.1f%%\n",
kk, 100*jac_pure, 100*cos_pure);
// (2) agreement between the two methods' top-k peer sets
double agree=0;
for(int a=0;a<W;a++){
auto tj=topk(jac,W,a,kk), tc=topk(cos,W,a,kk);
std::sort(tj.begin(),tj.end()); std::sort(tc.begin(),tc.end());
std::vector<int> inter; std::set_intersection(tj.begin(),tj.end(),tc.begin(),tc.end(),std::back_inserter(inter));
agree += (double)inter.size()/kk;
}
agree/=W;
printf("Agent vs cosine top-%d peer agreement: %.1f%%\n\n", kk, 100*agree);
// (3) spot-check: top peers of one content word, by class
int probe = content_id(1,0); // a known class-1 content word
printf("Probe word #%d (class %d) top-%d agent-peers (class in parens):\n ", probe, class_of(probe), kk);
for(int b:topk(jac,W,probe,kk)) printf("#%d(c%d) ", b, class_of(b));
printf("\n");
// FINDING (the real result): an agent that mates on bare STRUCTURE (neighbour
// SET overlap) fails ~= random, because the sets saturate; an agent that reasons
// over its world-model VALUES (count-weighted overlap = cosine) recovers the
// planted classes. Both are computed block-per-agent on GPU; the ONLY difference
// is set vs weighted. => Word-agents work, but the agent's world-model must carry
// VALUES, not just structure. Empirical proof of Linas's index(structure) vs
// payload(Values) split, and of the diary's Jaccard-vs-cosine result.
bool weighted_ok = cos_pure >= 0.75;
bool set_random = jac_pure < 0.40; // ~random for this class layout
printf("\nFINDING:\n");
printf(" * bare-SET agent (overlap): %.1f%% purity -> %s\n", 100*jac_pure,
set_random? "FAILS (~random; sets saturated, no signal)" : "carries some signal");
printf(" * VALUE-weighted agent (counts):%.1f%% purity -> %s\n", 100*cos_pure,
weighted_ok? "RECOVERS planted classes" : "weak");
printf(" => An agent's world-model must carry VALUES, not just structure.\n");
printf(" Set-intersection is the QUERY primitive (Proto A); SIMILARITY needs the Values.\n");
if(!weighted_ok){ fails++; printf("FAIL value-weighted agent did not recover classes\n"); }
printf("\n%s\n", fails? "SOME CHECKS FAILED" :
"ALL CHECKS PASSED - finding verified: word-agents need values, not just sets.");
cudaFree(d_row);cudaFree(d_col);cudaFree(d_cnt);cudaFree(d_norm);cudaFree(d_jac);cudaFree(d_cos);
return fails?1:0;
}