textslinger 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Keith Vertanen, Dylan Gaines
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: textslinger
3
+ Version: 0.1.0
4
+ Summary: TextSlinger text prediction toolkit.
5
+ Author-email: Dylan Gaines <dcgaines@mtu.edu>, Keith Vertanen <vertanen@mtu.edu>, CAMBI <cambi_support@googlegroups.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Keith Vertanen, Dylan Gaines
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://www.cambi.tech/
28
+ Project-URL: Source, https://github.com/kdv123/textpredict
29
+ Platform: Linux
30
+ Platform: Windows
31
+ Platform: Mac OS-X
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
34
+ Classifier: Natural Language :: English
35
+ Classifier: Intended Audience :: Science/Research
36
+ Classifier: Intended Audience :: End Users/Desktop
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: Programming Language :: Python
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Requires-Python: <3.11,>=3.9
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE.md
44
+ Requires-Dist: torch==2.2.0
45
+ Requires-Dist: torchvision==0.17.0
46
+ Requires-Dist: torchaudio==2.2.0
47
+ Requires-Dist: datasets==2.0.0
48
+ Requires-Dist: bitsandbytes==0.42.0
49
+ Requires-Dist: requests==2.32.3
50
+ Requires-Dist: kenlm==0.1
51
+ Requires-Dist: nlpaug==1.1.11
52
+ Requires-Dist: psutil==5.7.2
53
+ Requires-Dist: ipywidgets==8.1.3
54
+ Requires-Dist: sentencepiece==0.2.0
55
+ Requires-Dist: protobuf==4.25.3
56
+ Requires-Dist: evaluate==0.4.0
57
+ Requires-Dist: scikit-learn==1.2.2
58
+ Requires-Dist: accelerate==0.33.0
59
+ Requires-Dist: transformers==4.47.0
60
+ Requires-Dist: numpy==1.24.4
61
+ Requires-Dist: tqdm==4.62.2
62
+ Provides-Extra: release
63
+ Requires-Dist: twine==5.0.0; extra == "release"
64
+ Requires-Dist: build==1.2.2.post1; extra == "release"
65
+ Requires-Dist: wheel==0.43.0; extra == "release"
66
+ Dynamic: license-file
67
+
68
+ Python library for making text predictions using a language model.
69
+
70
+ **** Setting up a Python environment ****
71
+ These instructions are the same as those in the classify-aac which actually does training of models.
72
+ But the same environment should also work for evaluation using the models (though you may only need a subset of the packages).
73
+ This was tested on cheetah on 9/14/24.
74
+
75
+ If you don't have anaconda installed in your user account you'll first need to do that.
76
+ See: https://docs.anaconda.com/anaconda/install/linux/
77
+
78
+ % conda create -n aacllm python=3.10
79
+ % conda activate aacllm
80
+ % conda install pytorch torchvision torchaudio pytorch-cuda cuda mpi4py -c pytorch -c nvidia
81
+ % pip install 'git+https://github.com/potamides/uniformers.git#egg=uniformers'
82
+ % pip install --upgrade transformers
83
+ % pip install kenlm==0.1 --global-option="--max_order=12"
84
+ % pip install rbloom bitsandbytes requests nlpaug ipywidgets psutil datasets sentencepiece protobuf evaluate scikit-learn deepspeed accelerate
@@ -0,0 +1,17 @@
1
+ Python library for making text predictions using a language model.
2
+
3
+ **** Setting up a Python environment ****
4
+ These instructions are the same as those in the classify-aac which actually does training of models.
5
+ But the same environment should also work for evaluation using the models (though you may only need a subset of the packages).
6
+ This was tested on cheetah on 9/14/24.
7
+
8
+ If you don't have anaconda installed in your user account you'll first need to do that.
9
+ See: https://docs.anaconda.com/anaconda/install/linux/
10
+
11
+ % conda create -n aacllm python=3.10
12
+ % conda activate aacllm
13
+ % conda install pytorch torchvision torchaudio pytorch-cuda cuda mpi4py -c pytorch -c nvidia
14
+ % pip install 'git+https://github.com/potamides/uniformers.git#egg=uniformers'
15
+ % pip install --upgrade transformers
16
+ % pip install kenlm==0.1 --global-option="--max_order=12"
17
+ % pip install rbloom bitsandbytes requests nlpaug ipywidgets psutil datasets sentencepiece protobuf evaluate scikit-learn deepspeed accelerate
@@ -0,0 +1,73 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools>=61.0", "wheel>=0.37.1"]
4
+
5
+ [project]
6
+ name = "textslinger"
7
+ version = "0.1.0"
8
+ authors = [
9
+ {name="Dylan Gaines", email="dcgaines@mtu.edu"},
10
+ {name="Keith Vertanen", email="vertanen@mtu.edu"},
11
+ {name="CAMBI", email="cambi_support@googlegroups.com"},
12
+ ]
13
+ description = "TextSlinger text prediction toolkit."
14
+ readme = "README.md"
15
+ requires-python = ">=3.9,<3.11"
16
+ classifiers = [
17
+ 'License :: OSI Approved :: MIT License',
18
+ 'Topic :: Scientific/Engineering :: Human Machine Interfaces',
19
+ 'Natural Language :: English',
20
+ 'Intended Audience :: Science/Research',
21
+ 'Intended Audience :: End Users/Desktop',
22
+ 'Intended Audience :: Developers',
23
+ 'Programming Language :: Python',
24
+ 'Programming Language :: Python :: 3.9',
25
+ 'Programming Language :: Python :: 3.10',
26
+ ]
27
+
28
+ dependencies = [
29
+ "torch==2.2.0",
30
+ "torchvision==0.17.0",
31
+ "torchaudio==2.2.0",
32
+ "datasets==2.0.0",
33
+ "bitsandbytes==0.42.0",
34
+ "requests==2.32.3",
35
+ "kenlm==0.1",
36
+ "nlpaug==1.1.11",
37
+ "psutil==5.7.2",
38
+ "ipywidgets==8.1.3",
39
+ "sentencepiece==0.2.0",
40
+ "protobuf==4.25.3",
41
+ "evaluate==0.4.0",
42
+ "scikit-learn==1.2.2",
43
+ "accelerate==0.33.0",
44
+ "transformers==4.47.0",
45
+ "numpy==1.24.4",
46
+ "tqdm==4.62.2",
47
+ ]
48
+
49
+ [project.license]
50
+ file = "LICENSE.md"
51
+ license = "MIT"
52
+
53
+ [project.optional-dependencies]
54
+ release = [
55
+ "twine==5.0.0",
56
+ "build==1.2.2.post1",
57
+ "wheel==0.43.0",
58
+ ]
59
+
60
+ [project.urls]
61
+ "Homepage" = "https://www.cambi.tech/"
62
+ "Source" = "https://github.com/kdv123/textpredict"
63
+
64
+ [tool.setuptools]
65
+ # https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
66
+ platforms = ["Linux", "Windows", "Mac OS-X"]
67
+ include-package-data = true
68
+ zip-safe = true
69
+
70
+ # Python modules and packages that are included in the
71
+ # distribution package (and therefore become importable)
72
+ [tool.setuptools.packages.find]
73
+ exclude = ["examples", "scripts"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,437 @@
1
+ import torch
2
+ from typing import List, Tuple
3
+ from transformers import AutoModelForCausalLM, AutoTokenizer
4
+ import itertools
5
+ import heapq
6
+ from textslinger.language_model import LanguageModel, DEFAULT_SYMBOL_SET
7
+ from textslinger.exceptions import InvalidLanguageModelException
8
+ from scipy.special import logsumexp
9
+ from scipy.special import softmax
10
+ import time
11
+ from collections import defaultdict
12
+ from typing import Final
13
+
14
+
15
+ class CausalLanguageModel(LanguageModel):
16
+ """Character language model based on a pre-trained causal model, GPT-2 by default."""
17
+
18
+ def __init__(self,
19
+ symbol_set: List[str],
20
+ lang_model_name: str,
21
+ lm_path: str = None,
22
+ lm_device: str = "cpu",
23
+ lm_left_context: str = "",
24
+ beam_width: int = None,
25
+ fp16: bool = False,
26
+ mixed_case_context: bool = False,
27
+ case_simple: bool = False,
28
+ max_completed: int = None,
29
+ ):
30
+ """
31
+ Initialize instance variables and load the language model with given path
32
+ Args:
33
+ symbol_set - list of symbol strings
34
+ lang_model_name - name of the Hugging Face casual language model to load
35
+ lm_path - load fine-tuned model from specified directory
36
+ lm_device - device to use for making predictions (cpu, mps, or cuda)
37
+ lm_left_context - text to condition start of sentence on
38
+ beam_width - how many hypotheses to keep during the search, None=off
39
+ fp16 - convert model to fp16 to save memory/compute on CUDA
40
+ mixed_case_context - use mixed case for language model left context
41
+ case_simple - simple fixing of left context case
42
+ max_completed - stop search once we reach this many completed hypotheses, None=don't prune
43
+ """
44
+ super().__init__(symbol_set=symbol_set)
45
+ self.model = None
46
+ self.tokenizer = None
47
+ self.vocab_size = 0
48
+ self.valid_vocab = []
49
+ self.vocab = defaultdict(list)
50
+ # Since subword token ids are integers, use a list instead of a dictionary
51
+ self.index_to_word = []
52
+ self.index_to_word_lower = []
53
+ self.symbol_set_lower = None
54
+ self.device = lm_device
55
+ self.left_context = lm_left_context
56
+ self.fp16 = fp16
57
+ self.mixed_case_context = mixed_case_context
58
+ self.case_simple = case_simple
59
+ self.max_completed = max_completed
60
+
61
+ if not max_completed and not beam_width:
62
+ print(f"WARNING: using causal language model without any pruning, this can be slow!")
63
+ else:
64
+ print(f"Causal language model, beam_width {beam_width}, max_completed {max_completed}")
65
+
66
+ # We optionally load the model from a local directory, but if this is not
67
+ # specified, we load a Hugging Face model
68
+ self.model_name = lang_model_name
69
+ self.model_dir = lm_path if lm_path else self.model_name
70
+
71
+ # Parameters for the search
72
+ self.beam_width = beam_width
73
+
74
+ # Simple heuristic to correct case in the LM context
75
+ self.simple_upper_words = {"i": "I",
76
+ "i'll": "I'll",
77
+ "i've": "I've",
78
+ "i'd": "I'd",
79
+ "i'm": "I'm"}
80
+
81
+ # Track how much time spent in different parts of the predict function
82
+ self.predict_total_ns = 0
83
+ self.predict_inference_ns = 0
84
+
85
+
86
+ # Are we a model that automatically inserts a start token that we need to get rid of
87
+ self.drop_first_token = False
88
+
89
+ self.load()
90
+
91
+ def _build_vocab(self) -> None:
92
+ """
93
+ Build a vocabulary table mapping token index to word strings
94
+ """
95
+
96
+ # Loop over all the subword tokens in the LLM
97
+ for i in range(self.vocab_size):
98
+ # Create a map from the subword token integer ID to the mixed and lowercase string versions
99
+ word = self.tokenizer.decode([i])
100
+ word_lower = word.lower()
101
+ self.index_to_word += word,
102
+ self.index_to_word_lower += word_lower,
103
+
104
+ # Check if all the characters in the subword token are in our valid symbol set
105
+ valid = True
106
+ for ch in word_lower:
107
+ if ch == ' ':
108
+ continue
109
+ elif ch not in self.symbol_set_lower:
110
+ valid = False
111
+ break
112
+
113
+ # If the subword token symbols are all valid, then add it to the list of valid token IDs
114
+ if valid:
115
+ self.valid_vocab += i,
116
+ # Add this token ID to all lists for its valid text prefixes
117
+ for j in range(len(word)):
118
+ key = word_lower[0:j + 1]
119
+ self.vocab[key] += i,
120
+
121
+ # When done, self.vocab can be used to map to possible following subword tokens given some text, e.g.:
122
+ # self.vocab["cyclo"] = [47495, 49484]
123
+ # self.index_to_word[self.vocab["cyclo"][0]] = cyclop
124
+ # self.index_to_word[self.vocab["cyclo"][1]] = cyclopedia
125
+
126
+ (self.model_name.startswith("facebook/opt") or self.model_name.startswith("figmtu/opt") or "Llama-3.1" in self.model_name)
127
+
128
+ # Get the index we use for the start or end pseudo-word
129
+ if self.left_context == "":
130
+ if "gpt2" in self.model_name:
131
+ self.left_context = "<|endoftext|>"
132
+ elif "Llama-3.1" in self.model_name:
133
+ self.left_context = "<|begin_of_text|>"
134
+ # Seems to have both sentence start and end tokens: https://docs.mistral.ai/guides/tokenization/
135
+ elif "Mistral" in self.model_name:
136
+ self.left_context = "<s>"
137
+ else:
138
+ self.left_context = "</s>"
139
+
140
+ # OPT, Llama and Mistral all insert start token
141
+ self.drop_first_token = (self.model_name.startswith("facebook/opt") or
142
+ self.model_name.startswith("figmtu/opt") or
143
+ "Llama-3.1" in self.model_name or
144
+ "Mistral" in self.model_name)
145
+
146
+ # Get token id(s) for the left context we condition all sentences on
147
+ self.left_context_tokens = self._encode(self.left_context)
148
+ print(f"Causal: left_context = '{self.left_context}', left_context_tokens = {self.left_context_tokens}")
149
+
150
+ def _encode(self, text: str) -> List[int]:
151
+ tokens = self.tokenizer.encode(text)
152
+ # Both OPT and Llama automatically insert a start token which we want to control ourselves
153
+ if len(tokens) > 1 and self.drop_first_token:
154
+ tokens = tokens[1:]
155
+
156
+ return tokens
157
+
158
+ def _sequence_string(self, sequence: List[int]) -> str:
159
+ """
160
+ Convert a sequence of subword token IDs into a string with each token in ()'s
161
+ :param sequence: List of subword token IDs
162
+ :return: String
163
+ """
164
+ return "".join([f"({self.index_to_word[x]})" for x in sequence])
165
+
166
+ def get_all_tokens_text(self):
167
+ """
168
+ Return an array with the text of all subword tokens.
169
+ The array is in order by the integer index into the vocabulary.
170
+ This is mostly just for exploring the tokens in different LLMs.
171
+ :return: Array of subword token text strings.
172
+ """
173
+ result = []
174
+ for i in range(self.vocab_size):
175
+ result.append(self.tokenizer.decode([i]))
176
+ return result
177
+
178
+ def predict(self, evidence: List[str]) -> List[Tuple]:
179
+ """
180
+ Given an evidence of typed string, predict the probability distribution of
181
+ the next symbol
182
+ Args:
183
+ evidence - a list of characters (typed by the user)
184
+ Response:
185
+ A list of symbols with probability
186
+ """
187
+
188
+ assert self.model is not None, "language model does not exist!"
189
+ start_ns = time.time_ns()
190
+
191
+ context = "".join(evidence)
192
+
193
+ # If using the simple case feature, we need to go through the actual
194
+ # left context and capitalize the first letter in the sentence as
195
+ # well as any word in our list of words that should be capitalized.
196
+ if self.case_simple and len(context) > 0:
197
+ cased_context = ""
198
+ words = context.split()
199
+ for i, word in enumerate(words):
200
+ if i == 0 and word[0] >= 'a' and word[0] <= 'z':
201
+ word = word[0].upper() + word[1:]
202
+ if i > 0:
203
+ if word in self.simple_upper_words:
204
+ word = self.simple_upper_words[word]
205
+ cased_context += " "
206
+ cased_context += word
207
+ # Handle ending space in the context
208
+ if context[-1] == ' ':
209
+ cased_context += " "
210
+ context = cased_context
211
+
212
+ context_lower = context.lower()
213
+
214
+ # Index in the hypothesis string that is the next character after our context
215
+ target_pos = len(context_lower)
216
+
217
+ # For stats purposes track length of the prefix we are extending from space to match
218
+ # prefix_len = target_pos
219
+
220
+ # Look for the last space in the context, or -1 if no begin_text in context yet
221
+ pos = context_lower.rfind(" ")
222
+ tokens = []
223
+ tokens.extend(self.left_context_tokens)
224
+ if pos >= 0:
225
+ # Optionally, we condition on upper and lower case left context
226
+ if self.mixed_case_context:
227
+ truncated_context = context[0:pos]
228
+ else:
229
+ truncated_context = context_lower[0:pos]
230
+ tokens.extend(self._encode(truncated_context))
231
+ #prefix_len -= pos
232
+
233
+ #print(f"DEBUG, {context_lower} pos {pos}, prefix_len {prefix_len}")
234
+
235
+ # Constant indexes for use with the hypotheses tuples
236
+ LOGP: Final[int] = 0
237
+ SEQ: Final[int] = 1
238
+ LEN: Final[int] = 2
239
+
240
+ # Our starting hypothesis that we'll be extending.
241
+ # Format is (log likelihood, token id sequence, text length).
242
+ # Note: we only include tokens after any in left context.
243
+ start_length = 0
244
+ for x in tokens[len(self.left_context_tokens):]:
245
+ start_length += len(self.index_to_word_lower[x])
246
+ current_hypos = [(0.0, tokens, start_length)]
247
+
248
+ # We use a priority queue to track the top hypotheses during the beam search.
249
+ # For a beam of 8, empirical testing showed this was about the same amount
250
+ # of time as a simpler list that used a linear search to replace when full.
251
+ heapq.heapify(current_hypos)
252
+
253
+ # Create a hash mapping each valid following character to a list of log probabilities
254
+ char_to_log_probs = defaultdict(list)
255
+
256
+ # Add new extended hypotheses to this heap
257
+ next_hypos = []
258
+
259
+ # Tracks count of completed hypotheses
260
+ completed = 0
261
+
262
+ # Used to signal to while loop to stop the search
263
+ done = False
264
+
265
+ # Start a beam search forward from the backed off token sequence.
266
+ # Each iteration of this while loop extends hypotheses by all valid tokens.
267
+ # We only keep at most self.beam_width hypotheses in the valid heap.
268
+ # Stop extending search once we reach our max completed target.
269
+ while len(current_hypos) > 0 and not done:
270
+ # We'll explore hypothesis in order from most probable to least.
271
+ # This has little impact on how long it takes since this is only sorting a small number of things.
272
+ # But it is important with max_completed pruning since we want to bias for completing high probability things.
273
+ current_hypos.sort(reverse=True)
274
+
275
+ # Work on the hypotheses from the last round of extension.
276
+ # Create the torch tensor for the inference with a row for each hypothesis.
277
+ tokens_tensor = torch.tensor([x[SEQ] for x in current_hypos]).reshape(len(current_hypos), -1).to(self.device)
278
+
279
+ before_inference_ns = time.time_ns()
280
+ # Ask the LLM to predict tokens that come after our current set of hypotheses
281
+ with torch.no_grad():
282
+ # Compute the probabilities from the logits
283
+ log_probs = torch.log_softmax(self.model(tokens_tensor).logits[:, -1, :], dim=1)
284
+
285
+ # Create a big 2D tensor where each row is that hypothesis' current likelihood.
286
+ # First create a list of just the hypotheses' likelihoods.
287
+ # Then reshape to be a column vector.
288
+ # Then duplicate the column based on the number of subword tokens in the LLM.
289
+ add_tensor = torch.tensor([x[LOGP] for x in current_hypos]).reshape((log_probs.size()[0], 1)).repeat(1, log_probs.size()[1]).to(self.device)
290
+
291
+ # Add the current likelihoods with each subtoken's probability.
292
+ # Move it back to the CPU and convert to numpy since this makes it a lot faster to access for some reason.
293
+ new_log_probs = torch.add(log_probs, add_tensor).detach().cpu().numpy()
294
+ self.predict_inference_ns += time.time_ns() - before_inference_ns
295
+
296
+ for current_index, current in enumerate(current_hypos):
297
+ vocab = []
298
+ extra_vocab = []
299
+ # Extending this hypothesis must match the remaining text
300
+ remaining_context = context_lower[current[LEN]:]
301
+ if len(remaining_context) == 0:
302
+ # There is no remaining context thus all subword tokens that are valid under our symbol set
303
+ # should be considered when computing the probability of the next character.
304
+ vocab = self.valid_vocab
305
+ else:
306
+ if remaining_context in self.vocab:
307
+ # We have a list of subword tokens that match the remaining text.
308
+ # They could be the same length as the remaining text or longer and have the remaining text as a prefix.
309
+ vocab = self.vocab[remaining_context]
310
+
311
+ # We may need to use a subword token that doesn't completely consume the remaining text.
312
+ # Find these by tokenizing all possible lengths of text starting from the current position.
313
+ for i in range(1, len(remaining_context)):
314
+ tokenization = self._encode(context_lower[current[LEN]:current[LEN] + i])
315
+ # Ignore tokenizations involving multiple tokens since they involve an ID we would have already added.
316
+ if len(tokenization) == 1:
317
+ extra_vocab += tokenization[0],
318
+
319
+ # The below code takes the most time, results from pprofile on 5 phrases on an 2080 GPU:
320
+ # 299| 22484582| 89.5763| 3.9839e-06| 14.24%| for token_id in itertools.chain(vocab, extra_vocab):
321
+ # 300| 0| 0| 0| 0.00%| # For a hypothesis to finish it must extend beyond the existing typed context
322
+ # 301| 22483271| 93.7939| 4.17172e-06| 14.91%| subword_len = len(self.index_to_word_lower[token_id])
323
+ # 302| 22483271| 92.8608| 4.13022e-06| 14.76%| if (current[LEN] + subword_len) > len(context):
324
+ # 303| 0| 0| 0| 0.00%| # Add this likelihood to the list for the character at the prediction position.
325
+ # 304| 0| 0| 0| 0.00%| # Tracking the list and doing logsumpexp later was faster than doing it for each add.
326
+ # 305| 22480431| 106.353| 4.73094e-06| 16.90%| char_to_log_probs[self.index_to_word_lower[token_id][target_pos - current[LEN]]] += new_log_probs[current_index][token_id],
327
+ # 306| 22480431| 92.689| 4.1231e-06| 14.73%| completed += 1
328
+ # 307| 2840| 0.0124488| 4.38338e-06| 0.00%| elif not self.beam_width or len(next_hypos) <
329
+ #
330
+ # Tuning notes:
331
+ # - With a beam of 8 and max completed of 32,000, getting around 5x speedup on written dev set.
332
+ # - This results in a PPL increase of 0.0025 versus old results using only beam of >= 8.
333
+ # - Pruning based on log probability difference and based on minimum number of hypotheses per symbol in alphabet did worse.
334
+ # - Code for these other pruning methods was removed.
335
+ # Possible ways to make it faster:
336
+ # - Stop part way through the below for loop over (vocab, extra_vocab). But this seems weird since the token IDs are in
337
+ # no particular order, we'd be just stopping early on the last hypothesis being explored by the enclosing loop.
338
+ # - Sort the rows in the log prob results on the GPU. Use these to limit which token IDs we explore in the below
339
+ # for loop. Is it possible to do this without introducing too much extra work to limit to the high probability ones?
340
+
341
+ # Create a list of token indexes that are a prefix of the target text.
342
+ # We go over all the integer IDs in the vocab and extra_vocab lists.
343
+ for token_id in itertools.chain(vocab, extra_vocab):
344
+ # For a hypothesis to finish it must extend beyond the existing typed context
345
+ subword_len = len(self.index_to_word_lower[token_id])
346
+ if (current[LEN] + subword_len) > len(context):
347
+ # Add this likelihood to the list for the character at the prediction position.
348
+ # Tracking the list and doing logsumpexp later was faster than doing it for each add.
349
+ char_to_log_probs[self.index_to_word_lower[token_id][target_pos - current[LEN]]] += new_log_probs[current_index][token_id],
350
+ completed += 1
351
+ elif not self.beam_width or len(next_hypos) < self.beam_width:
352
+ # If we are under the beam limit then just add it
353
+ heapq.heappush(next_hypos,
354
+ (new_log_probs[current_index][token_id],
355
+ current[SEQ] + [token_id],
356
+ current[LEN] + subword_len))
357
+ elif new_log_probs[current_index][token_id] > next_hypos[0][LOGP]:
358
+ # Or replace the worst hypotheses with the new one
359
+ heapq.heappushpop(next_hypos,
360
+ (new_log_probs[current_index][token_id],
361
+ current[SEQ] + [token_id],
362
+ current[LEN] + subword_len))
363
+
364
+ # Break out of the for loop over hypotheses and while loop if we reach our max completed goal
365
+ if self.max_completed and completed >= self.max_completed:
366
+ done = True
367
+ break
368
+
369
+ # Swap in the extended set as the new current working set
370
+ current_hypos = next_hypos
371
+ next_hypos = []
372
+
373
+ # Parallel array to symbol_set for storing the marginals
374
+ char_probs = []
375
+ for ch in self.symbol_set_lower:
376
+ # Handle cases when symbols are never seen
377
+ if ch in char_to_log_probs:
378
+ char_probs += logsumexp(char_to_log_probs[ch]),
379
+ else:
380
+ char_probs += float("-inf"),
381
+
382
+ # Normalize to a distribution that sums to 1
383
+ char_probs = softmax(char_probs)
384
+
385
+ next_char_pred = {}
386
+ for i, ch in enumerate(self.symbol_set_lower):
387
+ # This works even if ch is a space
388
+ next_char_pred[ch.upper()] = char_probs[i]
389
+
390
+ end_ns = time.time_ns()
391
+ self.predict_total_ns += end_ns - start_ns
392
+
393
+ return list(sorted(next_char_pred.items(), key=lambda item: item[1], reverse=True))
394
+
395
+ def dump_predict_times(self) -> None:
396
+ """Print some stats about the prediction timing"""
397
+ if self.predict_total_ns > 0:
398
+ print(f"Predict %: "
399
+ f"inference {self.predict_inference_ns / self.predict_total_ns * 100.0:.3f}")
400
+
401
+ def load(self) -> None:
402
+ """
403
+ Load the language model and tokenizer, initialize class variables
404
+ """
405
+ try:
406
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_name, use_fast=False)
407
+ except BaseException:
408
+ raise InvalidLanguageModelException(f"{self.model_name} is not a valid model identifier on HuggingFace.")
409
+ self.vocab_size = self.tokenizer.vocab_size
410
+ try:
411
+ self.model = AutoModelForCausalLM.from_pretrained(self.model_dir)
412
+ if self.fp16 and self.device == "cuda":
413
+ self.model = self.model.half()
414
+ except:
415
+ raise InvalidLanguageModelException(f"{self.model_dir} is not a valid local folder or model identifier on HuggingFace.")
416
+
417
+ self.model.eval()
418
+
419
+ self.model.to(self.device)
420
+
421
+ self.symbol_set_lower = []
422
+
423
+ for ch in self.symbol_set:
424
+ # This works even if ch is a space
425
+ self.symbol_set_lower.append(ch.lower())
426
+
427
+ self._build_vocab()
428
+
429
+ def get_num_parameters(self) -> int:
430
+ """
431
+ Find out how many parameters the loaded model has
432
+ Args:
433
+ Response:
434
+ Integer number of parameters in the transformer model
435
+ """
436
+ return sum(p.numel() for p in self.model.parameters())
437
+