kerzgrr commited on
Commit
fa7a040
·
verified ·
1 Parent(s): 08b963f

Upload Senryu-2.1-1B (EMA weights)

Browse files
Files changed (2) hide show
  1. README.md +4 -4
  2. 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. The model called them sparingly: about 4% of those conversations call a tool at all.
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's in the news about fusion power this week?"
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
- - **Page fetches.** Only 0.3% of the default-prompt training conversations call `fetch-page`, and in our tests the model answered questions about a linked page from memory, inventing its contents, instead of fetching it. Paste the text in when a page matters.
289
- - **Search.** Results depend on the backend, and the model can misread or over-trust what it retrieves. Check anything that matters.
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