ensemble / palimseste /creative.py
thefinalboss's picture
Initial release: ENSEMBLE training-free AI — compressed .exp experts + Kuramoto brain
1f71c7d verified
Raw
History Blame Contribute Delete
8.7 kB
"""PALIMPSESTE — Creative generation via mixture logits.
The core problem: _logits() collapses K retrieved candidates into ONE HV via
majority sign, then takes argmax. This is structurally incapable of producing
novel tokens — it can only echo the majority.
The solution: Mixture Logits. Instead of one bundle, we use ALL K retrieved
candidates as weighted votes over the vocabulary:
mixture_logits[t] = sum_k( sim_k * similarity(token_hv[t], value_hv_k) )
A token that's close to MULTIPLE candidates gets boosted beyond what any single
candidate would predict. This is how novel compositions emerge — the model
finds tokens at the intersection of multiple knowledge fragments.
Additionally: anti-repetition via n-gram blocking prevents attractor loops.
"""
from __future__ import annotations
import numpy as np
from dataclasses import dataclass
from typing import Optional
from .hv import HV, bind, similarity, bits_to_signs
__all__ = ["CreativeGenerator", "CreativeResult"]
@dataclass
class CreativeResult:
"""Result of creative generation."""
text: str
n_candidates_per_step: list[int]
novel_tokens: int
temperature: float
mixture_diversity: float
class CreativeGenerator:
"""Generate text using mixture logits from multiple retrieved candidates.
Instead of retrieving one bundled HV, this generator retrieves K matches
and builds a mixture distribution over the vocabulary. Tokens at the
intersection of multiple candidates get boosted — enabling novel
compositions that were never explicitly stored.
This is how PALIMPSESTE creates, not just repeats.
"""
def __init__(self, lm, top_k: int = 5, diversity: float = 0.3) -> None:
"""
Parameters
----------
lm : PalimpsesteForCausalLM
The language model.
top_k : int
Number of retrieved candidates to use per step.
diversity : float
How much to boost intersection tokens (0=none, 1=max).
"""
self.lm = lm
self.top_k = top_k
self.diversity = diversity
def mixture_logits(self, context_ids: list[int]) -> np.ndarray:
"""Compute mixture logits from K retrieved candidates.
For each of the top-K retrieved traces, compute similarity between
the trace's value HV and every token HV. Weight by retrieval sim.
Sum to get mixture logits.
This produces a RICHER distribution than single-HV retrieval because
tokens at the intersection of candidates get extra boost.
"""
assert self.lm.tokenizer is not None
s = self.lm._state_hv(context_ids)
q = bind(self.lm._self_hv, s)
# Retrieve K matches (NOT the bundled result)
ret = self.lm.phi.retrieve(self.lm.mem, q)
V = getattr(self.lm.tokenizer, 'vocab_size_actual', None) or self.lm.config.vocab_size
logits = np.zeros(V, dtype=np.float64)
if not ret.matches:
return logits
# Sort by similarity, take top-K
scored = sorted(zip(ret.matches, ret.sims), key=lambda x: x[1], reverse=True)
top_matches = scored[:self.top_k]
if not top_matches:
return logits
# Ensure token bits cache is built
packed_len = len(top_matches[0][0].value.bits)
if (self.lm._token_bits_cache is None or
self.lm._token_bits_cache.shape[0] != V or
self.lm._token_bits_cache.shape[1] != packed_len):
self.lm._token_bits_cache = np.empty((V, packed_len), dtype=np.uint8)
for tid in range(V):
self.lm._token_bits_cache[tid] = self.lm.tokenizer.token_hv(tid).bits
tok_bits = self.lm._token_bits_cache
_POPCOUNT = np.array([bin(i).count('1') for i in range(256)], dtype=np.uint16)
D = self.lm.config.D
# For each candidate, compute token logits and add weighted
for match, sim in top_matches:
value_bits = match.value.bits
xored = np.bitwise_xor(tok_bits, value_bits[np.newaxis, :])
hamming_dists = _POPCOUNT[xored].sum(axis=1)
candidate_logits = 1.0 - 2.0 * hamming_dists / D
# Weight by retrieval similarity (map [-1,1] → [0,1])
weight = (sim + 1.0) / 2.0
# Add weighted logits
logits += weight * candidate_logits
# Diversity boost: tokens that are above threshold in this candidate
# AND already high in logits get extra boost (intersection effect)
if self.diversity > 0:
high_mask = candidate_logits > 0.0
intersection = logits > 0.0
boost_mask = high_mask & intersection
logits[boost_mask] += self.diversity * 0.1
# Normalize
if logits.max() > logits.min():
logits = (logits - logits.min()) / (logits.max() - logits.min())
return logits
def generate(
self,
prompt: str,
max_new_tokens: int = 100,
temperature: float = 0.0,
ngram_block: int = 3,
seed: int | None = None,
) -> CreativeResult:
"""Generate text creatively using mixture logits.
Parameters
----------
prompt : str
Input text to continue.
max_new_tokens : int
Maximum tokens to generate.
temperature : float
0.0 = greedy on mixture, >0 = sample from mixture.
ngram_block : int
Block repetition of n-grams of this length (3 = no trigram repeats).
seed : int | None
RNG seed for reproducibility.
"""
from .lm import BOS, EOS, PAD
assert self.lm.tokenizer is not None
tok = self.lm.tokenizer
rng = np.random.default_rng(seed) if seed is not None else np.random.default_rng()
ids = tok.encode(prompt, add_bos=True, add_eos=True) + [BOS]
generated: list[int] = []
candidates_per_step: list[int] = []
novel = 0
for step in range(max_new_tokens):
logits = self.mixture_logits(ids)
if logits.max() == 0.0 and logits.min() == 0.0:
break # cold start
# N-gram blocking: penalize tokens that would complete a recent n-gram
if ngram_block > 0 and len(generated) >= ngram_block - 1:
recent_ngram = tuple(generated[-(ngram_block - 1):]) if ngram_block > 1 else ()
# Check if this n-gram appeared before
for i in range(len(generated) - ngram_block + 1):
ng = tuple(generated[i:i + ngram_block - 1]) if ngram_block > 1 else ()
if ng == recent_ngram and i + ngram_block - 1 < len(generated):
blocked_token = generated[i + ngram_block - 1]
logits[blocked_token] *= 0.3 # penalize but don't forbid
# Select next token
if temperature <= 1e-6:
next_id = int(np.argmax(logits))
else:
z = logits / temperature
z -= z.max()
e = np.exp(z)
probs = e / e.sum()
probs[PAD] = 0.0
s = probs.sum()
if s <= 0:
break
probs /= s
next_id = int(rng.choice(len(probs), p=probs))
# Track if this token is "novel" (different from what standard
# retrieval would predict)
standard_logits = self.lm._logits(ids)
standard_next = int(np.argmax(standard_logits))
if next_id != standard_next:
novel += 1
generated.append(next_id)
ids.append(next_id)
candidates_per_step.append(self.top_k)
if next_id == EOS:
break
text = tok.decode(generated)
avg_diversity = novel / max(len(generated), 1)
return CreativeResult(
text=text,
n_candidates_per_step=candidates_per_step,
novel_tokens=novel,
temperature=temperature,
mixture_diversity=avg_diversity,
)
def respond(self, question: str, max_new_tokens: int = 100,
temperature: float = 0.0) -> str:
"""Respond to a question using creative generation."""
assert self.lm.tokenizer is not None
q_ids = self.lm.tokenizer.encode(question, add_bos=True, add_eos=True)
ctx = q_ids + [1] # BOS
result = self.generate(question, max_new_tokens=max_new_tokens,
temperature=temperature)
return result.text