lowerkey commited on
Commit
76d09fe
·
verified ·
1 Parent(s): 02afb58

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. bard_core.py +16 -79
  2. bard_game.py +13 -45
  3. bard_tokenizer.py +4 -8
  4. configuration_bard.py +1 -4
  5. modeling_bard.py +2 -9
bard_core.py CHANGED
@@ -1,4 +1,4 @@
1
- """The network of bard-small, taken whole from the training code (fictionet/model.py, numhead.py, train/pretrain.py): windowed attention with a key/value cache, the number encoder/decoder, the LM."""
2
  import re
3
  import math
4
  import time
@@ -7,7 +7,7 @@ from functools import lru_cache
7
  import torch
8
  from torch import nn
9
  from torch.nn import functional as F
10
- try: # only the training path (and a full forward without a cache) uses it
11
  from torch.nn.attention.flex_attention import create_block_mask, flex_attention
12
  except ImportError: # pragma: no cover
13
  create_block_mask = flex_attention = None
@@ -25,9 +25,6 @@ def sliding_window_block_mask(T: int, window: int, device: str):
25
 
26
 
27
  def apply_rope(x, base=10000.0, offset=0):
28
- """Rotary embedding on (B, H, T, D) for absolute positions offset .. offset+T-1.
29
- Frequencies are built in fp32 inside forward, so a .bfloat16() model cast can't
30
- quantise them (a registered buffer would get cast)."""
31
  T, D = x.shape[-2], x.shape[-1]
32
  inv_freq = base ** (-torch.arange(0, D, 2, device=x.device, dtype=torch.float32) / D)
33
  ang = torch.arange(offset, offset + T, device=x.device, dtype=torch.float32)[:, None] * inv_freq
@@ -37,23 +34,14 @@ def apply_rope(x, base=10000.0, offset=0):
37
 
38
 
39
  class LayerKV:
40
- """One layer's cached keys and values, (B, H, S, D), in a buffer that grows by doubling.
41
-
42
- Keys are stored after RoPE, at their absolute positions, so they never need
43
- re-rotating. A sliding-window layer keeps only the last `window - 1` keys - all
44
- any later query can still see - so its memory stays bounded however long the
45
- sequence runs; the kept span is moved to the front only when the buffer fills.
46
- """
47
 
48
  def __init__(self, window):
49
  self.window = window
50
- self.k = self.v = None # buffers
51
- self.lo = self.hi = 0 # live span [lo, hi) of the buffers
52
- self.start = 0 # absolute position of the key at `lo`
53
 
54
  def append(self, k, v):
55
- """Add T new keys/values; return all visible keys, values and the absolute
56
- position of the first of them."""
57
  T = k.shape[2]
58
  if self.k is None:
59
  cap = max(64, 2 * T)
@@ -62,7 +50,7 @@ class LayerKV:
62
  elif self.hi + T > self.k.shape[2]:
63
  live = self.hi - self.lo
64
  cap = self.k.shape[2]
65
- if live + T > cap // 2: # still mostly full after compacting: grow
66
  cap = 2 * (live + T)
67
  k_buf = self.k.new_empty(self.k.shape[:2] + (cap,) + self.k.shape[3:])
68
  v_buf = self.v.new_empty(k_buf.shape)
@@ -75,7 +63,6 @@ class LayerKV:
75
  return self.k[:, :, self.lo:self.hi], self.v[:, :, self.lo:self.hi], self.start
76
 
77
  def trim(self):
78
- """After this step's attention: forget keys no future query can reach."""
79
  if self.window is not None and self.hi - self.lo > self.window - 1:
80
  drop = self.hi - self.lo - (self.window - 1)
81
  self.lo += drop
@@ -83,9 +70,6 @@ class LayerKV:
83
 
84
 
85
  class KVCache:
86
- """Keys/values for every block of a CausalTransformer, plus how many tokens it has
87
- seen. Pass it to CausalTransformer.forward (or LM.hidden) to process only the new
88
- tokens: any number of them, on top of any amount already cached."""
89
 
90
  def __init__(self, transformer):
91
  self.windows = [block.window for block in transformer.blocks]
@@ -100,15 +84,6 @@ class KVCache:
100
 
101
 
102
  def attend(q, k, v, rope, window, block_mask, cache=None, pos=0):
103
- """Causal (optionally sliding-window) attention for (B, H, T, D) q/k/v.
104
-
105
- Without a cache this is exactly the training path. With one, q/k/v hold only the
106
- T new tokens at absolute positions pos .. pos+T-1: keys are rotated at those
107
- positions, appended to the cache, and the queries attend over cached + new keys
108
- under the true mask - key <= query, and query - key < window on sliding-window
109
- layers - so a chunk of any length (including one crossing the window edge) sees
110
- exactly what it would in a full forward pass.
111
- """
112
  if cache is None:
113
  if rope:
114
  q, k = apply_rope(q), apply_rope(k)
@@ -122,9 +97,9 @@ def attend(q, k, v, rope, window, block_mask, cache=None, pos=0):
122
  k_all, v_all, k_start = cache.append(k, v)
123
  S = k_all.shape[2]
124
  if T == 1 and (window is None or S <= window):
125
- out = F.scaled_dot_product_attention(q, k_all, v_all) # decode: all visible
126
  elif S == T and (window is None or T <= window):
127
- out = F.scaled_dot_product_attention(q, k_all, v_all, is_causal=True) # fresh prefill
128
  else:
129
  q_pos = torch.arange(pos, pos + T, device=q.device)[:, None]
130
  k_pos = torch.arange(k_start, k_start + S, device=q.device)[None, :]
@@ -143,7 +118,7 @@ class Attention(nn.Module):
143
  self.d_model = d_model
