File size: 34,973 Bytes
5863ad5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 | /* SPDX-License-Identifier: AGPL-3.0-or-later
* decode.c β RPI inference engine (the heart of it)
*
* This is the llama.cpp equivalent for Resonant Permutation Inference.
* No multiply-accumulate. Only permute, accumulate, measure, route.
*
* (c) 2026 Elyan Labs β Claude Opus + GPT-5.4 collaboration
*/
#include "rpi_format.h"
#include "rpi_runtime.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#if defined(__aarch64__)
#include <arm_neon.h>
#endif
/* Lane energy (L1 norm of the 64 int16 lanes). Used for routing decisions and
* emission strength. NEON version is BIT-EXACT vs the scalar loop: vabal_s16
* against zero computes |v| widened to s32 before accumulating, so even
* INT16_MIN contributes exactly 32768. */
static inline int32_t rpi_lane_energy(const int16_t *lanes) {
#if defined(__aarch64__)
int32x4_t acc = vdupq_n_s32(0);
int16x4_t zero = vdup_n_s16(0);
for (int j = 0; j < RPI_LANES; j += 8) {
int16x8_t v = vld1q_s16(lanes + j);
acc = vabal_s16(acc, vget_low_s16(v), zero);
acc = vabal_s16(acc, vget_high_s16(v), zero);
}
return vaddvq_s32(acc);
#else
int32_t energy = 0;
for (int j = 0; j < RPI_LANES; j++) {
int16_t v = lanes[j];
energy += (v >= 0) ? v : -v;
}
return energy;
#endif
}
/* ββ Coarse phase profiler (RPI_PROFILE=1) ββββββββββββββββββ
* Accumulates timebase ticks in three buckets so we can see where per-token
* time actually goes (permute vs routing/resonance vs vocab scoring) before
* optimizing the wrong one. Percentages only; no tb_freq needed. */
static uint64_t g_prof_perm = 0, g_prof_route = 0, g_prof_reson = 0, g_prof_emit = 0;
static uint64_t g_prof_wall0 = 0; /* generation start, for coverage */
static int g_prof_on = -1;
static int prof_enabled(void) {
if (g_prof_on < 0) {
const char *e = getenv("RPI_PROFILE");
g_prof_on = (e && e[0] && e[0] != '0') ? 1 : 0;
}
return g_prof_on;
}
/* ββ State Management βββββββββββββββββββββββββββββββββββββ */
void rpi_state_init(RPIState *st) {
memset(st, 0, sizeof(*st));
st->tb_prev = rpi_tb_now();
}
void rpi_state_reset(RPIState *st) {
st->n_active = 0;
st->round = 0;
st->sig_prev = 0;
memset(st->tok_scores, 0, sizeof(st->tok_scores));
st->n_touched = 0; /* tok_scores is fully zeroed, so the touched list restarts */
memset(st->rep_history, 0, sizeof(st->rep_history));
st->rep_pos = 0; /* reset means reset: no repetition carry-over */
}
/* ββ Hardware Detection βββββββββββββββββββββββββββββββββββ */
void rpi_hw_detect(RPIHWProfile *hw) {
memset(hw, 0, sizeof(*hw));
hw->n_threads = 1;
hw->cache_line_bytes = 64;
hw->l1_size_kb = 32;
hw->l2_size_kb = 256;
hw->l3_size_kb = 8192;
#if defined(__powerpc64__)
hw->has_vsx = 1;
hw->cache_line_bytes = 128; /* POWER8 uses 128-byte cache lines */
hw->l1_size_kb = 64;
hw->l2_size_kb = 512;
hw->l3_size_kb = 8192;
hw->tb_freq = 512000000; /* 512 MHz typical POWER8 timebase */
hw->lat_l1_ns = 1.2f;
hw->lat_l2_ns = 4.5f;
hw->lat_l3_ns = 12.0f;
hw->lat_dram_ns = 55.0f;
fprintf(stderr, "[RPI] POWER8 detected: VSX + 128-byte cache lines\n");
#elif defined(__ALTIVEC__)
hw->has_altivec = 1;
hw->cache_line_bytes = 32; /* G4 uses 32-byte cache lines */
hw->l1_size_kb = 32;
hw->l2_size_kb = 256;
hw->l3_size_kb = 0; /* G4 typically no L3 */
hw->tb_freq = 25000000; /* 25 MHz typical G4 timebase */
hw->lat_l1_ns = 2.0f;
hw->lat_l2_ns = 8.0f;
hw->lat_l3_ns = 0.0f;
hw->lat_dram_ns = 80.0f;
fprintf(stderr, "[RPI] PowerPC G4 detected: AltiVec + 32-byte cache lines\n");
#elif defined(__aarch64__)
/* ARM64 (Apple M-series, Pi 5, Graviton, Cortex...). NEON tbl is baseline.
* Read the actual counter frequency instead of assuming one β it varies by
* platform (Apple 24 MHz, Pi 5 54 MHz, Graviton differs). Wrong tb_freq
* poisons both the decode deadline budget and the tok/s report. */
{
uint64_t cntfrq = 0;
__asm__ volatile("mrs %0, cntfrq_el0" : "=r"(cntfrq));
hw->tb_freq = cntfrq ? cntfrq : 24000000;
}
hw->l3_size_kb = 0; /* no conventional L3 on these parts */
#ifdef __APPLE__
hw->cache_line_bytes = 128; /* Apple M-series use 128-byte lines */
hw->l1_size_kb = 128; /* M-series P-core L1 data cache */
hw->l2_size_kb = 16384; /* shared L2 (P-cluster) */
hw->lat_l1_ns = 0.9f; hw->lat_l2_ns = 3.5f; hw->lat_dram_ns = 45.0f;
fprintf(stderr, "[RPI] Apple Silicon detected: NEON tbl, tb_freq=%llu Hz\n",
(unsigned long long)hw->tb_freq);
#else
hw->cache_line_bytes = 64; /* generic AArch64 default */
hw->l1_size_kb = 64;
hw->l2_size_kb = 512;
hw->lat_l1_ns = 1.2f; hw->lat_l2_ns = 4.0f; hw->lat_dram_ns = 60.0f;
fprintf(stderr, "[RPI] AArch64 detected: NEON tbl, tb_freq=%llu Hz\n",
(unsigned long long)hw->tb_freq);
#endif
hw->lat_l3_ns = 0.0f;
#else
hw->tb_freq = 1000000000; /* assume ~1GHz for rdtsc */
hw->lat_l1_ns = 1.0f;
hw->lat_l2_ns = 4.0f;
hw->lat_l3_ns = 10.0f;
hw->lat_dram_ns = 50.0f;
fprintf(stderr, "[RPI] Generic CPU detected\n");
#endif
}
/* ββ Permutation Block Execution (generic C) ββββββββββββββ */
void rpi_run_perm_block_c(const RPIPermBlock *block,
const int16_t *in, int16_t *out) {
/*
* For each output lane:
* 1. Read from source lane (permutation)
* 2. Apply sign (negate if sign bit set)
* 3. Accumulate into output
*
* This is the ZERO-MULTIPLY core. Only permute + add/subtract.
*/
for (int i = 0; i < RPI_LANES; i++) {
uint8_t src = block->src_idx[i];
if (src == 0xFF) {
/* Zero input β skip (W=0 in ternary) */
continue;
}
int16_t val = in[src & 0x3F]; /* mask to valid range */
/* Apply sign: bit i of sign_bits β negate */
if ((block->sign_bits >> i) & 1) {
out[i] -= val; /* W=-1: subtract */
} else {
out[i] += val; /* W=+1: add */
}
}
}
/* ββ Run permutation blocks for one cell ββββββββββββββββββ */
static void run_cell_perms(const RPIModel *model, const RPICell *cell,
int16_t *lanes) {
/* Select backend */
void (*run_block)(const RPIPermBlock *, const int16_t *, int16_t *);
/* Runtime override for A/B benchmarking: RPI_SCALAR=1 forces the generic
* C backend on any platform. Resolved once. */
static int force_scalar = -1;
if (force_scalar < 0) {
const char *e = getenv("RPI_SCALAR");
force_scalar = (e && e[0] && e[0] != '0') ? 1 : 0;
}
if (force_scalar) {
run_block = rpi_run_perm_block_c;
} else {
#if defined(__POWER8_VECTOR__)
run_block = rpi_run_perm_block_vsx;
#elif defined(__ALTIVEC__)
run_block = rpi_run_perm_block_altivec;
#elif defined(__aarch64__)
/* Build the TBL control table once so the hot path stays pure-vector. */
rpi_neon_prepare(model->perm_blocks, model->hdr.n_perm_blocks);
run_block = rpi_run_perm_block_neon;
#else
run_block = rpi_run_perm_block_c;
#endif
}
#if defined(__aarch64__)
/* NEON fast path: the write-variant kernel (out = Β±in[src], no accumulate)
* is arithmetically identical to memset+accumulate and is in-place safe
* (all input loaded to registers before any store). So run each block
* directly lanes -> lanes: no tmp, no 128B memset, no 128B memcpy per
* block. RPI_OLD_PERM=1 keeps the old ping-pong for same-binary A/B. */
static int old_perm = -1;
if (old_perm < 0) {
const char *e = getenv("RPI_OLD_PERM");
old_perm = (e && e[0] && e[0] != '0') ? 1 : 0;
}
if (!force_scalar && !old_perm) {
for (uint32_t b = 0; b < cell->perm_block_count; b++) {
const RPIPermBlock *block = &model->perm_blocks[cell->perm_block_start + b];
rpi_run_perm_block_neon_write(block, lanes, lanes);
}
return;
}
#endif
/* Temporary buffer for ping-pong */
int16_t tmp[RPI_LANES];
for (uint32_t b = 0; b < cell->perm_block_count; b++) {
const RPIPermBlock *block = &model->perm_blocks[cell->perm_block_start + b];
memset(tmp, 0, sizeof(tmp));
run_block(block, lanes, tmp);
/* Ping-pong: tmp becomes input for next block */
memcpy(lanes, tmp, sizeof(tmp));
}
}
/* ββ Activate seed cells for a token ββββββββββββββββββββββ */
void rpi_activate_token(const RPIModel *model, RPIState *st,
uint32_t token_id) {
if (!model->embed_seeds || token_id >= model->hdr.vocab_size)
return;
uint32_t seed_cell = model->embed_seeds[token_id];
if (seed_cell >= model->hdr.n_cells)
return;
/* Add to active set if not already present and not full */
for (uint32_t i = 0; i < st->n_active; i++) {
if (st->active_ids[i] == seed_cell)
return; /* already active */
}
if (st->n_active < RPI_MAX_ACTIVE_CELLS) {
uint32_t idx = st->n_active++;
st->active_ids[idx] = seed_cell;
memset(st->active_lanes[idx], 0, sizeof(st->active_lanes[idx]));
/* Initialize lanes with a simple hash of token_id + cell_id */
uint64_t h = rpi_fnv1a(&token_id, sizeof(token_id));
for (int i = 0; i < RPI_LANES; i++) {
st->active_lanes[idx][i] = (int16_t)((h >> (i % 48)) & 0xFF) - 128;
}
}
}
/* ββ Cache Resonance Probing ββββββββββββββββββββββββββββββ */
void rpi_probe_resonance(const RPIModel *model, const RPIHWProfile *hw,
RPIState *st) {
/*
* For each active cell, measure access latency to its weight data.
* Faster access = stronger resonance = higher routing priority.
*
* This is the CACHE-RESONANCE ATTENTION mechanism.
* GPUs have uniform shared memory β they can't do this.
* CPU cache hierarchy IS the attention score function.
*/
for (uint32_t i = 0; i < st->n_active; i++) {
uint32_t cell_id = st->active_ids[i];
const RPICell *cell = &model->cells[cell_id];
/* Touch the first perm block of this cell and measure time */
if (cell->perm_block_count == 0) {
st->lat_score[i] = 0.0f;
continue;
}
const RPIPermBlock *block = &model->perm_blocks[cell->perm_block_start];
/* Timed probe: read the block data and measure latency */
uint64_t t0 = rpi_tb_now();
/* Force a real memory access (prevent optimization) */
volatile uint8_t sink = 0;
for (int j = 0; j < 8; j++) {
sink ^= block->src_idx[j * 8];
}
(void)sink;
uint64_t t1 = rpi_tb_now();
uint64_t dt = t1 - t0;
/* Convert to a score: faster access = higher score */
/* Normalize by hardware latency profile */
float ns_est = (float)dt * 1e9f / (float)hw->tb_freq;
if (ns_est <= hw->lat_l1_ns * 2.0f) {
st->lat_score[i] = 1.0f; /* L1: strong resonance */
} else if (ns_est <= hw->lat_l2_ns * 2.0f) {
st->lat_score[i] = 0.6f; /* L2: moderate */
} else if (ns_est <= hw->lat_l3_ns * 2.0f) {
st->lat_score[i] = 0.3f; /* L3: weak */
} else {
st->lat_score[i] = 0.05f; /* DRAM: minimal */
}
}
}
/* ββ One Recurrence Round βββββββββββββββββββββββββββββββββ */
void rpi_round(const RPIModel *model, const RPIHWProfile *hw,
RPIState *st) {
/*
* For each active cell:
* 1. Run permutation blocks (zero-multiply forward pass)
* 2. Follow routes to activate downstream cells
* 3. Measure cache resonance
*/
uint64_t _pt0 = prof_enabled() ? rpi_tb_now() : 0;
/* Phase 1: Run permutations on all active cells */
for (uint32_t i = 0; i < st->n_active; i++) {
uint32_t cell_id = st->active_ids[i];
const RPICell *cell = &model->cells[cell_id];
/* Check phase mask */
if (!((cell->phase_mask >> st->phase) & 1))
continue;
run_cell_perms(model, cell, st->active_lanes[i]);
}
if (prof_enabled()) { uint64_t t = rpi_tb_now(); g_prof_perm += t - _pt0; _pt0 = t; }
/* Phase 2: Follow routes β activate downstream cells.
*
* RPI_OLD_ROUTE=1 selects the original buffered version (double-copies the
* whole lane state into scratch buffers each round). The default routes
* IN PLACE: existing cells are untouched, new cells are appended straight
* into st->active_ids / st->active_lanes past the original count. Identical
* behavior (same dedup over the growing set, same decayed lanes from the
* source cell), minus four memcpys and a 16 KB stack buffer per round. */
static int old_route = -1;
if (old_route < 0) {
const char *e = getenv("RPI_OLD_ROUTE");
old_route = (e && e[0] && e[0] != '0') ? 1 : 0;
}
if (old_route) {
uint32_t new_active[RPI_MAX_ACTIVE_CELLS];
int16_t new_lanes[RPI_MAX_ACTIVE_CELLS][RPI_LANES];
uint32_t n_new;
memcpy(new_active, st->active_ids, st->n_active * sizeof(uint32_t));
memcpy(new_lanes, st->active_lanes, st->n_active * sizeof(new_lanes[0]));
n_new = st->n_active;
for (uint32_t i = 0; i < st->n_active; i++) {
int32_t energy = rpi_lane_energy(st->active_lanes[i]);
if (energy < 64 * 10) continue;
const RPICell *cell = &model->cells[st->active_ids[i]];
for (uint32_t r = 0; r < cell->route_count && r < RPI_MAX_ROUTES; r++) {
uint32_t dst = model->routes[cell->route_start + r].dst_cell_id;
int found = 0;
for (uint32_t k = 0; k < n_new; k++)
if (new_active[k] == dst) { found = 1; break; }
if (found || n_new >= model->hdr.max_active) continue;
new_active[n_new] = dst;
for (int j = 0; j < RPI_LANES; j++)
new_lanes[n_new][j] = st->active_lanes[i][j] >> 2;
n_new++;
}
}
st->n_active = n_new;
memcpy(st->active_ids, new_active, n_new * sizeof(uint32_t));
memcpy(st->active_lanes, new_lanes, n_new * sizeof(new_lanes[0]));
} else {
/* In-place, with an O(1) hash-set membership check instead of the old
* O(n_active) linear "already active?" scan. That scan made routing
* O(n_active^2 * routes) per round and dominated per-token time; the
* hash set makes it O(n_active * routes). Open addressing, linear
* probe; 0xFFFFFFFF marks empty. Sized well above max_active. */
#define RPI_SEEN_SZ 512
#define RPI_SEEN_MASK (RPI_SEEN_SZ - 1)
uint32_t seen[RPI_SEEN_SZ];
memset(seen, 0xFF, sizeof(seen));
for (uint32_t k = 0; k < st->n_active; k++) {
uint32_t id = st->active_ids[k];
uint32_t h = (id * 2654435761u) & RPI_SEEN_MASK;
while (seen[h] != 0xFFFFFFFFu && seen[h] != id) h = (h + 1) & RPI_SEEN_MASK;
seen[h] = id;
}
uint32_t orig_active = st->n_active;
for (uint32_t i = 0; i < orig_active; i++) {
int32_t energy = rpi_lane_energy(st->active_lanes[i]);
if (energy < 64 * 10) continue;
const RPICell *cell = &model->cells[st->active_ids[i]];
for (uint32_t r = 0; r < cell->route_count && r < RPI_MAX_ROUTES; r++) {
uint32_t dst = model->routes[cell->route_start + r].dst_cell_id;
uint32_t h = (dst * 2654435761u) & RPI_SEEN_MASK;
while (seen[h] != 0xFFFFFFFFu && seen[h] != dst) h = (h + 1) & RPI_SEEN_MASK;
if (seen[h] == dst || st->n_active >= model->hdr.max_active) continue;
uint32_t slot = st->n_active;
st->active_ids[slot] = dst;
for (int j = 0; j < RPI_LANES; j++)
st->active_lanes[slot][j] = st->active_lanes[i][j] >> 2;
st->n_active++;
seen[h] = dst; /* probe stopped at the empty slot for dst */
}
}
}
/* Opt-in correctness check (RPI_ROUTE_CHECK=1): the hash-set dedup is
* correct iff the active set never contains a duplicate. Scan for dups. */
{
static int route_check = -1;
if (route_check < 0) {
const char *e = getenv("RPI_ROUTE_CHECK");
route_check = (e && e[0] && e[0] != '0') ? 1 : 0;
}
if (route_check) {
for (uint32_t a = 0; a < st->n_active; a++)
for (uint32_t b = a + 1; b < st->n_active; b++)
if (st->active_ids[a] == st->active_ids[b]) {
static int rep = 0;
if (rep++ < 5)
fprintf(stderr, "[RPI] ROUTE_CHECK: duplicate active cell %u\n",
st->active_ids[a]);
}
}
}
if (prof_enabled()) { uint64_t t = rpi_tb_now(); g_prof_route += t - _pt0; _pt0 = t; }
/* Phase 3: Probe cache resonance */
rpi_probe_resonance(model, hw, st);
if (prof_enabled()) g_prof_reson += rpi_tb_now() - _pt0;
st->round++;
}
/* ββ Token Emission (v2: multi-cell voting + diversity) βββ */
/* Repetition-penalty history lives in RPIState (per-stream, thread-safe). */
uint32_t rpi_emit_next(const RPIModel *model, const RPIHWProfile *hw,
RPIState *st) {
(void)hw;
/* RPI_DENSE_EMIT=1 forces the old full-vocab path for same-binary A/B. */
static int dense_emit = -1;
if (dense_emit < 0) {
const char *e = getenv("RPI_DENSE_EMIT");
dense_emit = (e && e[0] && e[0] != '0') ? 1 : 0;
}
/* Clear scores. Sparse (default): wipe only slots dirtied last step, an
* O(touched) lazy clear that keeps the invariant "untouched reads 0" and
* replaces the O(vocab) memset (was ~a third of per-token time at 32k
* vocab). Dense: the old full-vocab memset. */
if (dense_emit) {
memset(st->tok_scores, 0, model->hdr.vocab_size * sizeof(int32_t));
} else {
for (uint32_t i = 0; i < st->n_touched; i++)
st->tok_scores[st->touched[i]] = 0;
}
st->n_touched = 0;
/* Collect emissions from ALL active cells (multi-cell voting) */
for (uint32_t i = 0; i < st->n_active; i++) {
uint32_t cell_id = st->active_ids[i];
if (cell_id >= model->hdr.n_cells) continue;
const RPICell *cell = &model->cells[cell_id];
/* Lane energy as cell activation strength */
int32_t energy = rpi_lane_energy(st->active_lanes[i]);
/* Bank diversity bonus: cells from underrepresented banks score higher */
int32_t bank_bonus = (cell->bank_id != (st->phase & 3)) ? 50 : 0;
/* Resonance bonus from cache timing */
int32_t lat_bonus = (int32_t)(st->lat_score[i] * 80.0f);
for (uint32_t e = 0; e < cell->emit_count && e < RPI_MAX_EMITS; e++) {
const RPIEmit *emit = &model->emits[cell->emit_start + e];
/* Phase check */
if (emit->phase_mask != 0x0F &&
!((emit->phase_mask >> st->phase) & 1))
continue;
int32_t score = (int32_t)emit->rank_bias
+ (energy >> 8) /* cell activation (reduced weight) */
+ lat_bonus
+ bank_bonus;
if (emit->token_id < model->hdr.vocab_size) {
st->tok_scores[emit->token_id] += score;
/* Record the dirtied slot so we can scan/clear it sparsely.
* Duplicates are fine: the score already accumulated in the
* slot, and the top-K scan below dedups. */
if (st->n_touched < RPI_MAX_TOUCHED)
st->touched[st->n_touched++] = emit->token_id;
}
}
}
/* Repetition penalty: halve scores of recently emitted tokens */
for (uint32_t r = 0; r < RPI_REP_WINDOW; r++) {
uint32_t prev_tok = st->rep_history[r];
if (prev_tok < model->hdr.vocab_size && st->tok_scores[prev_tok] > 0) {
st->tok_scores[prev_tok] >>= 2; /* quarter the score */
}
}
/* Top-K sampling with hardware entropy (K=8).
* Scan ONLY the touched candidates, not the whole vocab. The set of tokens
* with score>0 is exactly the touched set (untouched read 0 by invariant),
* so this yields the same top-K set as the old full-vocab scan. */
uint32_t top_ids[8] = {0};
int32_t top_scores[8] = {0};
if (dense_emit) {
/* Old full-vocab scan (A/B baseline). */
for (uint32_t t = 0; t < model->hdr.vocab_size; t++) {
if (st->tok_scores[t] <= 0) continue;
int min_idx = 0;
for (int k = 1; k < 8; k++)
if (top_scores[k] < top_scores[min_idx]) min_idx = k;
if (st->tok_scores[t] > top_scores[min_idx]) {
top_scores[min_idx] = st->tok_scores[t];
top_ids[min_idx] = t;
}
}
} else {
for (uint32_t idx = 0; idx < st->n_touched; idx++) {
uint32_t t = st->touched[idx];
int32_t sc = st->tok_scores[t];
if (sc <= 0) continue;
/* Skip if already placed (touched[] may contain duplicates). */
int dup = 0;
for (int k = 0; k < 8; k++)
if (top_scores[k] > 0 && top_ids[k] == t) { dup = 1; break; }
if (dup) continue;
int min_idx = 0;
for (int k = 1; k < 8; k++)
if (top_scores[k] < top_scores[min_idx]) min_idx = k;
if (sc > top_scores[min_idx]) {
top_scores[min_idx] = sc;
top_ids[min_idx] = t;
}
}
}
/* Opt-in differential self-check: verify the sparse top-K SET matches the
* old dense full-vocab scan. Resolved once; off by default. */
static int emit_check = -1;
if (emit_check < 0) {
const char *e = getenv("RPI_EMIT_CHECK");
emit_check = (e && e[0] && e[0] != '0') ? 1 : 0;
}
if (emit_check) {
uint32_t d_ids[8] = {0}; int32_t d_scores[8] = {0};
for (uint32_t t = 0; t < model->hdr.vocab_size; t++) {
if (st->tok_scores[t] <= 0) continue;
int mi = 0;
for (int k = 1; k < 8; k++) if (d_scores[k] < d_scores[mi]) mi = k;
if (st->tok_scores[t] > d_scores[mi]) { d_scores[mi] = st->tok_scores[t]; d_ids[mi] = t; }
}
/* Compare as multisets of (score) present with score>0: same top-K
* scores => same candidate strength distribution. */
int32_t ss[8], ds[8]; int ns = 0, nd = 0;
for (int k = 0; k < 8; k++) { if (top_scores[k] > 0) ss[ns++] = top_scores[k];
if (d_scores[k] > 0) ds[nd++] = d_scores[k]; }
for (int a = 0; a < ns; a++) for (int b = a + 1; b < ns; b++) if (ss[b] > ss[a]) { int32_t t = ss[a]; ss[a] = ss[b]; ss[b] = t; }
for (int a = 0; a < nd; a++) for (int b = a + 1; b < nd; b++) if (ds[b] > ds[a]) { int32_t t = ds[a]; ds[a] = ds[b]; ds[b] = t; }
int mismatch = (ns != nd);
for (int k = 0; !mismatch && k < ns; k++) if (ss[k] != ds[k]) mismatch = 1;
if (mismatch) {
static int reported = 0;
if (reported++ < 5)
fprintf(stderr, "[RPI] EMIT_CHECK MISMATCH: sparse topK=%d dense topK=%d\n", ns, nd);
}
}
/* RPI_GREEDY=1: deterministic argmax over the top-K instead of entropy
* sampling. This is the drafting mode for speculative decoding β a draft
* should be the model's best guess, and deterministic drafts make
* verifier acceptance measurable. */
static int greedy = -1;
if (greedy < 0) {
const char *e = getenv("RPI_GREEDY");
greedy = (e && e[0] && e[0] != '0') ? 1 : 0;
}
if (greedy) {
int best = 0;
for (int k = 1; k < 8; k++)
if (top_scores[k] > top_scores[best]) best = k;
uint32_t chosen_g = top_ids[best];
st->rep_history[st->rep_pos % RPI_REP_WINDOW] = chosen_g;
st->rep_pos++;
return chosen_g;
}
/* Sample from top-K using hardware entropy */
uint64_t tb = rpi_tb_now();
int32_t total = 0;
for (int k = 0; k < 8; k++) total += top_scores[k];
uint32_t chosen = top_ids[0];
if (total > 0) {
int32_t threshold = (int32_t)((tb ^ (tb >> 17)) % (uint64_t)total);
int32_t cumulative = 0;
for (int k = 0; k < 8; k++) {
cumulative += top_scores[k];
if (cumulative >= threshold) {
chosen = top_ids[k];
break;
}
}
}
/* Update repetition history */
st->rep_history[st->rep_pos % RPI_REP_WINDOW] = chosen;
st->rep_pos++;
return chosen;
}
/* ββ Full Token Decode (v2: min rounds + cross-bank activation) ββ */
uint32_t rpi_decode_token(const RPIModel *model, const RPIHWProfile *hw,
RPIState *st) {
st->round = 0;
st->sig_prev = 0;
st->tb_round_start = rpi_tb_now();
uint64_t budget = hw->tb_freq / 10; /* 100ms max */
uint64_t deadline = st->tb_round_start + budget;
uint32_t stable = 0;
uint32_t min_rounds = 3; /* MINIMUM 3 rounds before convergence check */
while (st->round < model->hdr.max_rounds &&
rpi_tb_now() < deadline) {
rpi_round(model, hw, st);
/* Force cross-bank activation every round:
* Pick a random cell from a different bank and activate it */
if (st->n_active < model->hdr.max_active && model->hdr.n_cells > 0) {
uint64_t tb_seed = rpi_tb_now();
uint32_t target_bank = (st->phase + st->round + 1) & 3;
uint32_t cells_per_bank = model->hdr.n_cells / model->hdr.n_banks;
uint32_t cross_cell = target_bank * cells_per_bank
+ (uint32_t)(tb_seed % cells_per_bank);
if (cross_cell < model->hdr.n_cells) {
/* Check not already active */
int found = 0;
for (uint32_t k = 0; k < st->n_active; k++) {
if (st->active_ids[k] == cross_cell) { found = 1; break; }
}
if (!found && st->n_active < RPI_MAX_ACTIVE_CELLS) {
uint32_t idx = st->n_active++;
st->active_ids[idx] = cross_cell;
/* Initialize with entropy-seeded lanes */
for (int j = 0; j < RPI_LANES; j++) {
st->active_lanes[idx][j] =
(int16_t)((tb_seed >> (j & 0x3F)) & 0x7F) - 64;
}
}
}
}
/* Only check convergence after minimum rounds */
if (st->round >= min_rounds) {
uint64_t sig = rpi_fnv1a(st->active_ids,
st->n_active * sizeof(uint32_t));
for (uint32_t i = 0; i < st->n_active; i++) {
int32_t sum = 0;
for (int j = 0; j < RPI_LANES; j++)
sum += st->active_lanes[i][j];
sig ^= rpi_fnv1a(&sum, sizeof(sum));
}
if (sig == st->sig_prev) {
stable++;
if (stable >= 2) break;
} else {
stable = 0;
}
st->sig_prev = sig;
}
}
/* Emit next token with diversity */
uint64_t _et0 = prof_enabled() ? rpi_tb_now() : 0;
uint32_t next = rpi_emit_next(model, hw, st);
if (prof_enabled()) g_prof_emit += rpi_tb_now() - _et0;
/* Activate seed cells for next token */
rpi_activate_token(model, st, next);
/* Advance phase (rotates through banks) */
st->phase = (st->phase + 1) & 3;
st->last_token = next;
st->total_tokens++;
st->tb_prev = rpi_tb_now();
return next;
}
/* ββ Generate Text ββββββββββββββββββββββββββββββββββββββββ */
/* ββ Context-copy drafting (RPI_NGRAM=1) βββββββββββββββββββββ
* RPI as a matmul-free PROCEDURAL accelerator: instead of asking the cell
* engine to invent the next token, find the longest suffix of the recent
* tokens that already occurred earlier in [prompt + generated] and COPY its
* continuation. This is prompt-lookup / n-gram speculative drafting β pure
* table permutation, zero multiplies, no model quality required β and it is
* the token class (copied spans, boilerplate, structure) where a draft can
* actually agree with an LLM verifier. Semantics stay with the LLM; the
* reordering-shaped tokens run here.
*
* hist(i) spans prompt then output. Longest match wins, ties go to the MOST
* RECENT occurrence. Returns the continuation token, or UINT32_MAX if no
* n-gram of length >= NGRAM_MIN matches. */
#define NGRAM_MAX 8
#define NGRAM_MIN 1 /* n=1 (most-recent unigram) rescues structural copies
like JSON keys after a novel value token; llama.cpp's
prompt-lookup allows it for the same reason */
static uint32_t ngram_copy_next(const uint32_t *prompt_ids, uint32_t prompt_len,
const uint32_t *output_ids, uint32_t n_out) {
const uint32_t T = prompt_len + n_out;
if (T < NGRAM_MIN + 1) return UINT32_MAX;
#define HIST(i) ((i) < prompt_len ? prompt_ids[(i)] : output_ids[(i) - prompt_len])
uint32_t nmax = (T - 1 < NGRAM_MAX) ? T - 1 : NGRAM_MAX;
for (uint32_t n = nmax; n >= NGRAM_MIN; n--) {
/* suffix = HIST(T-n .. T-1); scan for its most recent earlier copy */
for (uint32_t p = T - n; p-- > 0; ) { /* p = candidate match start */
uint32_t ok = 1;
for (uint32_t j = 0; j < n; j++) {
if (HIST(p + j) != HIST(T - n + j)) { ok = 0; break; }
}
if (ok && p + n < T)
return HIST(p + n); /* the copied continuation */
}
}
#undef HIST
return UINT32_MAX;
}
void rpi_generate(const RPIModel *model, const RPIHWProfile *hw,
RPIState *st, const uint32_t *prompt_ids,
uint32_t prompt_len, uint32_t max_tokens,
uint32_t *output_ids, uint32_t *n_output) {
if (prof_enabled()) g_prof_wall0 = rpi_tb_now();
static int ngram_mode = -1;
if (ngram_mode < 0) {
const char *e = getenv("RPI_NGRAM");
ngram_mode = (e && e[0] && e[0] != '0') ? 1 : 0;
}
/* Ingest prompt */
for (uint32_t i = 0; i < prompt_len; i++) {
rpi_activate_token(model, st, prompt_ids[i]);
/* Quick round for prompt tokens (fewer rounds needed) */
for (uint32_t r = 0; r < 2; r++) {
rpi_round(model, hw, st);
}
st->phase = (st->phase + 1) & 3;
}
/* Generate */
*n_output = 0;
for (uint32_t i = 0; i < max_tokens; i++) {
uint32_t tok;
if (ngram_mode) {
tok = ngram_copy_next(prompt_ids, prompt_len, output_ids, *n_output);
if (tok != UINT32_MAX) {
/* Copied continuation β DELIBERATELY lightweight. The contract
* of a copy is zero engine compute: no rounds, no emission
* scoring, no repetition history. Only seed-activation + phase
* advance, so a later fallback token still has context. A
* fallback after copies therefore runs on a state no pure-
* engine path produces; that hybrid is the design, not an
* accident. (A copied 0 propagates and stops generation below,
* same as an engine-emitted EOS.) */
rpi_activate_token(model, st, tok);
st->phase = (st->phase + 1) & 3;
st->last_token = tok;
st->total_tokens++;
} else {
/* No copyable span β this token is not procedural. In a
* dual-brain deployment this is the escalation point; standalone
* we fall back to the cell engine. */
tok = rpi_decode_token(model, hw, st);
}
} else {
tok = rpi_decode_token(model, hw, st);
}
output_ids[i] = tok;
(*n_output)++;
/* Stop on EOS (token 0 by convention) */
if (tok == 0)
break;
}
if (prof_enabled()) {
uint64_t tot = g_prof_perm + g_prof_route + g_prof_reson + g_prof_emit;
if (tot == 0) tot = 1;
fprintf(stderr, "[RPI] profile: permute=%.1f%% routing=%.1f%% "
"resonance=%.1f%% vocab-scoring=%.1f%%\n",
100.0 * g_prof_perm / tot,
100.0 * g_prof_route / tot,
100.0 * g_prof_reson / tot,
100.0 * g_prof_emit / tot);
/* Coverage: how much of REAL generation time the buckets account for.
* Percentages above are shares of the instrumented buckets only; work
* outside them (convergence sig hash, token activation, cross-bank
* injection, loop overhead) shows up here as "uncounted". */
uint64_t wall = rpi_tb_now() - g_prof_wall0;
if (wall == 0) wall = 1;
double ns_per_tick = (hw->tb_freq > 0) ? 1e9 / (double)hw->tb_freq : 0.0;
uint32_t ntok = (*n_output > 0) ? *n_output : 1;
fprintf(stderr, "[RPI] profile: buckets cover %.1f%% of wall "
"(uncounted %.1f%%); per token: perm=%.0fns route=%.0fns "
"reson=%.0fns emit=%.0fns uncounted=%.0fns wall=%.0fns\n",
100.0 * tot / wall, 100.0 * (double)(wall - tot) / wall,
g_prof_perm * ns_per_tick / ntok,
g_prof_route * ns_per_tick / ntok,
g_prof_reson * ns_per_tick / ntok,
g_prof_emit * ns_per_tick / ntok,
(double)(wall - tot) * ns_per_tick / ntok,
(double)wall * ns_per_tick / ntok);
}
}
|