Spaces:
Sleeping
Sleeping
File size: 11,677 Bytes
156e2a6 | 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 | """
Sparse trigram model with adaptive bigram+trigram counting.
Instead of building full 49K probability arrays, we work with sparse
representations: only top-K tokens per context + one "rest" bucket.
The CDF has ~TOP_K+1 entries instead of 49,153.
The arithmetic coder encodes a "symbol index" in range [0, TOP_K].
Index 0..TOP_K-1 = specific tokens, index TOP_K = rest bucket.
We maintain a mapping from token_id → symbol_index per step.
"""
import sys
from collections import Counter, defaultdict
import numpy as np
CDF_TOTAL = 1 << 16
MIN_PROB = 1
LAMBDA_TRI = 0.70
LAMBDA_BI = 0.25
LAMBDA_UNI = 0.05
LAMBDA_BI_ONLY = 0.80
LAMBDA_UNI_ONLY = 0.20
MAX_ADAPTIVE_WEIGHT = 0.35
ADAPTIVE_RAMP_TOKENS = 800
# How many tokens to keep in the sparse CDF (rest go into one bucket)
SPARSE_TOP_K = 512
class TrigramModel:
def __init__(self, table_path: str, verbose: bool = True):
self.verbose = verbose
self._load_table(table_path)
def _load_table(self, path: str):
if self.verbose:
print(f"Loading trigram table: {path}", file=sys.stderr)
data = np.load(path, allow_pickle=True)
self.vocab_size = int(data["vocab_size"][0])
self.tokenizer_name = str(data["tokenizer_name"][0])
self.unigram_probs = data["unigram_probs"].astype(np.float64)
self.bi_ctx_keys = data["bigram_context_keys"]
self.bi_top_tokens = data["bigram_top_tokens"]
self.bi_top_probs = data["bigram_top_probs"].astype(np.float64)
self.bi_remaining = data["bigram_remaining_mass"].astype(np.float64)
self.tri_ctx_keys = data["trigram_context_keys"]
self.tri_top_tokens = data["trigram_top_tokens"]
self.tri_top_probs = data["trigram_top_probs"].astype(np.float64)
self.tri_remaining = data["trigram_remaining_mass"].astype(np.float64)
# Precompute sorted unigram top-K indices for fast sparse building
self._uni_top_idx = np.argsort(self.unigram_probs)[::-1][:SPARSE_TOP_K].copy()
self._uni_top_set = set(self._uni_top_idx.tolist())
if self.verbose:
print(f" Vocab size: {self.vocab_size:,}", file=sys.stderr)
print(f" Bigram contexts: {len(self.bi_ctx_keys):,}", file=sys.stderr)
print(f" Trigram contexts: {len(self.tri_ctx_keys):,}", file=sys.stderr)
print(f" Sparse top-K: {SPARSE_TOP_K}", file=sys.stderr)
def _lookup_bigram(self, prev1):
idx = np.searchsorted(self.bi_ctx_keys, prev1)
if idx < len(self.bi_ctx_keys) and self.bi_ctx_keys[idx] == prev1:
return self.bi_top_tokens[idx], self.bi_top_probs[idx], self.bi_remaining[idx]
return None
def _lookup_trigram(self, prev2, prev1):
key = np.uint32((prev2 & 0xFFFF) << 16 | (prev1 & 0xFFFF))
idx = np.searchsorted(self.tri_ctx_keys, key)
if idx < len(self.tri_ctx_keys) and self.tri_ctx_keys[idx] == key:
return self.tri_top_tokens[idx], self.tri_top_probs[idx], self.tri_remaining[idx]
return None
class AdaptiveTrigramModel:
def __init__(self, table_path: str, verbose: bool = True):
self.static = TrigramModel(table_path, verbose=verbose)
self.vocab_size = self.static.vocab_size
self.tokenizer_name = self.static.tokenizer_name
self.reset()
def reset(self):
self.adapt_bi = defaultdict(Counter)
self.adapt_tri = defaultdict(Counter)
self.tokens_seen = 0
self._prev1 = None
self._prev2 = None
def update(self, token_id):
if self._prev1 is not None:
self.adapt_bi[self._prev1][token_id] += 1
if self._prev2 is not None and self._prev1 is not None:
self.adapt_tri[(self._prev2, self._prev1)][token_id] += 1
self._prev2 = self._prev1
self._prev1 = token_id
self.tokens_seen += 1
def get_sparse_cdf(self, context):
"""Return (token_ids, cdf) where token_ids has K entries and cdf has K+1.
token_ids[i] is the actual token id for symbol index i.
Symbol index K (= len(token_ids)) is the "rest" bucket covering all
tokens NOT in token_ids.
cdf[0] = 0, cdf[-1] = CDF_TOTAL.
"""
s = self.static
# --- Step 1: Collect candidate token set and their raw probabilities ---
# Start with unigram top tokens
candidates = set(s._uni_top_set)
# Gather raw component probabilities for candidates
# We'll store: for each candidate token, its static prob
# Also collect all tokens from bigram/trigram lookups + adaptive
bi_result = None
tri_result = None
if len(context) >= 1:
prev1 = context[-1]
bi_result = s._lookup_bigram(prev1)
if bi_result is not None:
valid = bi_result[1] > 0
candidates.update(bi_result[0][valid].tolist())
if len(context) >= 2:
prev2 = context[-2]
tri_result = s._lookup_trigram(prev2, prev1)
if tri_result is not None:
valid = tri_result[1] > 0
candidates.update(tri_result[0][valid].tolist())
# Add adaptive tokens
lambda_a = min(MAX_ADAPTIVE_WEIGHT, self.tokens_seen / ADAPTIVE_RAMP_TOKENS)
ada_bi_counter = None
ada_tri_counter = None
if lambda_a > 1e-12 and len(context) >= 1:
prev1 = context[-1]
ada_bi_counter = self.adapt_bi.get(prev1)
if ada_bi_counter:
candidates.update(ada_bi_counter.keys())
if len(context) >= 2:
prev2 = context[-2]
ada_tri_counter = self.adapt_tri.get((prev2, prev1))
if ada_tri_counter:
candidates.update(ada_tri_counter.keys())
# Convert to sorted array for deterministic order
token_ids = np.array(sorted(candidates), dtype=np.int64)
n = len(token_ids)
# --- Step 2: Build probability for each candidate ---
# Unigram probs for candidates
uni_probs = s.unigram_probs[token_ids]
# Static interpolation
if len(context) >= 2 and tri_result is not None and bi_result is not None:
# Full trigram + bigram + unigram
bi_probs = uni_probs * bi_result[2]
# Override with bigram top entries that appear in our candidates
bi_tok = bi_result[0]
bi_p = bi_result[1]
bi_valid = bi_p > 0
if bi_valid.any():
_map_into(bi_probs, token_ids, bi_tok[bi_valid], bi_p[bi_valid])
tri_probs = bi_probs * tri_result[2]
tri_tok = tri_result[0]
tri_p = tri_result[1]
tri_valid = tri_p > 0
if tri_valid.any():
_map_into(tri_probs, token_ids, tri_tok[tri_valid], tri_p[tri_valid])
static_probs = LAMBDA_TRI * tri_probs + LAMBDA_BI * bi_probs + LAMBDA_UNI * uni_probs
elif len(context) >= 2 and tri_result is not None:
tri_probs = uni_probs * tri_result[2]
tri_tok = tri_result[0]
tri_p = tri_result[1]
tri_valid = tri_p > 0
if tri_valid.any():
_map_into(tri_probs, token_ids, tri_tok[tri_valid], tri_p[tri_valid])
static_probs = (LAMBDA_TRI + LAMBDA_BI) * tri_probs + LAMBDA_UNI * uni_probs
elif bi_result is not None:
bi_probs = uni_probs * bi_result[2]
bi_tok = bi_result[0]
bi_p = bi_result[1]
bi_valid = bi_p > 0
if bi_valid.any():
_map_into(bi_probs, token_ids, bi_tok[bi_valid], bi_p[bi_valid])
static_probs = LAMBDA_BI_ONLY * bi_probs + LAMBDA_UNI_ONLY * uni_probs
else:
static_probs = uni_probs.copy()
# --- Step 3: Adaptive mixing ---
if lambda_a > 1e-12 and (ada_bi_counter or ada_tri_counter):
ada_bi_dist = None
if ada_bi_counter:
ada_bi_dist = _build_adaptive_sparse(
ada_bi_counter, token_ids, static_probs)
ada_tri_dist = None
if ada_tri_counter:
ada_tri_dist = _build_adaptive_sparse(
ada_tri_counter, token_ids, static_probs)
if ada_tri_dist is not None and ada_bi_dist is not None:
adaptive = 0.6 * ada_tri_dist + 0.4 * ada_bi_dist
elif ada_tri_dist is not None:
adaptive = ada_tri_dist
else:
adaptive = ada_bi_dist
final_probs = (1.0 - lambda_a) * static_probs + lambda_a * adaptive
else:
final_probs = static_probs
# --- Step 4: Compute rest mass (probability not in our candidates) ---
final_probs = np.maximum(final_probs, 1e-10)
candidate_sum = final_probs.sum()
rest_mass = max(1e-10, 1.0 - candidate_sum)
# --- Step 5: Build integer CDF with rest bucket at end ---
# We have n candidates + 1 rest bucket = n+1 symbols
total_symbols = n + 1
usable = CDF_TOTAL - total_symbols * MIN_PROB
all_probs = np.empty(total_symbols, dtype=np.float64)
all_probs[:n] = final_probs
all_probs[n] = rest_mass
all_probs /= all_probs.sum()
counts = (all_probs * usable).astype(np.int64)
counts = np.maximum(counts, 0) + MIN_PROB
diff = CDF_TOTAL - counts.sum()
if diff != 0:
counts[counts.argmax()] += diff
cdf = np.zeros(total_symbols + 1, dtype=np.int64)
np.cumsum(counts, out=cdf[1:])
cdf[-1] = CDF_TOTAL
return token_ids, cdf.tolist()
def get_rest_rank(self, token_id, excluded_sorted):
"""Map token_id to its rank among non-excluded tokens.
rank = how many tokens with id < token_id are NOT in excluded_sorted.
excluded_sorted must be a sorted list.
"""
rest_size = self.vocab_size - len(excluded_sorted)
if rest_size <= 0:
rest_size = 1
# rank = token_id minus (number of excluded tokens < token_id)
# excluded_sorted is sorted, binary search for count
lo, hi = 0, len(excluded_sorted)
while lo < hi:
mid = (lo + hi) >> 1
if excluded_sorted[mid] < token_id:
lo = mid + 1
else:
hi = mid
rank = token_id - lo
return rest_size, rank
def _map_into(target, target_tokens, src_tokens, src_probs):
"""Set target[i] = src_probs[j] where target_tokens[i] == src_tokens[j]."""
# Both are sorted, use merge-style
idx = np.searchsorted(target_tokens, src_tokens)
valid = (idx < len(target_tokens)) & (target_tokens[idx] == src_tokens)
target[idx[valid]] = src_probs[valid]
def _build_adaptive_sparse(counter, token_ids, static_probs):
"""Build adaptive distribution over sparse token_ids from a Counter."""
n = len(token_ids)
dist = static_probs.copy()
if not counter:
return dist
obs_tokens = np.array(list(counter.keys()), dtype=np.int64)
obs_counts = np.array(list(counter.values()), dtype=np.float64)
total = obs_counts.sum()
denom = total + len(obs_tokens) + 1.0
smoothed = (obs_counts + 1.0) / denom
remaining_frac = 1.0 / denom
dist *= remaining_frac
# Map observed tokens into our sparse set
idx = np.searchsorted(token_ids, obs_tokens)
valid = (idx < n) & (token_ids[idx] == obs_tokens)
dist[idx[valid]] = smoothed[valid]
s = dist.sum()
if s > 0:
dist /= s
return dist
|