144
  self.heads = heads
145
  self.head_dim = d_model // heads
146
- self.window = window # None -> global causal attention
147
  self.rope = rope
148
 
149
  self.qkvg = nn.Linear(d_model, 4 * d_model, bias=False)
@@ -205,13 +180,6 @@ class Block(nn.Module):
205
 
206
 
207
  class ParallelBlock(nn.Module):
208
- """PaLM-style parallel block: x + Attn(norm(x)) + FFN(norm(x)), actually fused.
209
-
210
- One norm; ONE input matmul producing q|k|v|g|gate|up; ONE output matmul over
211
- [gated attn | swiglu] (= attn.out and W_down stacked along the input dim, which is
212
- exactly out(a) + W_down(f)). 2 matmuls per block instead of 4, each wider, which is
213
- what a narrow model on a small GPU needs to keep the tensor cores fed.
214
- """
215
 
216
  def __init__(self, d_model, heads, ffn_expansion=4, window=None, rope=False):
217
  super().__init__()
@@ -265,9 +233,6 @@ class CausalTransformer(nn.Module):
265
  parallel: bool = False,
266
  rope: str = "local",
267
  ):
268
- """rope: "local" = RoPE on sliding-window layers, NoPE on global ones (RNoPE-SWA,
269
- Yang et al. 2025; SmolLM3 ablation at 3B: better long context, no short-context
270
- cost; NoPE LMs stay competitive down to ~250M, Haviv et al. 2022), "all", or "none"."""
271
  super().__init__()
272
  self.grad_checkpoint = grad_checkpoint
273
 
