psaiops 0.1.1__tar.gz → 0.2.1__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.

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: psaiops
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: Web apps to inspect & engineer NN activations.
5
5
  License: .github/LICENSE.md
6
6
  Author: apehex
@@ -12,9 +12,12 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: accelerate (>=1.10)
15
16
  Requires-Dist: deformers (>=0.0)
16
17
  Requires-Dist: gradio (>=5.0)
18
+ Requires-Dist: kernels (==0.10)
17
19
  Requires-Dist: requests (>=2.0)
20
+ Requires-Dist: triton (==3.4)
18
21
  Description-Content-Type: text/markdown
19
22
 
20
23
  # psAI ops <img src="images/logo.png" alt="apehex logo" width="32" height="32">
@@ -0,0 +1,19 @@
1
+ import gradio
2
+
3
+ import psaiops.elements.data
4
+
5
+ # AUTO-COMPLETE ################################################################
6
+
7
+ def update_dropdown(label: str, data: gradio.KeyUpData):
8
+ # model_dropdown.key_up(fn=update_dropdown, inputs=[model_dropdown, gradio.State("model")], outputs=model_dropdown, queue=False, show_progress="hidden")
9
+ datasets = psaiops.elements.data.query_huggingface(target=data.input_value, label=label, limit=16)
10
+ return gradio.update(choices=datasets, visible=True)
11
+
12
+ # with gradio.Blocks() as demo:
13
+ # model_dropdown = gradio.Dropdown(label="Models Auto-Complete", choices=[""], allow_custom_value=True)
14
+ # dataset_dropdown = gradio.Dropdown(label="Datasets Auto-Complete", choices=[""], allow_custom_value=True)
15
+ # spaces_dropdown = gradio.Dropdown(label="Spaces Auto-Complete", choices=[""], allow_custom_value=True)
16
+ # model_dropdown.key_up(fn=update_dropdown, inputs=[gradio.State("model")], outputs=model_dropdown, queue=False, show_progress="hidden")
17
+ # dataset_dropdown.key_up(fn=update_dropdown, inputs=[gradio.State("dataset")], outputs=dataset_dropdown, queue=False, show_progress="hidden")
18
+ # spaces_dropdown.key_up(fn=update_dropdown, inputs=[gradio.State("space")], outputs=spaces_dropdown, queue=False, show_progress="hidden")
19
+ # demo.launch(share=True, debug=True)
@@ -0,0 +1,192 @@
1
+ import functools
2
+
3
+ import gradio
4
+ import pandas
5
+ import torch
6
+ import torch.cuda
7
+
8
+ import psaiops.compose.contrast.lib
9
+
10
+ # META #########################################################################
11
+
12
+ TITLE = '''Contrastive Steering'''
13
+ INTRO = '''Add a delta of activation to a prompt to steer the model output in a specific latent direction.'''
14
+ STYLE = '''.giga-text input { font-size: 32px; }'''
15
+
16
+ MODEL = 'openai/gpt-oss-20b'
17
+
18
+ # COLORS #######################################################################
19
+
20
+ def create_color_map() -> dict:
21
+ return {
22
+ '-1': '#004444',
23
+ **{str(__i): '#{:02x}0000'.format(int(2.55 * __i)) for __i in range(101)}}
24
+
25
+ # INTRO ########################################################################
26
+
27
+ def create_intro_block(intro: str) -> dict:
28
+ __intro = gradio.Markdown(intro)
29
+ return {'intro_block': __intro}
30
+
31
+ # MODEL ########################################################################
32
+
33
+ def create_model_block() -> dict:
34
+ __model = gradio.Dropdown(label='Model ID', value='openai/gpt-oss-20b', choices=['openai/gpt-oss-20b'], scale=1, allow_custom_value=False, multiselect=False, interactive=True) # 'openai/gpt-oss-120b'
35
+ __layer = gradio.Slider(label='Layer Depth', value=12, minimum=0, maximum=23, step=1, scale=1, interactive=True)
36
+ return {
37
+ 'model_block': __model,
38
+ 'layer_block': __layer,}
39
+
40
+ # SAMPLING #####################################################################
41
+
42
+ def create_sampling_block() -> dict:
43
+ __tokens = gradio.Slider(label='Tokens', value=16, minimum=1, maximum=128, step=1, scale=1, interactive=True)
44
+ __topk = gradio.Slider(label='Top K', value=4, minimum=1, maximum=8, step=1, scale=1, interactive=True)
45
+ __topp = gradio.Slider(label='Top P', value=0.9, minimum=0.0, maximum=1.0, step=0.1, scale=1, interactive=True)
46
+ return {
47
+ 'tokens_block': __tokens,
48
+ 'topk_block': __topk,
49
+ 'topp_block': __topp,}
50
+
51
+ # REDUCTION ####################################################################
52
+
53
+ def create_reduction_block() -> dict:
54
+ __from = gradio.Slider(label='Average From', value=0, minimum=0, maximum=256, step=1, scale=1, interactive=True)
55
+ __to = gradio.Slider(label='Average To', value=256, minimum=0, maximum=256, step=1, scale=1, interactive=True)
56
+ return {
57
+ 'from_block': __from,
58
+ 'to_block': __to,}
59
+
60
+ # INPUTS #######################################################################
61
+
62
+ def create_inputs_row(operation: str='', index: int=0, label: bool=False) -> dict:
63
+ # __operation = gradio.Button(value=operation, variant='primary', size='lg', elem_classes='white-text', scale=1, interactive=False)
64
+ __operation = gradio.Dropdown(label=f'Operation', value=operation, choices=['', '+', '-', 'x', '.', '='], elem_classes='giga-text', scale=1, show_label=label, allow_custom_value=False, multiselect=False, interactive=False)
65
+ __alpha = gradio.Slider(label='Factor', value=1.0, minimum=0.0, maximum=16.0, step=0.1, scale=1, show_label=label, interactive=True)
66
+ __input = gradio.Textbox(label=f'Prompt', value='', placeholder='Some text.', lines=2, max_lines=2, scale=8, show_label=label, show_copy_button=True, interactive=True)
67
+ return {
68
+ f'operation_{index}_block': __operation,
69
+ f'factor_{index}_block': __alpha,
70
+ f'prompt_{index}_block': __input,}
71
+
72
+ # OUTPUTS ######################################################################
73
+
74
+ def create_outputs_block() -> dict:
75
+ __output = gradio.Textbox(label='= Total', value='', placeholder='Some text.', lines=2, max_lines=8, scale=1, show_label=True, show_copy_button=True, interactive=False)
76
+ return {'output_block': __output}
77
+
78
+ # ACTIONS ######################################################################
79
+
80
+ def create_actions_block() -> dict:
81
+ __process = gradio.Button(value='Process', variant='primary', size='lg', scale=1, interactive=True)
82
+ return {'process_block': __process,}
83
+
84
+ # TABLE ########################################################################
85
+
86
+ def create_table_block() -> dict:
87
+ __table = gradio.DataFrame(label='Summary', type='numpy', headers=None, row_count=4, col_count=256, scale=1, interactive=False)
88
+ return {'table_block': __table,}
89
+
90
+ # STATE ########################################################################
91
+
92
+ def create_state() -> dict:
93
+ return {}
94
+
95
+ # LAYOUT #######################################################################
96
+
97
+ def create_layout(intro: str=INTRO) -> dict:
98
+ __fields = {}
99
+ __fields.update(create_intro_block(intro=intro))
100
+ with gradio.Tabs():
101
+ with gradio.Tab('Equation') as __main_tab:
102
+ __fields.update({'main_tab': __main_tab})
103
+ with gradio.Row(equal_height=True):
104
+ __fields.update(create_inputs_row(operation='', index=0, label=True))
105
+ with gradio.Row(equal_height=True):
106
+ __fields.update(create_inputs_row(operation='-', index=1, label=False))
107
+ with gradio.Row(equal_height=True):
108
+ __fields.update(create_inputs_row(operation='+', index=2, label=False))
109
+ with gradio.Row(equal_height=True):
110
+ __fields.update(create_outputs_block())
111
+ with gradio.Row(equal_height=True):
112
+ __fields.update(create_actions_block())
113
+ with gradio.Tab('Details') as __details_tab:
114
+ with gradio.Row(equal_height=True):
115
+ __fields.update(create_table_block())
116
+ with gradio.Tab('Settings') as __settings_tab:
117
+ __fields.update({'settings_tab': __settings_tab})
118
+ with gradio.Column(scale=1):
119
+ with gradio.Row(equal_height=True):
120
+ __fields.update(create_model_block())
121
+ with gradio.Row(equal_height=True):
122
+ __fields.update(create_sampling_block())
123
+ with gradio.Row(equal_height=True):
124
+ __fields.update(create_reduction_block())
125
+ # __fields.update(create_display_block())
126
+ return __fields
127
+
128
+ # EVENTS #######################################################################
129
+
130
+ def update_layer_range(value: float, model: str) -> dict:
131
+ return gradio.update(maximum=35, value=min(35, int(value))) if '120b' in model else gradio.update(maximum=23, value=min(23, int(value)))
132
+
133
+ def update_table_data(positive: str, negative: str, prompt: str, output: str, tokenizer: object) -> pandas.DataFrame:
134
+ # array of token IDs
135
+ __outputs = tokenizer([positive, negative, prompt, output], return_tensors='pt', padding=True)
136
+ # array of token strings
137
+ __tokens = [tokenizer.convert_ids_to_tokens(__s) for __s in __outputs['input_ids']]
138
+ # shift the special characters
139
+ __tokens = [[__t.replace(chr(0x0120), ' ').replace(chr(0x010a), '\\n') for __t in __s] for __s in __tokens]
140
+ # mask the tokens that differ between positive and negative prompts
141
+ __masks = psaiops.compose.contrast.lib.compute_sequence_mask(tokens=__outputs['input_ids'])
142
+ # convert into a data frame
143
+ __data = pandas.DataFrame(__tokens)
144
+ # color the background in red for the positions marked by the mask
145
+ return __data.style.apply(update_table_style, masks=pandas.DataFrame(__masks), axis=None)
146
+
147
+ def update_table_style(data: pandas.DataFrame, masks: pandas.DataFrame) -> pandas.DataFrame:
148
+ return pandas.DataFrame(masks.replace({True: 'background-color: rgb(255, 0, 0, 64%)', False: 'background-color: rgb(0, 0, 0, 0%)',}))
149
+
150
+ # APP ##########################################################################
151
+
152
+ def create_app(title: str=TITLE, intro: str=INTRO, style: str=STYLE, model: str=MODEL) -> gradio.Blocks:
153
+ __fields = {}
154
+ with gradio.Blocks(theme=gradio.themes.Soft(), title=title, css=style) as __app:
155
+ # load the model
156
+ __device = 'cuda' if torch.cuda.is_available() else 'cpu'
157
+ __model = psaiops.compose.contrast.lib.get_model(name=model, device=__device)
158
+ __tokenizer = psaiops.compose.contrast.lib.get_tokenizer(name=model, device=__device)
159
+ # adapt the computing functions
160
+ __compute = functools.partial(psaiops.compose.contrast.lib.steer_model_output, model_obj=__model, tokenizer_obj=__tokenizer, device_str=__device)
161
+ __format = functools.partial(update_table_data, tokenizer=__tokenizer)
162
+ # create the UI
163
+ __fields.update(create_layout(intro=intro))
164
+ # init the state
165
+ __fields.update(create_state())
166
+ # wire the input fields
167
+ __fields['model_block'].change(
168
+ fn=update_layer_range,
169
+ inputs=[__fields[__k] for __k in ['layer_block', 'model_block']],
170
+ outputs=__fields['layer_block'],
171
+ queue=False,
172
+ show_progress='hidden')
173
+ __fields['output_block'].change(
174
+ fn=__format,
175
+ inputs=[__fields[__k] for __k in ['prompt_0_block', 'prompt_1_block', 'prompt_2_block', 'output_block']],
176
+ outputs=__fields['table_block'],
177
+ queue=False,
178
+ show_progress='hidden')
179
+ __fields['process_block'].click(
180
+ fn=__compute,
181
+ inputs=[__fields[__k] for __k in ['prompt_0_block', 'prompt_1_block', 'prompt_2_block', 'factor_0_block', 'factor_1_block', 'factor_2_block', 'tokens_block', 'topk_block', 'topp_block', 'layer_block']],
182
+ outputs=__fields['output_block'],
183
+ queue=False,
184
+ show_progress='full')
185
+ # gradio application
186
+ return __app
187
+
188
+ # MAIN #########################################################################
189
+
190
+ if __name__ == '__main__':
191
+ __app = create_app()
192
+ __app.launch(share=True, debug=True)
@@ -0,0 +1,179 @@
1
+ import functools
2
+
3
+ import torch
4
+ import torch.nn
5
+ import torch.nn.modules
6
+ import transformers
7
+
8
+ import deformers.models.openai.gptoss
9
+ import mlable.shapes
10
+
11
+ # LOAD #########################################################################
12
+
13
+ @functools.lru_cache(maxsize=4)
14
+ def get_tokenizer(name: str, device: str='cpu'):
15
+ return transformers.AutoTokenizer.from_pretrained(
16
+ name,
17
+ use_fast=True,
18
+ dtype='auto',
19
+ device_map=device)
20
+
21
+ @functools.lru_cache(maxsize=2)
22
+ def get_model(name: str, device: str='cpu'):
23
+ __model = deformers.models.openai.gptoss.GptOssForCausalInference.from_pretrained(
24
+ name,
25
+ dtype='auto',
26
+ device_map=device)
27
+ # toggle the inference mode (not training)
28
+ __model.eval()
29
+ # transformers model
30
+ return __model
31
+
32
+ # PREPROCESS #####################################################################
33
+
34
+ @functools.lru_cache(maxsize=4)
35
+ def preprocess_token_ids(
36
+ tokenizer: object,
37
+ prompts: list,
38
+ device: str='cpu'
39
+ ) -> dict:
40
+ # tokenize
41
+ __inputs = tokenizer(prompts, return_tensors='pt', padding=True)
42
+ # move to the main device
43
+ return {__k: __v.to(device) for __k, __v in __inputs.items()}
44
+
45
+ # HOOK #########################################################################
46
+
47
+ def capture_hidden_activation(
48
+ module: torch.nn.modules.Module,
49
+ inputs: torch.Tensor,
50
+ outputs: torch.Tensor,
51
+ index: int,
52
+ captured: dict,
53
+ ) -> None:
54
+ captured[index] = outputs # (B, S, E)
55
+
56
+ # MASKS ########################################################################
57
+
58
+ def compute_sequence_mask(
59
+ tokens: torch.Tensor, # (B, S)
60
+ # masks: torch.Tensor, # (B, S)
61
+ ) -> torch.Tensor:
62
+ __shape = mlable.shapes.divide(tokens.shape, axis=0, factor=2, insert=True)
63
+ # group the samples two by two
64
+ __data = tokens.reshape(__shape)
65
+ # compare each sample with its neighbor
66
+ __masks = __data[:, :1] != __data[:, 1:]
67
+ # apply the same mask to both samples
68
+ return __masks.expand(__shape).reshape(tokens.shape)
69
+
70
+ # REDUCTION ####################################################################
71
+
72
+ def compute_delta_activation(
73
+ data: torch.Tensor, # (B, S, E)
74
+ masks: torch.Tensor, # (B, S,)
75
+ signs: torch.Tensor, # (B,)
76
+ keepdim: bool=True,
77
+ ) -> torch.Tensor:
78
+ __dtype = data.dtype
79
+ __device = data.device
80
+ __dim0, __dim1, __dim2 = tuple(data.shape)
81
+ # sign each sample along the batch axis
82
+ __shape = tuple(mlable.shapes.filter(data.shape, axes=[0]))
83
+ __signs = signs.to(dtype=__dtype, device=__device).view(__shape)
84
+ # combine along the batch axis to keep the shortest mask on the sequence axis
85
+ __shape = tuple(mlable.shapes.filter(data.shape, axes=[0, 1]))
86
+ __masks = masks.to(dtype=__dtype, device=__device).view(__shape)
87
+ # mean factor: half the signs size along the batch axis and the number of positions kept along the sequence axis
88
+ __factor = (0.5 * float(__dim0) * __masks.sum(dim=1, keepdim=True)).clamp(min=1.0)
89
+ # take the difference along the batch axis and the average along the sequence axis
90
+ return (data * __signs * __masks / __factor).sum(dim=[0, 1], keepdim=keepdim)
91
+
92
+ # DELTA ########################################################################
93
+
94
+ def add_delta_activation(
95
+ module: torch.nn.modules.Module,
96
+ inputs: torch.Tensor,
97
+ outputs: torch.Tensor,
98
+ delta: torch.Tensor,
99
+ alpha: torch.Tensor,
100
+ beta: torch.Tensor,
101
+ ) -> torch.Tensor:
102
+ # expand the single feature axis of the delta
103
+ __shape = mlable.shapes.filter(outputs.shape, axes=[-1])
104
+ # rescale the delta
105
+ return alpha * outputs + beta * delta.view(__shape)
106
+
107
+ # MAIN #########################################################################
108
+
109
+ def steer_model_output(
110
+ positive_str: str,
111
+ negative_str: str,
112
+ prompt_str: str,
113
+ positive_rate: float,
114
+ negative_rate: float,
115
+ prompt_rate: float,
116
+ token_num: int,
117
+ topk_num: int,
118
+ topp_num: float,
119
+ layer_idx: int,
120
+ device_str: str,
121
+ model_obj: object,
122
+ tokenizer_obj: object,
123
+ ) -> str:
124
+ # parse & sanitize
125
+ __prompt0 = positive_str.strip()
126
+ __prompt1 = negative_str.strip()
127
+ __prompt2 = prompt_str.strip()
128
+ __alpha0 = max(0.0, float(positive_rate))
129
+ __alpha1 = max(0.0, float(negative_rate))
130
+ __alpha2 = max(0.0, float(prompt_rate))
131
+ __count = max(1, int(token_num))
132
+ __topk = max(1, int(topk_num))
133
+ __topp = max(0.0, float(topp_num))
134
+ __index = max(0, int(layer_idx))
135
+ # store hidden states
136
+ __captured = {}
137
+ # stop if inputs are missing
138
+ if not (__prompt0 and __prompt1 and __prompt2):
139
+ return ''
140
+ # tokenize the 2 prompts and pad to same length
141
+ __inputs = preprocess_token_ids(tokenizer=tokenizer_obj, prompts=(__prompt0, __prompt1), device=device_str)
142
+ # forward hook to capture output hidden state
143
+ __hook = functools.partial(capture_hidden_activation, index=__index, captured=__captured)
144
+ # attach to the model
145
+ __handle = model_obj.model.layers[__index].register_forward_hook(__hook)
146
+ with torch.no_grad():
147
+ # inference mode
148
+ model_obj.eval().to(device_str)
149
+ # prefill with a single forward
150
+ __outputs = model_obj(**__inputs, use_cache=True, output_attentions=False, output_hidden_states=False, return_dict=True)
151
+ # stop capturing activations
152
+ __handle.remove()
153
+ # select only the positions where the tokens differ
154
+ __masks = compute_sequence_mask(tokens=__inputs['input_ids'])
155
+ # activation delta at layer L
156
+ __delta = compute_delta_activation(data=__captured[__index], masks=__masks, signs=torch.Tensor([1, -1]), keepdim=False)
157
+ # add the delta on every forward pass
158
+ __hook = functools.partial(add_delta_activation, alpha=__alpha2, beta=0.5 * (__alpha0 + __alpha1), delta=__delta)
159
+ # attach to the model
160
+ __handle = model_obj.model.layers[__index].register_forward_hook(__hook)
161
+ # now process the user input
162
+ __inputs = preprocess_token_ids(tokenizer=tokenizer_obj, prompts=(prompt_str,), device=device_str)
163
+ # generate the new with tampered activations
164
+ with torch.no_grad():
165
+ __outputs = model_obj.generate(
166
+ **__inputs,
167
+ max_new_tokens=__count,
168
+ do_sample=(0.0 < __topp < 1.0) or (__topk > 0),
169
+ top_k=__topk if (__topk > 0) else None,
170
+ top_p=__topp if (0.0 < __topp <= 1.0) else None,
171
+ return_dict_in_generate=True,
172
+ output_hidden_states=False,
173
+ output_attentions=False,
174
+ output_scores=False,
175
+ use_cache=True)
176
+ # stop altering the activations
177
+ __handle.remove()
178
+ # single string
179
+ return tokenizer_obj.decode(__outputs.sequences[0], skip_special_tokens=True)
File without changes
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "psaiops"
3
- version = "0.1.1"
3
+ version = "0.2.1"
4
4
  description = "Web apps to inspect & engineer NN activations."
5
5
  license = ".github/LICENSE.md"
6
6
  readme = ".github/README.md"
@@ -9,9 +9,12 @@ packages = [{include = "psaiops"}]
9
9
 
10
10
  [tool.poetry.dependencies]
11
11
  python = ">=3.10, <3.14"
12
+ accelerate = ">=1.10"
12
13
  deformers = ">=0.0"
13
14
  gradio = ">=5.0"
15
+ kernels = "0.10"
14
16
  requests = ">=2.0"
17
+ triton = "==3.4"
15
18
 
16
19
  [tool.poetry.group.dev.dependencies]
17
20
  pytest = "*"
File without changes
File without changes