Upload Senryu-2.1-1B (EMA weights)
Browse files- README.md +4 -4
- tiny_gdn/repetition.py +460 -0
README.md
CHANGED
|
@@ -118,7 +118,7 @@ In Python, `tiny_gdn.senryu_chat.senryu_system_prompt()` returns it dated today.
|
|
| 118 |
|
| 119 |
## Default tools
|
| 120 |
|
| 121 |
-
Every conversation under the default prompt offered these three tools, in this order.
|
| 122 |
|
| 123 |
| Tool | Arguments | Result, as trained |
|
| 124 |
|------|-----------|--------------------|
|
|
@@ -197,7 +197,7 @@ python inference.py
|
|
| 197 |
|
| 198 |
```bash
|
| 199 |
python inference.py --prompt "Explain recursion in two sentences."
|
| 200 |
-
python inference.py --prompt "What
|
| 201 |
python inference.py --tools none --system ""
|
| 202 |
python inference.py --no-think --prompt "Reply in one sentence."
|
| 203 |
python inference.py --ngram-device cpu
|
|
@@ -285,8 +285,8 @@ Only assistant turns are trained. The released EMA weights reach a loss of 1.433
|
|
| 285 |
## Limitations
|
| 286 |
|
| 287 |
- **Scale.** 1.09B core parameters and no pretraining stage: expect thin world knowledge, factual errors and invented details.
|
| 288 |
-
- **
|
| 289 |
-
- **Search.**
|
| 290 |
- **Language.** Training data is mostly English.
|
| 291 |
- **Hardware.** A CUDA GPU is required.
|
| 292 |
|
|
|
|
| 118 |
|
| 119 |
## Default tools
|
| 120 |
|
| 121 |
+
Every conversation under the default prompt offered these three tools, in this order. Calls were rare: about 4% of those conversations call a tool at all.
|
| 122 |
|
| 123 |
| Tool | Arguments | Result, as trained |
|
| 124 |
|------|-----------|--------------------|
|
|
|
|
| 197 |
|
| 198 |
```bash
|
| 199 |
python inference.py --prompt "Explain recursion in two sentences."
|
| 200 |
+
python inference.py --prompt "What is 48213 * 9377?"
|
| 201 |
python inference.py --tools none --system ""
|
| 202 |
python inference.py --no-think --prompt "Reply in one sentence."
|
| 203 |
python inference.py --ngram-device cpu
|
|
|
|
| 285 |
## Limitations
|
| 286 |
|
| 287 |
- **Scale.** 1.09B core parameters and no pretraining stage: expect thin world knowledge, factual errors and invented details.
|
| 288 |
+
- **Tool calls.** The model calls tools much less often than it should. It used the calculator reliably in our tests, but asked for this week's news, told outright to search the web, or given a link to read, it answered from memory and invented the details in 9 of 11 tries. Only 1.5% of its default-prompt training conversations search the web and 0.3% fetch a page. Paste a page's text in when it matters.
|
| 289 |
+
- **Search.** When it does search, it can misread or over-trust the results, and it sometimes repeats a search or lists results in a loop. Check anything that matters.
|
| 290 |
- **Language.** Training data is mostly English.
|
| 291 |
- **Hardware.** A CUDA GPU is required.
|
| 292 |
|
tiny_gdn/repetition.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Markdown-safe repetition control for TinyGDN sampling.
|
| 2 |
+
|
| 3 |
+
Unigram presence over a window hammers newlines, list markers, heading
|
| 4 |
+
hashes, table pipes, and backticks. This module:
|
| 5 |
+
|
| 6 |
+
- applies the usual logit penalty only to **content** tokens that already
|
| 7 |
+
appeared at least twice in the window
|
| 8 |
+
- bans the next token when it would complete a repeated n-gram **and**
|
| 9 |
+
that next token is content, so ``##``, ``- ``, ``|``, and ````` can
|
| 10 |
+
still repeat
|
| 11 |
+
- applies DRY ("don't repeat yourself"): a content token that would extend
|
| 12 |
+
a verbatim copy of an earlier stretch of the same segment loses
|
| 13 |
+
``multiplier * base ** (match - allowed_length)`` logits, so loops break
|
| 14 |
+
within a few tokens while ordinary word reuse goes untouched
|
| 15 |
+
|
| 16 |
+
DRY works per segment. Special tokens (turn markers, ``<think>``,
|
| 17 |
+
``</think>``) and the end of a tool call start a new one, so an answer may
|
| 18 |
+
copy its draft from the reasoning. Copies are looked for in the current
|
| 19 |
+
segment and in earlier assistant turns, never in system, user or tool text,
|
| 20 |
+
so a reply may quote the prompt but not re-paste an earlier reply. Inside a
|
| 21 |
+
code fence or a tool call DRY is off, and a digit is only penalized once its
|
| 22 |
+
copy runs :data:`DRY_DIGIT_SLACK` tokens longer than other tokens need, so
|
| 23 |
+
code, tool arguments and restated results come out as the model means them
|
| 24 |
+
while a loop of numbers still breaks.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
from collections import Counter
|
| 30 |
+
from collections.abc import Sequence
|
| 31 |
+
from dataclasses import dataclass
|
| 32 |
+
|
| 33 |
+
import torch
|
| 34 |
+
from tokenizers import Tokenizer
|
| 35 |
+
|
| 36 |
+
DEFAULT_REPETITION_NGRAM_SIZE = 0
|
| 37 |
+
MIN_CONTENT_UNIGRAM_COUNT = 2
|
| 38 |
+
|
| 39 |
+
DRY_SEQUENCE_BREAKERS = ("\n", ":", '"', "*")
|
| 40 |
+
DRY_MAX_MATCH = 64
|
| 41 |
+
DRY_MAX_EXPONENT = 64.0
|
| 42 |
+
# A copy found in an earlier turn counts this many tokens shorter, so a reply
|
| 43 |
+
# may reuse a heading or a term from an earlier one but not re-paste it.
|
| 44 |
+
DRY_SOURCE_SLACK = 2
|
| 45 |
+
# A digit needs a copy this many tokens longer before it is penalized.
|
| 46 |
+
DRY_DIGIT_SLACK = 4
|
| 47 |
+
CODE_FENCE = "```"
|
| 48 |
+
# How this tokenizer family spells a tool-call tag: "<" or "</" (possibly
|
| 49 |
+
# merged into the token before), then these pieces.
|
| 50 |
+
TOOL_TAG_PIECES = ("tool", "_", "call", ">")
|
| 51 |
+
TOOL_TAG_OPEN = "<tool_call>"
|
| 52 |
+
TOOL_TAG_CLOSE = "</tool_call>"
|
| 53 |
+
TURN_START = "<|im_start|>"
|
| 54 |
+
TURN_END = "<|im_end|>"
|
| 55 |
+
ASSISTANT_HEADER = f"{TURN_START}assistant\n"
|
| 56 |
+
|
| 57 |
+
STRUCTURAL_CHARS = frozenset(
|
| 58 |
+
" \t\n\r\f\v"
|
| 59 |
+
"#*_`~[](){}|<>!.,;:?/+-=\\'\"“”‘’•–—…·"
|
| 60 |
+
)
|
| 61 |
+
SPECIAL_TOKEN_NAMES = frozenset(
|
| 62 |
+
{
|
| 63 |
+
"<think>",
|
| 64 |
+
"</think>",
|
| 65 |
+
"<tool_call>",
|
| 66 |
+
"</tool_call>",
|
| 67 |
+
"<tool_response>",
|
| 68 |
+
"</tool_response>",
|
| 69 |
+
"<tools>",
|
| 70 |
+
"</tools>",
|
| 71 |
+
"<code>",
|
| 72 |
+
"</code>",
|
| 73 |
+
}
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def token_text_is_structural(decoded: str, piece: str | None = None) -> bool:
|
| 78 |
+
"""True for whitespace, markdown/punctuation, and chat specials."""
|
| 79 |
+
|
| 80 |
+
raw = piece or ""
|
| 81 |
+
if raw.startswith("<|") or raw in SPECIAL_TOKEN_NAMES:
|
| 82 |
+
return True
|
| 83 |
+
if decoded.startswith("<|") or decoded in SPECIAL_TOKEN_NAMES:
|
| 84 |
+
return True
|
| 85 |
+
if decoded.strip() == "":
|
| 86 |
+
return True
|
| 87 |
+
body = decoded.strip()
|
| 88 |
+
if all(character in STRUCTURAL_CHARS for character in body):
|
| 89 |
+
return True
|
| 90 |
+
if len(body) <= 4 and body[:-1].isdecimal() and body[-1:] in ".)":
|
| 91 |
+
return True
|
| 92 |
+
return False
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
_FLAGS: dict[int, tuple[bool, ...]] = {}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def structural_token_flags(tokenizer: Tokenizer) -> tuple[bool, ...]:
|
| 99 |
+
"""Per-id flags. Unused ids are treated as structural (never penalized)."""
|
| 100 |
+
|
| 101 |
+
flags: list[bool] = []
|
| 102 |
+
vocab_size = tokenizer.get_vocab_size()
|
| 103 |
+
for token_id in range(vocab_size):
|
| 104 |
+
piece = tokenizer.id_to_token(token_id)
|
| 105 |
+
if piece is None:
|
| 106 |
+
flags.append(True)
|
| 107 |
+
continue
|
| 108 |
+
decoded = tokenizer.decode([token_id], skip_special_tokens=False)
|
| 109 |
+
flags.append(token_text_is_structural(decoded, piece))
|
| 110 |
+
return tuple(flags)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def cached_structural_token_flags(tokenizer: Tokenizer) -> tuple[bool, ...]:
|
| 114 |
+
key = id(tokenizer)
|
| 115 |
+
flags = _FLAGS.get(key)
|
| 116 |
+
if flags is None:
|
| 117 |
+
flags = structural_token_flags(tokenizer)
|
| 118 |
+
_FLAGS[key] = flags
|
| 119 |
+
return flags
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def content_token_mask(
|
| 123 |
+
tokenizer: Tokenizer, vocab_size: int, device: torch.device | str
|
| 124 |
+
) -> torch.Tensor:
|
| 125 |
+
"""``[vocab_size]`` bools, True for the content tokens the penalty may touch.
|
| 126 |
+
|
| 127 |
+
Model ids past the tokenizer's vocabulary count as structural.
|
| 128 |
+
"""
|
| 129 |
+
flags = cached_structural_token_flags(tokenizer)
|
| 130 |
+
known = min(len(flags), vocab_size)
|
| 131 |
+
content = torch.zeros(vocab_size, dtype=torch.bool, device=device)
|
| 132 |
+
content[:known] = ~torch.tensor(flags[:known], dtype=torch.bool)
|
| 133 |
+
return content
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _as_id_list(token_ids: Sequence[int] | torch.Tensor, window: int) -> list[int]:
|
| 137 |
+
if isinstance(token_ids, torch.Tensor):
|
| 138 |
+
values = token_ids.detach().tolist()
|
| 139 |
+
else:
|
| 140 |
+
values = list(token_ids)
|
| 141 |
+
if window > 0:
|
| 142 |
+
return values[-window:]
|
| 143 |
+
return values
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def content_ngram_banned(
|
| 147 |
+
recent: Sequence[int],
|
| 148 |
+
ngram_size: int,
|
| 149 |
+
structural: Sequence[bool],
|
| 150 |
+
) -> set[int]:
|
| 151 |
+
"""Next-token ids that would finish a previously seen content n-gram."""
|
| 152 |
+
|
| 153 |
+
if ngram_size < 2 or len(recent) < ngram_size:
|
| 154 |
+
return set()
|
| 155 |
+
vocab = len(structural)
|
| 156 |
+
seen: dict[tuple[int, ...], set[int]] = {}
|
| 157 |
+
prefix_len = ngram_size - 1
|
| 158 |
+
for index in range(len(recent) - ngram_size + 1):
|
| 159 |
+
nxt = recent[index + prefix_len]
|
| 160 |
+
if nxt < 0 or nxt >= vocab or structural[nxt]:
|
| 161 |
+
continue
|
| 162 |
+
prefix = tuple(recent[index : index + prefix_len])
|
| 163 |
+
seen.setdefault(prefix, set()).add(nxt)
|
| 164 |
+
current = tuple(recent[-prefix_len:])
|
| 165 |
+
return set(seen.get(current, ()))
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def apply_repetition_penalty(
|
| 169 |
+
logits: torch.Tensor,
|
| 170 |
+
token_ids: Sequence[int] | torch.Tensor,
|
| 171 |
+
*,
|
| 172 |
+
penalty: float,
|
| 173 |
+
window: int,
|
| 174 |
+
ngram_size: int = DEFAULT_REPETITION_NGRAM_SIZE,
|
| 175 |
+
structural: Sequence[bool],
|
| 176 |
+
min_unigram_count: int = MIN_CONTENT_UNIGRAM_COUNT,
|
| 177 |
+
) -> torch.Tensor:
|
| 178 |
+
"""In-place HF-style penalty on content unigrams, plus content n-gram bans."""
|
| 179 |
+
|
| 180 |
+
if penalty == 1.0:
|
| 181 |
+
return logits
|
| 182 |
+
recent = _as_id_list(token_ids, window)
|
| 183 |
+
if not recent:
|
| 184 |
+
return logits
|
| 185 |
+
vocab = len(structural)
|
| 186 |
+
|
| 187 |
+
if penalty != 1.0:
|
| 188 |
+
penalize = [
|
| 189 |
+
token_id
|
| 190 |
+
for token_id, count in Counter(recent).items()
|
| 191 |
+
if count >= min_unigram_count
|
| 192 |
+
and 0 <= token_id < vocab
|
| 193 |
+
and not structural[token_id]
|
| 194 |
+
]
|
| 195 |
+
if penalize:
|
| 196 |
+
index = torch.tensor(penalize, dtype=torch.long, device=logits.device)
|
| 197 |
+
selected = logits[index]
|
| 198 |
+
logits.index_copy_(
|
| 199 |
+
0,
|
| 200 |
+
index,
|
| 201 |
+
torch.where(selected < 0, selected * penalty, selected / penalty),
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
if ngram_size >= 2:
|
| 205 |
+
banned = content_ngram_banned(recent, ngram_size, structural)
|
| 206 |
+
if banned:
|
| 207 |
+
index = torch.tensor(list(banned), dtype=torch.long, device=logits.device)
|
| 208 |
+
logits.index_fill_(0, index, float("-inf"))
|
| 209 |
+
return logits
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@dataclass(frozen=True)
|
| 213 |
+
class DryVocabulary:
|
| 214 |
+
"""Per-id flags DRY needs, for every id of the tokenizer.
|
| 215 |
+
|
| 216 |
+
``tag_pieces`` holds the ids of :data:`TOOL_TAG_PIECES` when the tokenizer
|
| 217 |
+
spells tool-call tags over several tokens, and is empty otherwise.
|
| 218 |
+
``assistant_header`` and ``turn_start``/``turn_end`` find earlier
|
| 219 |
+
assistant turns; without them only the current segment is searched.
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
breaker: tuple[bool, ...]
|
| 223 |
+
target: tuple[bool, ...]
|
| 224 |
+
digit: tuple[bool, ...]
|
| 225 |
+
boundary: tuple[bool, ...]
|
| 226 |
+
fence: tuple[bool, ...]
|
| 227 |
+
tool_open: tuple[bool, ...]
|
| 228 |
+
tool_close: tuple[bool, ...]
|
| 229 |
+
tag_open: tuple[bool, ...]
|
| 230 |
+
tag_close: tuple[bool, ...]
|
| 231 |
+
tag_pieces: tuple[int, ...]
|
| 232 |
+
assistant_header: tuple[int, ...] = ()
|
| 233 |
+
turn_start: int | None = None
|
| 234 |
+
turn_end: int | None = None
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
@dataclass(frozen=True)
|
| 238 |
+
class DryState:
|
| 239 |
+
"""Where a context's DRY segment starts, and whether DRY is off at its end."""
|
| 240 |
+
|
| 241 |
+
start: int
|
| 242 |
+
fence: bool
|
| 243 |
+
tool: bool
|
| 244 |
+
|
| 245 |
+
@property
|
| 246 |
+
def exempt(self) -> bool:
|
| 247 |
+
return self.fence or self.tool
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def build_dry_vocabulary(tokenizer: Tokenizer) -> DryVocabulary:
|
| 251 |
+
structural = cached_structural_token_flags(tokenizer)
|
| 252 |
+
columns: dict[str, list[bool]] = {
|
| 253 |
+
name: []
|
| 254 |
+
for name in (
|
| 255 |
+
"breaker",
|
| 256 |
+
"target",
|
| 257 |
+
"digit",
|
| 258 |
+
"boundary",
|
| 259 |
+
"fence",
|
| 260 |
+
"tool_open",
|
| 261 |
+
"tool_close",
|
| 262 |
+
"tag_open",
|
| 263 |
+
"tag_close",
|
| 264 |
+
)
|
| 265 |
+
}
|
| 266 |
+
for token_id in range(tokenizer.get_vocab_size()):
|
| 267 |
+
piece = tokenizer.id_to_token(token_id)
|
| 268 |
+
decoded = "" if piece is None else tokenizer.decode([token_id], skip_special_tokens=False)
|
| 269 |
+
boundary = piece is not None and (piece.startswith("<|") or piece in ("<think>", "</think>"))
|
| 270 |
+
breaker = piece is None or boundary or any(mark in decoded for mark in DRY_SEQUENCE_BREAKERS)
|
| 271 |
+
columns["boundary"].append(boundary)
|
| 272 |
+
columns["breaker"].append(breaker)
|
| 273 |
+
columns["target"].append(piece is not None and not structural[token_id] and not breaker)
|
| 274 |
+
columns["digit"].append(decoded.strip().isdigit())
|
| 275 |
+
columns["fence"].append(decoded.count(CODE_FENCE) % 2 == 1)
|
| 276 |
+
columns["tool_open"].append(piece == TOOL_TAG_OPEN)
|
| 277 |
+
columns["tool_close"].append(piece == TOOL_TAG_CLOSE)
|
| 278 |
+
columns["tag_close"].append(decoded.endswith("</"))
|
| 279 |
+
columns["tag_open"].append(decoded.endswith("<") and not decoded.endswith("</"))
|
| 280 |
+
pieces = [tokenizer.token_to_id(piece) for piece in TOOL_TAG_PIECES]
|
| 281 |
+
tag_pieces = (
|
| 282 |
+
()
|
| 283 |
+
if tokenizer.token_to_id(TOOL_TAG_OPEN) is not None or any(piece is None for piece in pieces)
|
| 284 |
+
else tuple(int(piece) for piece in pieces if piece is not None)
|
| 285 |
+
)
|
| 286 |
+
turn_start = tokenizer.token_to_id(TURN_START)
|
| 287 |
+
turn_end = tokenizer.token_to_id(TURN_END)
|
| 288 |
+
has_turns = turn_start is not None and turn_end is not None
|
| 289 |
+
return DryVocabulary(
|
| 290 |
+
**{name: tuple(values) for name, values in columns.items()},
|
| 291 |
+
tag_pieces=tag_pieces,
|
| 292 |
+
assistant_header=(
|
| 293 |
+
tuple(tokenizer.encode(ASSISTANT_HEADER, add_special_tokens=False).ids) if has_turns else ()
|
| 294 |
+
),
|
| 295 |
+
turn_start=turn_start if has_turns else None,
|
| 296 |
+
turn_end=turn_end if has_turns else None,
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
_DRY_VOCABULARIES: dict[int, DryVocabulary] = {}
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def cached_dry_vocabulary(tokenizer: Tokenizer) -> DryVocabulary:
|
| 304 |
+
key = id(tokenizer)
|
| 305 |
+
vocabulary = _DRY_VOCABULARIES.get(key)
|
| 306 |
+
if vocabulary is None:
|
| 307 |
+
vocabulary = build_dry_vocabulary(tokenizer)
|
| 308 |
+
_DRY_VOCABULARIES[key] = vocabulary
|
| 309 |
+
return vocabulary
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _flag(flags: tuple[bool, ...], token: int) -> bool:
|
| 313 |
+
return 0 <= token < len(flags) and flags[token]
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
def dry_tool_tag(tokens: Sequence[int], position: int, vocabulary: DryVocabulary) -> tuple[bool, bool]:
|
| 317 |
+
"""Whether the token at ``position`` completes an opening or a closing tool-call tag."""
|
| 318 |
+
|
| 319 |
+
token = tokens[position]
|
| 320 |
+
if _flag(vocabulary.tool_open, token) or _flag(vocabulary.tool_close, token):
|
| 321 |
+
return _flag(vocabulary.tool_open, token), _flag(vocabulary.tool_close, token)
|
| 322 |
+
size = len(vocabulary.tag_pieces)
|
| 323 |
+
if size == 0 or position < size:
|
| 324 |
+
return False, False
|
| 325 |
+
if tuple(tokens[position - size + 1 : position + 1]) != vocabulary.tag_pieces:
|
| 326 |
+
return False, False
|
| 327 |
+
before = tokens[position - size]
|
| 328 |
+
return _flag(vocabulary.tag_open, before), _flag(vocabulary.tag_close, before)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def dry_state(tokens: Sequence[int], vocabulary: DryVocabulary) -> DryState:
|
| 332 |
+
"""The DRY segment state after ``tokens``, as the graph sampler tracks it token by token."""
|
| 333 |
+
|
| 334 |
+
begin = 0
|
| 335 |
+
for position in range(len(tokens) - 1, -1, -1):
|
| 336 |
+
if _flag(vocabulary.boundary, tokens[position]):
|
| 337 |
+
begin = position + 1
|
| 338 |
+
break
|
| 339 |
+
start, fence, tool = begin, False, False
|
| 340 |
+
for position in range(begin, len(tokens)):
|
| 341 |
+
opens, closes = dry_tool_tag(tokens, position, vocabulary)
|
| 342 |
+
fence ^= _flag(vocabulary.fence, tokens[position])
|
| 343 |
+
if opens:
|
| 344 |
+
tool = True
|
| 345 |
+
elif closes:
|
| 346 |
+
start, fence, tool = position + 1, False, False
|
| 347 |
+
return DryState(start=start, fence=fence, tool=tool)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def dry_sources(tokens: Sequence[int], vocabulary: DryVocabulary) -> list[tuple[int, int]]:
|
| 351 |
+
"""``[begin, end)`` spans of the assistant turns before the last one in ``tokens``."""
|
| 352 |
+
|
| 353 |
+
header = vocabulary.assistant_header
|
| 354 |
+
if not header or vocabulary.turn_start is None:
|
| 355 |
+
return []
|
| 356 |
+
current = next(
|
| 357 |
+
(position for position in range(len(tokens) - 1, -1, -1) if tokens[position] == vocabulary.turn_start),
|
| 358 |
+
None,
|
| 359 |
+
)
|
| 360 |
+
if current is None:
|
| 361 |
+
return []
|
| 362 |
+
spans: list[tuple[int, int]] = []
|
| 363 |
+
begin: int | None = None
|
| 364 |
+
for position in range(current):
|
| 365 |
+
token = tokens[position]
|
| 366 |
+
if begin is not None and (token == vocabulary.turn_end or token == vocabulary.turn_start):
|
| 367 |
+
spans.append((begin, position))
|
| 368 |
+
begin = None
|
| 369 |
+
if token == vocabulary.turn_start and tuple(tokens[position : position + len(header)]) == header:
|
| 370 |
+
begin = position + len(header)
|
| 371 |
+
if begin is not None and begin < current:
|
| 372 |
+
spans.append((begin, current))
|
| 373 |
+
return spans
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def dry_match_lengths(
|
| 377 |
+
tokens: Sequence[int],
|
| 378 |
+
*,
|
| 379 |
+
start: int,
|
| 380 |
+
breaker: tuple[bool, ...],
|
| 381 |
+
sources: Sequence[tuple[int, int]] = (),
|
| 382 |
+
lower: int = 0,
|
| 383 |
+
) -> dict[int, int]:
|
| 384 |
+
"""For each token that followed an earlier copy of the context's tail, the longest such copy.
|
| 385 |
+
|
| 386 |
+
The tail stays inside the segment that begins at ``start``; copies may lie
|
| 387 |
+
in that segment or, from ``lower`` on, in the ``sources`` spans, where
|
| 388 |
+
they count :data:`DRY_SOURCE_SLACK` tokens shorter.
|
| 389 |
+
"""
|
| 390 |
+
|
| 391 |
+
if len(tokens) - start < 1:
|
| 392 |
+
return {}
|
| 393 |
+
last = tokens[-1]
|
| 394 |
+
if _flag(breaker, last):
|
| 395 |
+
return {}
|
| 396 |
+
eligible = bytearray(len(tokens))
|
| 397 |
+
for begin, end in sources:
|
| 398 |
+
eligible[max(begin, lower) : max(end, lower)] = b"\x01" * (max(end, lower) - max(begin, lower))
|
| 399 |
+
eligible[start:] = b"\x01" * (len(tokens) - start)
|
| 400 |
+
lengths: dict[int, int] = {}
|
| 401 |
+
for index in range(len(tokens) - 1):
|
| 402 |
+
if not eligible[index] or tokens[index] != last:
|
| 403 |
+
continue
|
| 404 |
+
match = 1
|
| 405 |
+
while match < DRY_MAX_MATCH and index - match >= 0 and eligible[index - match] and len(tokens) - 1 - match >= start:
|
| 406 |
+
recent = tokens[-1 - match]
|
| 407 |
+
if tokens[index - match] != recent or _flag(breaker, recent):
|
| 408 |
+
break
|
| 409 |
+
match += 1
|
| 410 |
+
if index < start:
|
| 411 |
+
match -= DRY_SOURCE_SLACK
|
| 412 |
+
if match < 1:
|
| 413 |
+
continue
|
| 414 |
+
following = tokens[index + 1]
|
| 415 |
+
lengths[following] = max(match, lengths.get(following, 0))
|
| 416 |
+
return lengths
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def apply_dry_penalty(
|
| 420 |
+
logits: torch.Tensor,
|
| 421 |
+
tokens: Sequence[int],
|
| 422 |
+
*,
|
| 423 |
+
multiplier: float,
|
| 424 |
+
base: float,
|
| 425 |
+
allowed_length: int,
|
| 426 |
+
window: int,
|
| 427 |
+
vocabulary: DryVocabulary,
|
| 428 |
+
) -> torch.Tensor:
|
| 429 |
+
"""In-place DRY penalty on ``[vocab]`` logits for the context ``tokens``."""
|
| 430 |
+
|
| 431 |
+
if multiplier == 0:
|
| 432 |
+
return logits
|
| 433 |
+
tokens = list(tokens)
|
| 434 |
+
state = dry_state(tokens, vocabulary)
|
| 435 |
+
if state.exempt:
|
| 436 |
+
return logits
|
| 437 |
+
lower = 0 if window <= 0 else max(len(tokens) - window, 0)
|
| 438 |
+
lengths = dry_match_lengths(
|
| 439 |
+
tokens,
|
| 440 |
+
start=max(state.start, lower),
|
| 441 |
+
breaker=vocabulary.breaker,
|
| 442 |
+
sources=dry_sources(tokens, vocabulary),
|
| 443 |
+
lower=lower,
|
| 444 |
+
)
|
| 445 |
+
vocab = logits.shape[-1]
|
| 446 |
+
effective = {
|
| 447 |
+
token: match - (DRY_DIGIT_SLACK if _flag(vocabulary.digit, token) else 0)
|
| 448 |
+
for token, match in lengths.items()
|
| 449 |
+
if token < vocab and _flag(vocabulary.target, token)
|
| 450 |
+
}
|
| 451 |
+
penalized = {
|
| 452 |
+
token: multiplier * base ** min(match - allowed_length, DRY_MAX_EXPONENT)
|
| 453 |
+
for token, match in effective.items()
|
| 454 |
+
if match >= allowed_length
|
| 455 |
+
}
|
| 456 |
+
if penalized:
|
| 457 |
+
index = torch.tensor(list(penalized), dtype=torch.long, device=logits.device)
|
| 458 |
+
amount = torch.tensor(list(penalized.values()), dtype=logits.dtype, device=logits.device)
|
| 459 |
+
logits.index_add_(0, index, -amount)
|
| 460 |
+
return logits
|