@@ -310,12 +275,6 @@ class CausalTransformer(nn.Module):
310
  positions, like steering, need it). 0 otherwise."""
311
 
312
  def forward(self, x, masks=None, cache=None):
313
- """masks: precomputed block_masks(T, device); pass them when this runs inside an outer
314
- torch.compile, so mask construction stays out of the compiled graph.
315
-
316
- cache: a KVCache. x then holds only the tokens not yet in it (any number), the
317
- cache is extended with them, and the result is identical to running the whole
318
- sequence - at the cost of the new tokens only."""
319
  if cache is not None:
320
  pos = cache.pos
321
  self.cache_offset = pos
@@ -340,19 +299,13 @@ class CausalTransformer(nn.Module):
340
  return self.norm(x)
341
 
342
 
343
- # ----------------------------------------------------------------------------- numbers
344
-
345
- import re # noqa: E402
346
-
347
- MAX_DIGITS = 8 # 10**8 - 1 < 2**27
348
  LEN_SHIFT = 27
349
  VALUE_MASK = (1 << LEN_SHIFT) - 1
350
 
351
  NUMBER_RE = re.compile(r"( ?)([0-9]+)")
352
 
353
 
354
- # ----------------------------------------------------------------------------- packing
355
-
356
  def pack(digits: str) -> int:
357
  assert 0 < len(digits) <= MAX_DIGITS
358
  return int(digits) | (len(digits) << LEN_SHIFT)
@@ -363,7 +316,6 @@ def unpack(packed: int) -> str:
363
 
364
 
365
  def split_numbers(text):
366
- """-> list of str (text segment) and (spaced: bool, packed: int) items, in order."""
367
  out, pos = [], 0
368
  for m in NUMBER_RE.finditer(text):
369
  if m.start() > pos:
@@ -386,8 +338,6 @@ def _pow10(n, device):
386
  return 10 ** torch.arange(n, device=device, dtype=torch.long)
387
 
388
 
389
- # ----------------------------------------------------------------------------- encoder
390
-
391
  class NumberEncoder(nn.Module):
392
  def __init__(self, d_model, n_freq=4):
393
  super().__init__()
@@ -400,9 +350,8 @@ class NumberEncoder(nn.Module):
400
  nn.init.zeros_(m.bias)
401
 
402
  def forward(self, packed):
403
- """packed: (...) int, 0 where the token is not a number -> (..., d_model), zero there."""
404
  L, value = _lengths_values(packed)
405
- digits = value.unsqueeze(-1) // _pow10(MAX_DIGITS, packed.device) % 10 # LSB first
406
  present = (torch.arange(MAX_DIGITS, device=packed.device) < L.unsqueeze(-1)).float()
407
  freqs = torch.arange(1, self.n_freq + 1, dtype=torch.float32, device=packed.device)
408
  angle = 2 * math.pi * digits.float().unsqueeze(-1) / 10 * freqs
@@ -417,8 +366,6 @@ class NumberEncoder(nn.Module):
417
  return out * (packed != 0).unsqueeze(-1).to(out.dtype)
418
 
419
 
420
- # ----------------------------------------------------------------------------- decoder
421
-
422
  class NumberDecoder(nn.Module):
423
  def __init__(self, d_model, width=256, heads=4, layers=2):
424
  super().__init__()
@@ -426,8 +373,8 @@ class NumberDecoder(nn.Module):
426
  nn.Linear(width, MAX_DIGITS, bias=False))
427
  self.ctx = nn.Linear(d_model, width, bias=False)
428
  self.len_emb = nn.Embedding(MAX_DIGITS, width)
429
- self.place_emb = nn.Embedding(MAX_DIGITS, width) # power of ten of the slot
430
- self.prev_emb = nn.Embedding(11, width) # previous digit, 10 = start
431
  self.blocks = nn.ModuleList([Block(width, heads) for _ in range(layers)])
432
  self.norm = nn.RMSNorm(width)
433
  self.out = nn.Linear(width, 10, bias=False)
@@ -439,12 +386,11 @@ class NumberDecoder(nn.Module):
439
  nn.init.normal_(b.ffn.W_down.weight, std=0.02 / math.sqrt(2 * layers))
440
 
441
  def _digit_logits(self, h, L, prev):
442
- """h (N, d), L (N,) in 1..MAX, prev (N, MAX) previous-digit ids -> (N, MAX, 10)."""
443
  j = torch.arange(MAX_DIGITS, device=h.device)
444
- power = (L.unsqueeze(-1) - 1 - j).clamp(min=0) # slots past L are masked anyway
445
  x = (self.ctx(h) + self.len_emb(L - 1)).unsqueeze(1) + self.place_emb(power) + self.prev_emb(prev)
446
  for b in self.blocks:
447
- x = b(x) # no mask -> causal SDPA
448
  return self.out(self.norm(x))
449
 
450
  @staticmethod
@@ -456,9 +402,6 @@ class NumberDecoder(nn.Module):
456
  return L, digits, valid
457
 
458
  def nll(self, h, packed, weight=None):
459
- """Summed NLL (length + every digit) of the target numbers. h (N, d), packed (N,).
460
- weight (N,) masks padding rows, so N can be a fixed capacity (static shapes -> compile);
461
- padded rows must still hold a valid packed number (e.g. pack("0"))."""
462
  L, digits, valid = self._msb_digits(packed)
463
  prev = torch.cat([torch.full_like(digits[:, :1], 10), digits[:, :-1]], 1)
464
  len_nll = F.cross_entropy(self.len_head(h).float(), L - 1, reduction="none")
@@ -469,7 +412,6 @@ class NumberDecoder(nn.Module):
469
 
470
  @torch.no_grad()
471
  def generate(self, h, temperature=0.0):
472
- """h (N, d) -> packed numbers (N,). temperature 0 = greedy."""
473
  def pick(logits):
474
  if temperature <= 0:
475
  return logits.argmax(-1)
@@ -488,19 +430,15 @@ class NumberDecoder(nn.Module):
488
  return value | (L << LEN_SHIFT)
489
 
490
 
491
- # ----------------------------------------------------------------------------- the language model
492
-
493
  LOGIT_SOFTCAP = 30.0
494
 
495
 
496
  def softcap(logits):
497
- # Gemma 2 / modded-nanogpt: bounds the logits smoothly
498
  return LOGIT_SOFTCAP * torch.tanh(logits.float() / LOGIT_SOFTCAP)
499
 
500
 
501
  class LM(nn.Module):
502
- """Token LM + dedicated number path: <|num|>/<|snum|> inputs get NumberEncoder(value) added to their embedding;
503
- where the next token is a number, NumberDecoder predicts its digits."""
504
 
505
  def __init__(self, vocab, d_model, heads, n_blocks, window, global_every, parallel=True, rope="local"):
506
  super().__init__()
@@ -512,5 +450,4 @@ class LM(nn.Module):
512
  self.num_dec = NumberDecoder(d_model)
513
 
514
  def hidden(self, idx, vals, masks=None, cache=None):
515
- """cache: a KVCache - then idx/vals hold only the tokens not yet in it."""
516
  return self.body(self.emb(idx) + self.num_enc(vals), masks, cache)
 
1
+ """The network of bard-small."""
2
  import re
3
  import math
4
  import time
 
7
  import torch
8
  from torch import nn
9
  from torch.nn import functional as F
10
+ try:
11
  from torch.nn.attention.flex_attention import create_block_mask, flex_attention
12
  except ImportError: # pragma: no cover
13
  create_block_mask = flex_attention = None
 
25
 
26
 
27
  def apply_rope(x, base=10000.0, offset=0):
 
 
 
28
  T, D = x.shape[-2], x.shape[-1]
29
  inv_freq = base ** (-torch.arange(0, D, 2, device=x.device, dtype=torch.float32) / D)
30
  ang = torch.arange(offset, offset + T, device=x.device, dtype=torch.float32)[:, None] * inv_freq
 
34
 
35
 
36
  class LayerKV:
 
 
 
 
 
 
 
37
 
38
  def __init__(self, window):
39
  self.window = window
40
+ self.k = self.v = None
41
+ self.lo = self.hi = 0
42
+ self.start = 0
43
 
44
  def append(self, k, v):
 
 
45
  T = k.shape[2]
46
  if self.k is None:
47
  cap = max(64, 2 * T)
 
50
  elif self.hi + T > self.k.shape[2]:
51
  live = self.hi - self.lo
52
  cap = self.k.shape[2]
53
+ if live + T > cap // 2:
54
  cap = 2 * (live + T)
55
  k_buf = self.k.new_empty(self.k.shape[:2] + (cap,) + self.k.shape[3:])
56
  v_buf = self.v.new_empty(k_buf.shape)
 
63
  return self.k[:, :, self.lo:self.hi], self.v[:, :, self.lo:self.hi], self.start
64
 
65
  def trim(self):
 
66
  if self.window is not None and self.hi - self.lo > self.window - 1:
67
  drop = self.hi - self.lo - (self.window - 1)
68
  self.lo += drop
 
70
 
71
 
72
  class KVCache:
 
 
 
73
 
74
  def __init__(self, transformer):
75
  self.windows = [block.window for block in transformer.blocks]
 
84
 
85
 
86
  def attend(q, k, v, rope, window, block_mask, cache=None, pos=0):
 
 
 
 
 
 
 
 
 
87
  if cache is None:
88
  if rope:
89
  q, k = apply_rope(q), apply_rope(k)
 
97
  k_all, v_all, k_start = cache.append(k, v)
98
  S = k_all.shape[2]
99
  if T == 1 and (window is None or S <= window):
100
+ out = F.scaled_dot_product_attention(q, k_all, v_all)
101
  elif S == T and (window is None or T <= window):
102
+ out = F.scaled_dot_product_attention(q, k_all, v_all, is_causal=True)
103
  else:
104
  q_pos = torch.arange(pos, pos + T, device=q.device)[:, None]
105
  k_pos = torch.arange(k_start, k_start + S, device=q.device)[None, :]
 
118
  self.d_model = d_model
119
  self.heads = heads
120
  self.head_dim = d_model // heads
121
+ self.window = window
122
  self.rope = rope
123
 
124
  self.qkvg = nn.Linear(d_model, 4 * d_model, bias=False)
 
180
 
181
 
182
  class ParallelBlock(nn.Module):
 
 
 
 
 
 
 
183
 
184
  def __init__(self, d_model, heads, ffn_expansion=4, window=None, rope=False):
185
  super().__init__()
 
233
  parallel: bool = False,
234
  rope: str = "local",
235
  ):
 
 
 
236
  super().__init__()
237
  self.grad_checkpoint = grad_checkpoint
238
 
 
275
  positions, like steering, need it). 0 otherwise."""
