psaiops 0.4.7__py3-none-any.whl

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.

Potentially problematic release.


This version of psaiops might be problematic. Click here for more details.

@@ -0,0 +1,303 @@
1
+ import functools
2
+
3
+ import gradio
4
+ import torch
5
+ import torch.cuda
6
+
7
+ import psaiops.common.model
8
+ import psaiops.common.tokenizer
9
+ import psaiops.score.attention.lib
10
+
11
+ # META #########################################################################
12
+
13
+ STYLE = '''.white-text span { color: white; }'''
14
+ TITLE = '''Attention Scoring'''
15
+ INTRO = '''Score each token according to the weights of the attention layers.\nUnder construction, only "openai/gpt-oss-20b" is available for now.'''
16
+
17
+ MODEL = 'openai/gpt-oss-20b'
18
+
19
+ # COLORS #######################################################################
20
+
21
+ def create_color_map() -> dict:
22
+ return {
23
+ '-1': '#004444',
24
+ **{str(__i): '#{:02x}0000'.format(int(2.55 * __i)) for __i in range(101)}}
25
+
26
+ # INTRO ########################################################################
27
+
28
+ def create_intro_block(intro: str) -> dict:
29
+ __intro = gradio.Markdown(intro, line_breaks=True)
30
+ return {'intro_block': __intro}
31
+
32
+ # MODEL ########################################################################
33
+
34
+ def create_model_block() -> dict:
35
+ __model = gradio.Dropdown(label='Model', value='openai/gpt-oss-20b', choices=['openai/gpt-oss-20b'], scale=1, allow_custom_value=False, multiselect=False, interactive=True) # 'openai/gpt-oss-120b'
36
+ return {'model_block': __model,}
37
+
38
+ # SAMPLING #####################################################################
39
+
40
+ def create_sampling_block() -> dict:
41
+ __tokens = gradio.Slider(label='Tokens', value=16, minimum=1, maximum=128, step=1, scale=1, interactive=True)
42
+ __topk = gradio.Slider(label='Top K', value=4, minimum=1, maximum=8, step=1, scale=1, interactive=True)
43
+ __topp = gradio.Slider(label='Top P', value=0.9, minimum=0.0, maximum=1.0, step=0.1, scale=1, interactive=True)
44
+ return {
45
+ 'tokens_block': __tokens,
46
+ 'topk_block': __topk,
47
+ 'topp_block': __topp}
48
+
49
+ # TARGET #######################################################################
50
+
51
+ def create_target_block() -> dict:
52
+ __target = gradio.Radio(label='Score', value='Inputs', choices=['Inputs', 'Everything'], scale=1, interactive=True)
53
+ return {'target_block': __target}
54
+
55
+ # DISPLAY ######################################################################
56
+
57
+ # def create_display_block() -> dict:
58
+ # __display = gradio.Radio(label='Display', value='Tokens', choices=['Tokens', 'Indexes'], scale=1, interactive=True)
59
+ # return {'display_block': __display}
60
+
61
+ # INPUTS #######################################################################
62
+
63
+ def create_inputs_block() -> dict:
64
+ __input = gradio.Textbox(label='Prompt', value='', placeholder='A string of tokens to score.', lines=4, scale=1, show_copy_button=True, interactive=True)
65
+ return {'input_block': __input}
66
+
67
+ # OUTPUTS ######################################################################
68
+
69
+ def create_outputs_block() -> dict:
70
+ __output = gradio.HighlightedText(label='Scores', value='', scale=1, interactive=False, show_legend=False, show_inline_category=False, combine_adjacent=False, color_map=create_color_map(), elem_classes='white-text')
71
+ return {'output_block': __output}
72
+
73
+ # SELECT #######################################################################
74
+
75
+ def create_selection_block() -> dict:
76
+ __position = gradio.Slider(label='Token Position', value=-1, minimum=-1, maximum=15, step=1, scale=1, interactive=True) # info='-1 to average on all tokens'
77
+ __layer = gradio.Slider(label='Layer Depth', value=12, minimum=-1, maximum=23, step=1, scale=1, interactive=True) # info='-1 to average on all layers'
78
+ __head = gradio.Slider(label='Attention Head', value=-1, minimum=-1, maximum=63, step=1, scale=1, interactive=True) # info='-1 to average on all heads'
79
+ return {
80
+ 'position_block': __position,
81
+ 'layer_block': __layer,
82
+ 'head_block': __head,}
83
+
84
+ # ACTIONS ######################################################################
85
+
86
+ def create_actions_block() -> dict:
87
+ __process = gradio.Button('Process', variant='primary', size='lg', scale=1, interactive=True)
88
+ return {'process_block': __process,}
89
+
90
+ # STATE ########################################################################
91
+
92
+ def create_state() -> dict:
93
+ return {
94
+ 'input_state': gradio.State(None),
95
+ 'output_state': gradio.State(None),
96
+ 'attention_state': gradio.State(None),}
97
+
98
+ # LAYOUT #######################################################################
99
+
100
+ def create_layout(intro: str=INTRO) -> dict:
101
+ __fields = {}
102
+ __fields.update(create_intro_block(intro=intro))
103
+ with gradio.Tabs():
104
+ with gradio.Tab('Score Tokens') as __main_tab:
105
+ __fields.update({'main_tab': __main_tab})
106
+ with gradio.Row(equal_height=True):
107
+ __fields.update(create_inputs_block())
108
+ __fields.update(create_outputs_block())
109
+ with gradio.Row(equal_height=True):
110
+ __fields.update(create_selection_block())
111
+ with gradio.Row(equal_height=True):
112
+ __fields.update(create_actions_block())
113
+ with gradio.Tab('Settings') as __settings_tab:
114
+ __fields.update({'settings_tab': __settings_tab})
115
+ with gradio.Column(scale=1):
116
+ with gradio.Row(equal_height=True):
117
+ __fields.update(create_model_block())
118
+ with gradio.Row(equal_height=True):
119
+ __fields.update(create_sampling_block())
120
+ with gradio.Row(equal_height=True):
121
+ __fields.update(create_target_block())
122
+ # __fields.update(create_display_block())
123
+ return __fields
124
+
125
+ # EVENTS #######################################################################
126
+
127
+ def update_layer_range(value: float, model: str) -> dict:
128
+ return gradio.update(maximum=35, value=min(35, int(value))) if '120b' in model else gradio.update(maximum=23, value=min(23, int(value)))
129
+
130
+ def update_position_range(value: float, tokens: float) -> dict:
131
+ return gradio.update(maximum=int(tokens) - 1, value=min(int(tokens) - 1, int(value)))
132
+
133
+ def update_computation_state(
134
+ token_num: float,
135
+ topk_num: float,
136
+ topp_num: float,
137
+ token_idx: float,
138
+ layer_idx: float,
139
+ head_idx: float,
140
+ prompt_str: str,
141
+ device_str: str,
142
+ model_obj: object,
143
+ tokenizer_obj: object,
144
+ ) -> tuple:
145
+ # sanitize the inputs
146
+ __token_num = max(1, min(128, int(token_num)))
147
+ __topk_num = max(1, min(8, int(topk_num)))
148
+ __topp_num = max(0.0, min(1.0, float(topp_num)))
149
+ __token_idx = max(-1, min(__token_num, int(token_idx)))
150
+ __layer_idx = max(-1, int(layer_idx))
151
+ __head_idx = max(-1, int(head_idx))
152
+ __prompt_str = prompt_str.strip()
153
+ __device_str = device_str if (device_str in ['cpu', 'cuda']) else 'cpu'
154
+ # exit if some values are missing
155
+ if (not __prompt_str) or (model_obj is None) or (tokenizer_obj is None):
156
+ return ([], [], [], torch.empty(0))
157
+ # handle all exceptions at once
158
+ try:
159
+ # dictionary {'input_ids': _, 'attention_mask': _}
160
+ __input_data = psaiops.common.tokenizer.preprocess_token_ids(
161
+ tokenizer_obj=tokenizer_obj,
162
+ prompt_str=__prompt_str,
163
+ device_str=__device_str)
164
+ # parse the inputs
165
+ __input_dim = int(__input_data['input_ids'].shape[-1])
166
+ # tensor (1, T)
167
+ __output_data = psaiops.common.model.generate_token_ids(
168
+ model_obj=model_obj,
169
+ input_ids=__input_data['input_ids'],
170
+ attention_mask=__input_data['attention_mask'],
171
+ token_num=__token_num,
172
+ topk_num=__topk_num,
173
+ topp_num=__topp_num)
174
+ # tensor (L, S, H, T, T)
175
+ __attention_data = psaiops.score.attention.lib.compute_attention_weights(
176
+ model_obj=model_obj,
177
+ token_obj=__output_data)
178
+ # reduce the layer, sample, head and output token axes => tensor (T,)
179
+ __score_data = psaiops.score.attention.lib.reduce_attention_weights(
180
+ attention_data=__attention_data,
181
+ token_idx=__token_idx,
182
+ layer_idx=__layer_idx,
183
+ head_idx=__head_idx,
184
+ input_dim=__input_dim)
185
+ # translate the scores into integer labels
186
+ __labels = psaiops.score.attention.lib.postprocess_attention_scores(
187
+ attention_data=__score_data,
188
+ input_dim=__input_dim,
189
+ token_idx=__token_idx)
190
+ # detokenize the IDs
191
+ __tokens = psaiops.common.tokenizer.postprocess_token_ids(
192
+ tokenizer_obj=tokenizer_obj,
193
+ token_obj=__output_data)
194
+ # update each component => (input, output, attention, highligh) states
195
+ return (
196
+ list(zip(__tokens, __labels)),
197
+ __tokens[:__input_dim],
198
+ __tokens[__input_dim:],
199
+ __attention_data,)
200
+ except:
201
+ raise Exception('Attention generation aborted with an error.')
202
+
203
+ def update_text_highlight(
204
+ token_idx: float,
205
+ layer_idx: float,
206
+ head_idx: float,
207
+ input_data: list,
208
+ output_data: list,
209
+ attention_data: torch.Tensor,
210
+ ) -> list:
211
+ # sanitize the inputs
212
+ __input_data = input_data or []
213
+ __output_data = output_data or []
214
+ __attention_data = torch.empty(0) if (attention_data is None) else attention_data
215
+ __input_dim = len(__input_data)
216
+ __output_dim = len(__output_data)
217
+ __token_idx = max(-1, min(__output_dim, int(token_idx)))
218
+ __layer_idx = max(-1, int(layer_idx))
219
+ __head_idx = max(-1, int(head_idx))
220
+ # exit if the data has not yet been computed
221
+ if (not __input_data) or (not __output_data) or (attention_data is None) or (len(attention_data) == 0):
222
+ return gradio.update()
223
+ # handle all exceptions at once
224
+ try:
225
+ # concat input and output tokens
226
+ __tokens = __input_data + __output_data
227
+ # reduce the layer, sample, head and output token axes => tensor (T,)
228
+ __scores = psaiops.score.attention.lib.reduce_attention_weights(
229
+ attention_data=__attention_data,
230
+ token_idx=__token_idx,
231
+ layer_idx=__layer_idx,
232
+ head_idx=__head_idx,
233
+ input_dim=__input_dim)
234
+ # translate the scores into integer labels
235
+ __labels = psaiops.score.attention.lib.postprocess_attention_scores(
236
+ attention_data=__scores,
237
+ input_dim=__input_dim,
238
+ token_idx=__token_idx)
239
+ # update the component with [(token, label), ...]
240
+ return list(zip(__tokens, __labels))
241
+ except:
242
+ raise Exception('Attention reduction aborted with an error.')
243
+
244
+ # APP ##########################################################################
245
+
246
+ def create_app(title: str=TITLE, intro: str=INTRO, style: str=STYLE, model: str=MODEL) -> gradio.Blocks:
247
+ __fields = {}
248
+ with gradio.Blocks(theme=gradio.themes.Soft(), title=title, css=style) as __app:
249
+ # load the model
250
+ __device = 'cuda' if torch.cuda.is_available() else 'cpu'
251
+ __model = psaiops.common.model.get_model(name=model, device=__device)
252
+ __tokenizer = psaiops.common.tokenizer.get_tokenizer(name=model, device=__device)
253
+ # adapt the computing function
254
+ __compute = functools.partial(update_computation_state, model_obj=__model, tokenizer_obj=__tokenizer, device_str=__device)
255
+ # create the UI
256
+ __fields.update(create_layout(intro=intro))
257
+ # init the state
258
+ __fields.update(create_state())
259
+ # wire the input fields
260
+ __fields['tokens_block'].change(
261
+ fn=update_position_range,
262
+ inputs=[__fields[__k] for __k in ['position_block', 'tokens_block']],
263
+ outputs=__fields['position_block'],
264
+ queue=False,
265
+ show_progress='hidden')
266
+ __fields['model_block'].change(
267
+ fn=update_layer_range,
268
+ inputs=[__fields[__k] for __k in ['layer_block', 'model_block']],
269
+ outputs=__fields['layer_block'],
270
+ queue=False,
271
+ show_progress='hidden')
272
+ __fields['process_block'].click(
273
+ fn=__compute,
274
+ inputs=[__fields[__k] for __k in ['tokens_block', 'topk_block', 'topp_block', 'position_block', 'layer_block', 'head_block', 'input_block']],
275
+ outputs=[__fields[__k] for __k in ['output_block', 'input_state', 'output_state', 'attention_state']],
276
+ queue=False,
277
+ show_progress='full')
278
+ __fields['position_block'].change(
279
+ fn=update_text_highlight,
280
+ inputs=[__fields[__k] for __k in ['position_block', 'layer_block', 'head_block', 'input_state', 'output_state', 'attention_state']],
281
+ outputs=__fields['output_block'],
282
+ queue=False,
283
+ show_progress='hidden')
284
+ __fields['layer_block'].change(
285
+ fn=update_text_highlight,
286
+ inputs=[__fields[__k] for __k in ['position_block', 'layer_block', 'head_block', 'input_state', 'output_state', 'attention_state']],
287
+ outputs=__fields['output_block'],
288
+ queue=False,
289
+ show_progress='hidden')
290
+ __fields['head_block'].change(
291
+ fn=update_text_highlight,
292
+ inputs=[__fields[__k] for __k in ['position_block', 'layer_block', 'head_block', 'input_state', 'output_state', 'attention_state']],
293
+ outputs=__fields['output_block'],
294
+ queue=False,
295
+ show_progress='hidden')
296
+ # gradio application
297
+ return __app
298
+
299
+ # MAIN #########################################################################
300
+
301
+ if __name__ == '__main__':
302
+ __app = create_app()
303
+ __app.launch(share=True, debug=True)
@@ -0,0 +1,118 @@
1
+ import torch
2
+
3
+ import psaiops.common.model
4
+ import psaiops.common.tokenizer
5
+
6
+ # COMPUTE ########################################################################
7
+
8
+ def compute_attention_weights(
9
+ model_obj: object,
10
+ token_obj: torch.Tensor,
11
+ ) -> torch.Tensor:
12
+ # process the full sequence
13
+ with torch.no_grad():
14
+ __outputs = model_obj(
15
+ input_ids=token_obj,
16
+ output_attentions=True,
17
+ return_dict=True)
18
+ # parse the outputs
19
+ return torch.stack(__outputs.attentions, dim=0)
20
+
21
+ # REDUCE #######################################################################
22
+
23
+ def reduce_attention_weights(
24
+ attention_data: torch.Tensor,
25
+ token_idx: int, # -1 => avg over all tokens
26
+ layer_idx: int, # -1 => avg over layers
27
+ head_idx: int, # -1 => avg over heads
28
+ input_dim: int,
29
+ ) -> torch.Tensor:
30
+ # parse
31
+ __layer_dim, __batch_dim, __head_dim, __output_dim, __output_dim = tuple(attention_data.shape) # L, B, H, T, T
32
+ __layer_idx = min(layer_idx, __layer_dim - 1)
33
+ __head_idx = min(head_idx, __head_dim - 1)
34
+ __token_idx = min(token_idx, __output_dim - input_dim - 1) # T = I + O
35
+ # select the relevant data along each axis
36
+ __layer_slice = slice(None) if (__layer_idx < 0) else slice(__layer_idx, __layer_idx + 1)
37
+ __sample_slice = slice(None)
38
+ __head_slice = slice(None) if (__head_idx < 0) else slice(__head_idx, __head_idx + 1)
39
+ __token_slice = slice(input_dim - 1, __output_dim) if (__token_idx < 0) else slice(input_dim + __token_idx - 1, input_dim + __token_idx)
40
+ # filter the data
41
+ __data = attention_data[__layer_slice, __sample_slice, __head_slice, __token_slice, slice(None)]
42
+ # reduce all the axes but the last
43
+ return __data.mean(dim=tuple(range(len(__data.shape) - 1)))
44
+
45
+ # FORMAT #########################################################################
46
+
47
+ def postprocess_attention_scores(
48
+ attention_data: torch.Tensor, # (T,)
49
+ input_dim: int,
50
+ token_idx: int,
51
+ ) -> list:
52
+ __output_dim = int(attention_data.shape[-1])
53
+ # isolate the scores of the input prompt
54
+ __input_slice = slice(0, input_dim)
55
+ # mask the token that were used to compute the scores
56
+ __token_idx = min(token_idx, __output_dim - input_dim - 1) # T = I + O
57
+ __output_range = list(range(__output_dim - input_dim)) if (__token_idx < 0) else [__token_idx]
58
+ __output_mask = torch.BoolTensor([__i in __output_range for __i in range(__output_dim - input_dim)])
59
+ # normalize the scores
60
+ __input_scores = attention_data[__input_slice] / (attention_data[__input_slice].max() + 1e-5)
61
+ # round to obtain integer labels from 0 to 100
62
+ __input_scores = torch.round(100.0 * __input_scores, decimals=0).type(torch.int32)
63
+ # the generated tokens are not scored
64
+ __output_scores = torch.where(__output_mask, -1, 0).type(torch.int32)
65
+ # native list of serialized integers
66
+ return [str(__i) for __i in __input_scores.tolist() + __output_scores.tolist()] # (I,) + (O,) = (T,)
67
+
68
+ # COMPUTE ########################################################################
69
+
70
+ def score_tokens(
71
+ prompt_str: str,
72
+ token_num: int,
73
+ topk_num: int,
74
+ topp_num: float,
75
+ token_idx: int,
76
+ layer_idx: int,
77
+ head_idx: int,
78
+ device_str: str,
79
+ model_obj: object,
80
+ tokenizer_obj: object,
81
+ ) -> list:
82
+ # dictionary {'input_ids': _, 'attention_mask': _}
83
+ __inputs = psaiops.common.tokenizer.preprocess_token_ids(
84
+ tokenizer_obj=tokenizer_obj,
85
+ prompt_str=prompt_str,
86
+ device_str=device_str)
87
+ # parse the inputs
88
+ __input_dim = int(__inputs['input_ids'].shape[-1])
89
+ # tensor (1, T)
90
+ __outputs = psaiops.common.tokenizer.model.generate_token_ids(
91
+ model_obj=model_obj,
92
+ input_ids=__inputs['input_ids'],
93
+ attention_mask=__inputs['attention_mask'],
94
+ token_num=token_num,
95
+ topk_num=topk_num,
96
+ topp_num=topp_num)
97
+ # tensor (L, S, H, T, T)
98
+ __attentions = compute_attention_weights(
99
+ model_obj=model_obj,
100
+ token_obj=__outputs)
101
+ # reduce the layer, sample, head and output token axes => tensor (T,)
102
+ __scores = reduce_attention_weights(
103
+ __attentions,
104
+ token_idx=token_idx,
105
+ layer_idx=layer_idx,
106
+ head_idx=head_idx,
107
+ input_dim=__input_dim)
108
+ # translate the scores into integer labels
109
+ __labels = postprocess_attention_scores(
110
+ __scores,
111
+ input_dim=__input_dim,
112
+ token_idx=token_idx)
113
+ # detokenize the IDs
114
+ __tokens = psaiops.common.tokenizer.postprocess_token_ids(
115
+ tokenizer_obj=tokenizer_obj,
116
+ token_obj=__outputs)
117
+ # match tokens and labels for the HighlightedText field
118
+ return list(zip(__tokens, __labels))
File without changes