276
 
277
  def forward(self, x, masks=None, cache=None):
 
 
 
 
 
 
278
  if cache is not None:
279
  pos = cache.pos
280
  self.cache_offset = pos
 
299
  return self.norm(x)
300
 
301
 
302
+ MAX_DIGITS = 8
 
 
 
 
303
  LEN_SHIFT = 27
304
  VALUE_MASK = (1 << LEN_SHIFT) - 1
305
 
306
  NUMBER_RE = re.compile(r"( ?)([0-9]+)")
307
 
308
 
 
 
309
  def pack(digits: str) -> int:
310
  assert 0 < len(digits) <= MAX_DIGITS
311
  return int(digits) | (len(digits) << LEN_SHIFT)
 
316
 
317
 
318
  def split_numbers(text):
 
319
  out, pos = [], 0
320
  for m in NUMBER_RE.finditer(text):
321
  if m.start() > pos:
 
338
  return 10 ** torch.arange(n, device=device, dtype=torch.long)
339
 
340
 
 
 
341
  class NumberEncoder(nn.Module):
342
  def __init__(self, d_model, n_freq=4):
343
  super().__init__()
 
350
  nn.init.zeros_(m.bias)
351
 
352
  def forward(self, packed):
 
353
  L, value = _lengths_values(packed)
354
+ digits = value.unsqueeze(-1) // _pow10(MAX_DIGITS, packed.device) % 10
355
  present = (torch.arange(MAX_DIGITS, device=packed.device) < L.unsqueeze(-1)).float()
356
  freqs = torch.arange(1, self.n_freq + 1, dtype=torch.float32, device=packed.device)
357
  angle = 2 * math.pi * digits.float().unsqueeze(-1) / 10 * freqs
 
366
  return out * (packed != 0).unsqueeze(-1).to(out.dtype)
367
 
368
 
 
 
369
  class NumberDecoder(nn.Module):
370
  def __init__(self, d_model, width=256, heads=4, layers=2):
371
  super().__init__()
 
373
  nn.Linear(width, MAX_DIGITS, bias=False))
374
  self.ctx = nn.Linear(d_model, width, bias=False)
375
  self.len_emb = nn.Embedding(MAX_DIGITS, width)
376
+ self.place_emb = nn.Embedding(MAX_DIGITS, width)
377
+ self.prev_emb = nn.Embedding(11, width)
378
  self.blocks = nn.ModuleList([Block(width, heads) for _ in range(layers)])
379
  self.norm = nn.RMSNorm(width)
380
  self.out = nn.Linear(width, 10, bias=False)
 
386
  nn.init.normal_(b.ffn.W_down.weight, std=0.02 / math.sqrt(2 * layers))
387
 
388
  def _digit_logits(self, h, L, prev):
 
389
  j = torch.arange(MAX_DIGITS, device=h.device)
390
+ power = (L.unsqueeze(-1) - 1 - j).clamp(min=0)
391
  x = (self.ctx(h) + self.len_emb(L - 1)).unsqueeze(1) + self.place_emb(power) + self.prev_emb(prev)
392
  for b in self.blocks:
393
+ x = b(x)
394
  return self.out(self.norm(x))
395
 
396
  @staticmethod
 
402
  return L, digits, valid
403
 
404
  def nll(self, h, packed, weight=None):
 
 
 
405
  L, digits, valid = self._msb_digits(packed)
406
  prev = torch.cat([torch.full_like(digits[:, :1], 10), digits[:, :-1]], 1)
407
  len_nll = F.cross_entropy(self.len_head(h).float(), L - 1, reduction="none")
 
412
 
413
  @torch.no_grad()
414
  def generate(self, h, temperature=0.0):
 
415
  def pick(logits):
416
  if temperature <= 0:
417
  return logits.argmax(-1)
 
430
  return value | (L << LEN_SHIFT)
431
 
432
 
 
 
433
  LOGIT_SOFTCAP = 30.0
434
 
435
 
436
  def softcap(logits):
437
+
438
  return LOGIT_SOFTCAP * torch.tanh(logits.float() / LOGIT_SOFTCAP)
439
 
440
 
441
  class LM(nn.Module):
 
 
442
 
443
  def __init__(self, vocab, d_model, heads, n_blocks, window, global_every, parallel=True, rope="local"):
444
  super().__init__()
 
450
  self.num_dec = NumberDecoder(d_model)
451
 
452
  def hidden(self, idx, vals, masks=None, cache=None):
 
453
  return self.body(self.emb(idx) + self.num_enc(vals), masks, cache)
bard_game.py CHANGED
@@ -1,10 +1,4 @@
1
- """The story engine of bard-small. The model narrates and proposes state changes and choices; this code applies the
2
- changes it believes, keeps the state, steers the length of a passage and the shape of the choices, and keeps the
3
- tale as one string in the model's own format (see format.txt).
4
-
5
- <|start|>state<|content|>money\\n100<|end|><|start|>story<|content|>...narration...<|vars|>money<|add|>-20
6
- <|choices|>Pay<|sep|>Run<|end|><|start|>state<|content|>money\\n80<|end|><|start|>player<|content|>Pay<|end|>...
7
- """
8
  import math
9
  import re
10
  import time
@@ -21,7 +15,7 @@ TITLE = "Stochastic Lands of Prophecy"
21
  DEFAULT_OPENING = "You wake up in a dungeon."
22
  MAX_INPUT = 300
23
  ANNOUNCE = {"money", "gold", "health"}
24
- LENGTHS = {"short": 60, "medium": 150, "long": None} # narration tokens a passage aims at
25
  SAMPLING = dict(temperature=0.8, top_k=50, top_p=0.95, rep_penalty=1.1, rep_window=128)
26
 
27
  OP_RE = re.compile(r"^\s*(.+?)\s*<\|(add|set)\|>\s*(-?\s*\d+)\s*$")
@@ -30,7 +24,7 @@ SENTENCE_DONE = re.compile(r"[.!?…][\"”’')\]]*(\s|$)")
30
  SPELLED_MARKER = re.compile(r"<?\|\s*(vars|choices|sep|end|add|set)\s*\|\s*>?", re.I)
31
  TURN = re.compile(r"<\|start\|>(state|story|player)<\|content\|>(.*?)<\|end\|>", re.S)
32
 
33
- # the story site's own interface links, not story choices ("End Game and Leave Comments", "Start over", ...)
34
  META_CHOICE = re.compile(
35
  r"leave (a )?comments?|rate (this|the) (story|game)|end (the )?game|give (me )?(a )?rating|"
36
  r"back to (the )?(menu|site|story list|main menu)|return to (the )?main menu|main menu|"
@@ -40,8 +34,7 @@ META_CHOICE = re.compile(
40
  r"(tale|story|game|adventure|line)\W*$|this is the end of (your|the) (adventure|story|game)",
41
  re.I)
42
 
43
- # changes the engine only believes when the narration is about them: a 137M model writes "money -10" after being
44
- # eaten alive; the passage has to mention money for that
45
  EVIDENCE = {
46
  "money": r"\b(money|coins?|gold|silver|copper|cash|dollars?|bucks|credits?|price|pay|paid|"
47
  r"buy|bought|sell|sold|cost|steal|stole|stolen|rob|robbed|reward|bribe|purse|wallet|"
@@ -51,7 +44,7 @@ EVIDENCE = {
51
  }
52
  EVIDENCE["gold"] = EVIDENCE["money"]
53
  EVIDENCE = {k: re.compile(v, re.I) for k, v in EVIDENCE.items()}
54
- AMOUNTS = {"money", "gold", "coins", "health"} # owned as an amount: spent and earned, never below 0
55
 
56
 
57
  def story_choices(choices):
@@ -64,7 +57,6 @@ def believable(name, narration):
64
 
65
 
66
  def sane(name, old, new):
67
- """Spending more than you have is refused, and so is a more than tenfold gain."""
68
  if name not in AMOUNTS or old is None:
69
  return True
70
  return 0 <= new <= max(1000, 10 * old)
@@ -90,7 +82,6 @@ def parse_state(content):
90
 
91
 
92
  def clean_input(text):
93
- """A player's words: no control characters, and no way to write a marker."""
94
  text = "".join(c for c in text if c == " " or unicodedata.category(c)[0] != "C")
95
  text = text.replace("<|", "<").replace("|>", ">")
96
  return re.sub(r"\s+", " ", text).strip()[:MAX_INPUT]
@@ -101,7 +92,6 @@ def repair(raw):
101
 
102
 
103
  def narration_so_far(raw):
104
- """What a reader may see of a turn being written: up to the first marker, and never a stray "<" or "|"."""
105
  visible = re.split(r"<\||\|", raw, maxsplit=1)[0]
106
  return visible[:-1] if visible.endswith("<") else visible
107
 
@@ -117,7 +107,6 @@ def unique(choices):
117
 
118
 
119
  def parse_turn(raw):
120
- """The model's output for one turn -> (narration, [(name, op, value)], [choices], problems)."""
121
  problems = []
122
  fixed = repair(raw)
123
  if fixed != raw:
@@ -143,18 +132,7 @@ def parse_turn(raw):
143
  return text.strip(), ops, choices, problems
144
 
145
 
146
- # ----------------------------------------------------------------------------- steering
147
-
148
  class LengthControl:
149
- """Steers one story turn (the adjust_logits hook of generate) without retraining.
150
-
151
- Narration: it cannot end before the model has written a sentence of its own after the opening; past a token budget
152
- the markers that end it (<|vars|>, <|choices|>) get a growing boost, only right after a sentence ends, so text is
153
- never cut mid-sentence; from twice the budget it ends at the next sentence end. <|end|> is only ever the model's
154
- own choice (it ends the story).
155
-
156
- Choices (once <|choices|> is written): at least MIN_CHOICES, none empty, none a copy of another, and a push away
157
- from retracing an earlier choice word by word."""
158
 
159
  MIN_CHOICES = 2
160
  MAX_CHOICES = 5
@@ -162,8 +140,8 @@ class LengthControl:
162
  COPY = 1.5
163
  COPY_MAX = 8.0
164
  ECHO = 4.0
165
- MIN_WORDS = 4 # a sentence of its own is at least this many words
166
- GIVE_UP = 90 # tokens after which a narration with no sentence in it is left to the model
167
 
168
  def __init__(self, tok, mode="medium", vars_weight=1.0):
169
  self.tok = tok
@@ -174,7 +152,7 @@ class LengthControl:
174
  self.stop = self.go_on + [tok.eos_id]
175
  first = min(tok.specials.values())
176
  self.silent = {i for i in range(first) if tok.vocab.id_to_token_decoded(i) == ""}
177
- self.rep_free = False # read by generate(): no repetition penalty while choices are written
178
  text_like = {self.sep_id, tok.eos_id, tok.specials["<|num|>"], tok.specials["<|snum|>"]}
179
  self.no_specials = torch.tensor([i for i in range(first, 1 << 14) if i not in text_like], dtype=torch.long)
180
  self.pipes = torch.tensor([i for i in range(first) if "|" in tok.vocab.id_to_token_decoded(i)], dtype=torch.long)
@@ -185,7 +163,6 @@ class LengthControl:
185
  self.mode, self.budget = mode, LENGTHS[mode]
186
 
187
  def start_part(self, raw):
188
- """Called before each generate() call of a turn; raw = the turn so far."""
189
  self.before = raw
190
  self.done = "<|" in raw
191
  self.offset = 0 if self.done else len(self.tok.encode(raw)[0])
@@ -276,13 +253,9 @@ class LengthControl:
276
  return logits
277
 
278
 
279
- # ----------------------------------------------------------------------------- sampling
280
-
281
  @torch.no_grad()
282
  def generate(lm, tok, seed, max_new, ctx, rng, temperature=0.8, top_k=50, top_p=0.95, rep_penalty=1.1,
283
  rep_window=128, adjust_logits=None):
284
- """Yield (token id, packed number) as they are sampled. The seed is read once into a key/value cache; every step
285
- then runs one token. Past `ctx` tokens the cache is rebuilt from the last 3/4 of it."""
286
  device = lm.head.weight.device
287
  seed_ids, seed_vals = tok.encode(seed)
288
  ids = [tok.eos_id] + seed_ids.tolist()
@@ -338,22 +311,19 @@ def generate(lm, tok, seed, max_new, ctx, rng, temperature=0.8, top_k=50, top_p=
338
  yield nxt, packed
339
 
340
 
341
- # ----------------------------------------------------------------------------- tales
342
-
343
  @dataclass
344
  class Tale:
345
- """A tale: the whole of it as one string in the model's format (`history`), and what the engine knows of it."""
346
 
347
  history: str
348
  state: dict = field(default_factory=dict)
349
  title: str = ""
350
  length: str = "medium"
351
- text: str = "" # the last passage
352
  choices: list = field(default_factory=list)
353
  ended: bool = False
354
  turns: int = 0
355
- pending: str | None = None # an opening not yet continued by the model
356
- changes: list = field(default_factory=list) # what the last turn did to the state: (name, old, new)
357
  created: float = field(default_factory=time.time)
358
 
359
  def to_dict(self):
@@ -364,7 +334,6 @@ class Tale:
364
  return cls(**d)
365
 
366
  def transcript(self):
367
- """What a reader saw: [("story", text), ("player", text), ...]."""
368
  out = []
369
  for role, content in TURN.findall(self.history):
370
  if role == "player":
@@ -391,7 +360,6 @@ class StoryEngine:
391
 
392
  @torch.no_grad()
393
  def turn(self, tale, move, on_text=None, seed=None, max_new=450, max_parts=20, **sampling):
394
- """Tell one turn of the tale (in place). move: the player's choice or own words (None for a new tale)."""
395
  if tale.pending is None:
396
  move = clean_input(move or "")
397
  if not move:
@@ -428,9 +396,9 @@ class StoryEngine:
428
  old = state.get(name)
429
  new = v if op == "set" else (old or 0) + v
430
  if not believable(name, text):
431
- continue # "money -10" for being eaten alive
432
  if name in ANNOUNCE and op == "set" and v != 0:
433
- continue # shown amounts are earned or spent, not set
434
  if not sane(name, old, new):
435
  continue
436
  state[name] = new
 
1
+ """The story engine of bard-small."""
 
 
 
 
 
 
2
  import math
3
  import re
4
  import time
 
15
  DEFAULT_OPENING = "You wake up in a dungeon."
16
  MAX_INPUT = 300
17
  ANNOUNCE = {"money", "gold", "health"}
18
+ LENGTHS = {"short": 60, "medium": 150, "long": None}
19
  SAMPLING = dict(temperature=0.8, top_k=50, top_p=0.95, rep_penalty=1.1, rep_window=128)
20
 
21
  OP_RE = re.compile(r"^\s*(.+?)\s*<\|(add|set)\|>\s*(-?\s*\d+)\s*$")
 
24
  SPELLED_MARKER = re.compile(r"<?\|\s*(vars|choices|sep|end|add|set)\s*\|\s*>?", re.I)
25
  TURN = re.compile(r"<\|start\|>(state|story|player)<\|content\|>(.*?)<\|end\|>", re.S)
26
 
27
+
28
  META_CHOICE = re.compile(
29
  r"leave (a )?comments?|rate (this|the) (story|game)|end (the )?game|give (me )?(a )?rating|"
30
  r"back to (the )?(menu|site|story list|main menu)|return to (the )?main menu|main menu|"
 
34
  r"(tale|story|game|adventure|line)\W*$|this is the end of (your|the) (adventure|story|game)",
35
  re.I)
36
 
37
+
 
38
  EVIDENCE = {
39
  "money": r"\b(money|coins?|gold|silver|copper|cash|dollars?|bucks|credits?|price|pay|paid|"
40
  r"buy|bought|sell|sold|cost|steal|stole|stolen|rob|robbed|reward|bribe|purse|wallet|"
 
44
  }
45
  EVIDENCE["gold"] = EVIDENCE["money"]
46
  EVIDENCE = {k: re.compile(v, re.I) for k, v in EVIDENCE.items()}
47
+ AMOUNTS = {"money", "gold", "coins", "health"}
48
 
49
 
50
  def story_choices(choices):
 
57
 
58
 
59
  def sane(name, old, new):
 
60
  if name not in AMOUNTS or old is None:
61
  return True
62
  return 0 <= new <= max(1000, 10 * old)
 
82
 
83
 
84
  def clean_input(text):
 
85
  text = "".join(c for c in text if c == " " or unicodedata.category(c)[0] != "C")
86
  text = text.replace("<|", "<").replace("|>", ">")
87
  return re.sub(r"\s+", " ", text).strip()[:MAX_INPUT]
 
92
 
93
 
94
  def narration_so_far(raw):
 
95
  visible = re.split(r"<\||\|", raw, maxsplit=1)[0]
96
  return visible[:-1] if visible.endswith("<") else visible
97
 
 
107
 
108
 
109
  def parse_turn(raw):
 
110
  problems = []
111
  fixed = repair(raw)
112
  if fixed != raw:
 
132
  return text.strip(), ops, choices, problems
133
 
134
 
 
 
135
  class LengthControl:
 
 
 
 
 
 
 
 
 
136
 
137
  MIN_CHOICES = 2
138
  MAX_CHOICES = 5
 
140
  COPY = 1.5
141
  COPY_MAX = 8.0
142
  ECHO = 4.0
143
+ MIN_WORDS = 4
144
+ GIVE_UP = 90
145
 
146
  def __init__(self, tok, mode="medium", vars_weight=1.0):
147
  self.tok = tok
 
152
  self.stop = self.go_on + [tok.eos_id]
153
  first = min(tok.specials.values())
154
  self.silent = {i for i in range(first) if tok.vocab.id_to_token_decoded(i) == ""}
155
+ self.rep_free = False
156
  text_like = {self.sep_id, tok.eos_id, tok.specials["<|num|>"], tok.specials["<|snum|>"]}
157
  self.no_specials = torch.tensor([i for i in range(first, 1 << 14) if i not in text_like], dtype=torch.long)
158
  self.pipes = torch.tensor([i for i in range(first) if "|" in tok.vocab.id_to_token_decoded(i)], dtype=torch.long)
 
163
  self.mode, self.budget = mode, LENGTHS[mode]
164
 
165
  def start_part(self, raw):
 
166
  self.before = raw
167
  self.done = "<|" in raw
168
  self.offset = 0 if self.done else len(self.tok.encode(raw)[0])
 
253
  return logits
254
 
255
 
 
 
256
  @torch.no_grad()
257
  def generate(lm, tok, seed, max_new, ctx, rng, temperature=0.8, top_k=50, top_p=0.95, rep_penalty=1.1,
258
  rep_window=128, adjust_logits=None):
 
 
259
  device = lm.head.weight.device
260
  seed_ids, seed_vals = tok.encode(seed)
261
  ids = [tok.eos_id] + seed_ids.tolist()
 
311
  yield nxt, packed
312
 
313
 
 
 
314
  @dataclass
315
  class Tale:
 
316
 
317
  history: str
318
  state: dict = field(default_factory=dict)
319
  title: str = ""
320
  length: str = "medium"
321
+ text: str = ""
322
  choices: list = field(default_factory=list)
323
  ended: bool = False
324
  turns: int = 0
325
+ pending: str | None = None
326
+ changes: list = field(default_factory=list)
327
  created: float = field(default_factory=time.time)
328
 
329
  def to_dict(self):
 
334
  return cls(**d)
335
 
336
  def transcript(self):
 
337
  out = []
338
  for role, content in TURN.findall(self.history):
339
  if role == "player":
 
360
 
361
  @torch.no_grad()
362
  def turn(self, tale, move, on_text=None, seed=None, max_new=450, max_parts=20, **sampling):
 
363
  if tale.pending is None:
364
  move = clean_input(move or "")
365
  if not move:
 
396
  old = state.get(name)
397
  new = v if op == "set" else (old or 0) + v
398
  if not believable(name, text):
399
+ continue
400
  if name in ANNOUNCE and op == "set" and v != 0:
401
+ continue
402
  if not sane(name, old, new):
403
  continue
404
  state[name] = new
bard_tokenizer.py CHANGED
@@ -1,5 +1,4 @@
1
- """The tokenizer of bard-small: tokenmonster (fiction-16000-strict-v1) plus the special tokens of the story format, and
2
- numbers kept apart: every run of digits is one <|num|>/<|snum|> token whose value travels in a parallel stream."""
3
  import os
4
  import re
5
 
@@ -7,8 +6,8 @@ import numpy as np
7
  import tokenmonster
8
 
9
  EOS = "<|end|>"
10
- NUM_MARKERS = ["<|num|>", "<|snum|>"] # a number, and a number preceded by one space
11
- MAX_DIGITS = 8 # 10**8 - 1 < 2**27
12
  LEN_SHIFT = 27
13
  VALUE_MASK = (1 << LEN_SHIFT) - 1
14
  NUMBER_RE = re.compile(r"( ?)([0-9]+)")
@@ -24,7 +23,6 @@ def unpack(packed: int) -> str:
24
 
25
 
26
  def split_numbers(text):
27
- """-> a list of str (text) and (spaced, packed) items, in order."""
28
  out, pos = [], 0
29
  for m in NUMBER_RE.finditer(text):
30
  if m.start() > pos:
@@ -60,7 +58,6 @@ class BardTokenizer:
60
  return max(self.specials.values()) + 1
61
 
62
  def encode(self, text):
63
- """-> (ids, values): token ids, and at each <|num|>/<|snum|> the packed number (else 0)."""
64
  plan = split_numbers(text)
65
  segs = [item for item in plan if isinstance(item, str)]
66
  seg_ids = iter(self.vocab.tokenize(segs) if segs else [])
@@ -89,8 +86,7 @@ class BardTokenizer:
89
  run = []
90
  parts.append((" " if i == self.snum_id else "") + unpack(v))
91
  elif i in self._special_text:
92
- # a marker is its own text: decoded inside a run, the capitalisation state of the token before it
93
- # can eat its first character
94
  if run:
95
  parts.append(self.vocab.decode(run))
96
  run = []
 
1
+ """The tokenizer of bard-small."""
 
2
  import os
3
  import re
4
 
 
6
  import tokenmonster
7
 
8
  EOS = "<|end|>"
9
+ NUM_MARKERS = ["<|num|>", "<|snum|>"]
10
+ MAX_DIGITS = 8
11
  LEN_SHIFT = 27
12
  VALUE_MASK = (1 << LEN_SHIFT) - 1
13
  NUMBER_RE = re.compile(r"( ?)([0-9]+)")
 
23
 
24
 
25
  def split_numbers(text):
 
26
  out, pos = [], 0
27
  for m in NUMBER_RE.finditer(text):
28
  if m.start() > pos:
 
58
  return max(self.specials.values()) + 1
59
 
60
  def encode(self, text):
 
61
  plan = split_numbers(text)
62
  segs = [item for item in plan if isinstance(item, str)]
63
  seg_ids = iter(self.vocab.tokenize(segs) if segs else [])
 
86
  run = []
87
  parts.append((" " if i == self.snum_id else "") + unpack(v))
88
  elif i in self._special_text:
89
+
 
90
  if run:
91
  parts.append(self.vocab.decode(run))
92
  run = []
configuration_bard.py CHANGED
@@ -2,9 +2,6 @@ from transformers import PretrainedConfig
2
 
3
 
4
  class BardConfig(PretrainedConfig):
5
- """Architecture of bard-small: a 36-layer, 512-wide decoder (parallel attention/FFN blocks, sliding-window attention
6
- with a global layer every fourth, RoPE on the windowed layers, QK-norm, gated attention, SwiGLU, logit softcap) with
7
- a dedicated number encoder/decoder."""
8
 
9
  model_type = "bard"
10
 
@@ -31,5 +28,5 @@ class BardConfig(PretrainedConfig):
31
  self.rope = rope
32
  self.parallel = parallel
33
  self.context = context
34
- self.vars_weight = vars_weight # the weight state changes had in training: the story engine undoes it
35
  super().__init__(**kwargs)
 
2
 
3
 
4
  class BardConfig(PretrainedConfig):
 
 
 
5
 
6
  model_type = "bard"
7
 
 
28
  self.rope = rope
29
  self.parallel = parallel
30
  self.context = context
31
+ self.vars_weight = vars_weight
32
  super().__init__(**kwargs)
modeling_bard.py CHANGED
@@ -32,13 +32,10 @@ class BardForStory(PreTrainedModel):
32
  self.post_init()
33
 
34
  def _init_weights(self, module):
35
- pass # the weights are loaded; the layers were initialised as they were trained
36
-
37
- # ------------------------------------------------------------------------- the tokens
38
 
39
  @property
40
  def tokenizer(self) -> BardTokenizer:
41
- """The tokenizer (tokenmonster plus the story format's special tokens), loaded from the repo's own files."""
42
  if self._tokenizer is None:
43
  where = getattr(self.config, "_name_or_path", "") or ""
44
  if os.path.isdir(where):
@@ -51,16 +48,12 @@ class BardForStory(PreTrainedModel):
51
  return self._tokenizer
52
 
53
  def forward(self, input_ids, number_values=None, **kwargs):
54
- """Logits for every position. `number_values` holds, at each <|num|>/<|snum|> token, the packed number (see
55
- BardTokenizer.encode); zeros elsewhere."""
56
  if number_values is None:
57
  number_values = torch.zeros_like(input_ids)
58
- cache = KVCache(self.lm.body) # the cached path: ordinary attention with explicit masks, no flex_attention
59
  h = self.lm.hidden(input_ids, number_values, cache=cache)
60
  return CausalLMOutput(logits=softcap(torch.nn.functional.linear(h, self.lm.head.weight)))
61
 
62
- # ------------------------------------------------------------------------- the game
63
-
64
  @property
65
  def engine(self) -> StoryEngine:
66
  if self._engine is None:
 
32
  self.post_init()
33
 
34
  def _init_weights(self, module):
35
+ pass
 
 
36
 
37
  @property
38
  def tokenizer(self) -> BardTokenizer:
 
39
  if self._tokenizer is None:
40
  where = getattr(self.config, "_name_or_path", "") or ""
41
  if os.path.isdir(where):
 
48
  return self._tokenizer
49
 
50
  def forward(self, input_ids, number_values=None, **kwargs):
 
 
51
  if number_values is None:
52
  number_values = torch.zeros_like(input_ids)
53
+ cache = KVCache(self.lm.body)
54
  h = self.lm.hidden(input_ids, number_values, cache=cache)
55
  return CausalLMOutput(logits=softcap(torch.nn.functional.linear(h, self.lm.head.weight)))
56
 
 
 
57
  @property
58
  def engine(self) -> StoryEngine:
59
  if self._engine is None: