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

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.0
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,178 @@
1
+ import functools
2
+
3
+ import gradio
4
+ import torch
5
+ import torch.cuda
6
+
7
+ import psaiops.compose.contrast.lib
8
+
9
+ # META #########################################################################
10
+
11
+ TITLE = '''Contrastive Steering'''
12
+ INTRO = '''Add a delta of activation to a prompt to steer the model output in a specific latent direction.'''
13
+ STYLE = '''.giga-text input { font-size: 32px; }'''
14
+
15
+ MODEL = 'openai/gpt-oss-20b'
16
+
17
+ # COLORS #######################################################################
18
+
19
+ def create_color_map() -> dict:
20
+ return {
21
+ '-1': '#004444',
22
+ **{str(__i): '#{:02x}0000'.format(int(2.55 * __i)) for __i in range(101)}}
23
+
24
+ # INTRO ########################################################################
25
+
26
+ def create_intro_block(intro: str) -> dict:
27
+ __intro = gradio.Markdown(intro)
28
+ return {'intro_block': __intro}
29
+
30
+ # MODEL ########################################################################
31
+
32
+ def create_model_block() -> dict:
33
+ __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'
34
+ __layer = gradio.Slider(label='Layer Depth', value=12, minimum=0, maximum=23, step=1, scale=1, interactive=True)
35
+ return {
36
+ 'model_block': __model,
37
+ 'layer_block': __layer,}
38
+
39
+ # SAMPLING #####################################################################
40
+
41
+ def create_sampling_block() -> dict:
42
+ __tokens = gradio.Slider(label='Tokens', value=16, minimum=1, maximum=128, step=1, scale=1, interactive=True)
43
+ __topk = gradio.Slider(label='Top K', value=4, minimum=1, maximum=8, step=1, scale=1, interactive=True)
44
+ __topp = gradio.Slider(label='Top P', value=0.9, minimum=0.0, maximum=1.0, step=0.1, scale=1, interactive=True)
45
+ return {
46
+ 'tokens_block': __tokens,
47
+ 'topk_block': __topk,
48
+ 'topp_block': __topp,}
49
+
50
+ # REDUCTION ####################################################################
51
+
52
+ def create_reduction_block() -> dict:
53
+ __from = gradio.Slider(label='Average From', value=0, minimum=0, maximum=256, step=1, scale=1, interactive=True)
54
+ __to = gradio.Slider(label='Average To', value=256, minimum=0, maximum=256, step=1, scale=1, interactive=True)
55
+ return {
56
+ 'from_block': __from,
57
+ 'to_block': __to,}
58
+
59
+ # INPUTS #######################################################################
60
+
61
+ def create_inputs_row(operation: str='', index: int=0, label: bool=False) -> dict:
62
+ # __operation = gradio.Button(value=operation, variant='primary', size='lg', elem_classes='white-text', scale=1, interactive=False)
63
+ __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)
64
+ __alpha = gradio.Slider(label='Factor', value=1.0, minimum=0.0, maximum=16.0, step=0.1, scale=1, show_label=label, interactive=True)
65
+ __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)
66
+ return {
67
+ f'operation_{index}_block': __operation,
68
+ f'factor_{index}_block': __alpha,
69
+ f'prompt_{index}_block': __input,}
70
+
71
+ # OUTPUTS ######################################################################
72
+
73
+ def create_outputs_block() -> dict:
74
+ __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)
75
+ return {'output_block': __output}
76
+
77
+ # ACTIONS ######################################################################
78
+
79
+ def create_actions_block() -> dict:
80
+ __process = gradio.Button(value='Process', variant='primary', size='lg', scale=1, interactive=True)
81
+ return {'process_block': __process,}
82
+
83
+ # TABLE ########################################################################
84
+
85
+ def create_table_block() -> dict:
86
+ __table = gradio.DataFrame(label='Summary', type='numpy', headers=None, row_count=4, col_count=256, scale=1, interactive=False)
87
+ return {'table_block': __table,}
88
+
89
+ # STATE ########################################################################
90
+
91
+ def create_state() -> dict:
92
+ return {}
93
+
94
+ # LAYOUT #######################################################################
95
+
96
+ def create_layout(intro: str=INTRO) -> dict:
97
+ __fields = {}
98
+ __fields.update(create_intro_block(intro=intro))
99
+ with gradio.Tabs():
100
+ with gradio.Tab('Equation') as __main_tab:
101
+ __fields.update({'main_tab': __main_tab})
102
+ with gradio.Row(equal_height=True):
103
+ __fields.update(create_inputs_row(operation='', index=0, label=True))
104
+ with gradio.Row(equal_height=True):
105
+ __fields.update(create_inputs_row(operation='-', index=1, label=False))
106
+ with gradio.Row(equal_height=True):
107
+ __fields.update(create_inputs_row(operation='+', index=2, label=False))
108
+ with gradio.Row(equal_height=True):
109
+ __fields.update(create_outputs_block())
110
+ with gradio.Row(equal_height=True):
111
+ __fields.update(create_actions_block())
112
+ with gradio.Tab('Details') as __details_tab:
113
+ with gradio.Row(equal_height=True):
114
+ __fields.update(create_table_block())
115
+ with gradio.Tab('Settings') as __settings_tab:
116
+ __fields.update({'settings_tab': __settings_tab})
117
+ with gradio.Column(scale=1):
118
+ with gradio.Row(equal_height=True):
119
+ __fields.update(create_model_block())
120
+ with gradio.Row(equal_height=True):
121
+ __fields.update(create_sampling_block())
122
+ with gradio.Row(equal_height=True):
123
+ __fields.update(create_reduction_block())
124
+ # __fields.update(create_display_block())
125
+ return __fields
126
+
127
+ # EVENTS #######################################################################
128
+
129
+ def update_layer_range(value: float, model: str) -> dict:
130
+ return gradio.update(maximum=35, value=min(35, int(value))) if '120b' in model else gradio.update(maximum=23, value=min(23, int(value)))
131
+
132
+ def update_table_data(positive: str, negative: str, prompt: str, output: str, tokenizer: object) -> list:
133
+ __outputs = tokenizer([positive, negative, prompt, output], padding=True)
134
+ return [tokenizer.convert_ids_to_tokens(__s) for __s in __outputs['input_ids']]
135
+
136
+ # APP ##########################################################################
137
+
138
+ def create_app(title: str=TITLE, intro: str=INTRO, style: str=STYLE, model: str=MODEL) -> gradio.Blocks:
139
+ __fields = {}
140
+ with gradio.Blocks(theme=gradio.themes.Soft(), title=title, css=style) as __app:
141
+ # load the model
142
+ __device = 'cuda' if torch.cuda.is_available() else 'cpu'
143
+ __model = psaiops.compose.contrast.lib.get_model(name=model, device=__device)
144
+ __tokenizer = psaiops.compose.contrast.lib.get_tokenizer(name=model, device=__device)
145
+ # adapt the computing functions
146
+ __compute = functools.partial(psaiops.compose.contrast.lib.steer_model_output, model_obj=__model, tokenizer_obj=__tokenizer, device_str=__device)
147
+ __format = functools.partial(update_table_data, tokenizer=__tokenizer)
148
+ # create the UI
149
+ __fields.update(create_layout(intro=intro))
150
+ # init the state
151
+ __fields.update(create_state())
152
+ # wire the input fields
153
+ __fields['model_block'].change(
154
+ fn=update_layer_range,
155
+ inputs=[__fields[__k] for __k in ['layer_block', 'model_block']],
156
+ outputs=__fields['layer_block'],
157
+ queue=False,
158
+ show_progress='hidden')
159
+ __fields['output_block'].change(
160
+ fn=__format,
161
+ inputs=[__fields[__k] for __k in ['prompt_0_block', 'prompt_1_block', 'prompt_2_block', 'output_block']],
162
+ outputs=__fields['table_block'],
163
+ queue=False,
164
+ show_progress='hidden')
165
+ __fields['process_block'].click(
166
+ fn=__compute,
167
+ 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']],
168
+ outputs=__fields['output_block'],
169
+ queue=False,
170
+ show_progress='full')
171
+ # gradio application
172
+ return __app
173
+
174
+ # MAIN #########################################################################
175
+
176
+ if __name__ == '__main__':
177
+ __app = create_app()
178
+ __app.launch(share=True, debug=True)
@@ -0,0 +1,175 @@
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
+ # keep the intersection of the masks
63
+ __masks = masks.prod(dim=0, keepdim=False)
64
+ # and only the positions that differ between positive and negative prompts
65
+ return __masks * (tokens[0] != tokens[1])
66
+
67
+ # REDUCTION ####################################################################
68
+
69
+ def compute_delta_activation(
70
+ data: torch.Tensor, # (B, S, E)
71
+ masks: torch.Tensor, # (S,)
72
+ signs: torch.Tensor, # (B,)
73
+ keepdim: bool=True,
74
+ ) -> torch.Tensor:
75
+ __dtype = data.dtype
76
+ __device = data.device
77
+ # sign each sample along the batch axis
78
+ __shape = tuple(mlable.shapes.filter(data.shape, axes=[0]))
79
+ __signs = signs.to(dtype=__dtype, device=__device).view(__shape)
80
+ # combine along the batch axis to keep the shortest mask on the sequence axis
81
+ __shape = tuple(mlable.shapes.filter(data.shape, axes=[1]))
82
+ __masks = masks.to(dtype=__dtype, device=__device).view(__shape)
83
+ # mean factor: half the signs size along the batch axis and the number of positions kept along the sequence axis
84
+ __factor = (0.5 * float(len(__signs)) * __masks.sum()).clamp(min=1e-8)
85
+ # take the difference along the batch axis and the average along the sequence axis
86
+ return (data * __signs * __masks).sum(dim=[0, 1], keepdim=keepdim) / __factor
87
+
88
+ # DELTA ########################################################################
89
+
90
+ def add_delta_activation(
91
+ module: torch.nn.modules.Module,
92
+ inputs: torch.Tensor,
93
+ outputs: torch.Tensor,
94
+ delta: torch.Tensor,
95
+ alpha: torch.Tensor,
96
+ beta: torch.Tensor,
97
+ ) -> torch.Tensor:
98
+ # expand the single feature axis of the delta
99
+ __shape = mlable.shapes.filter(outputs.shape, axes=[-1])
100
+ # rescale the delta
101
+ return alpha * outputs + beta * delta.view(__shape)
102
+
103
+ # MAIN #########################################################################
104
+
105
+ def steer_model_output(
106
+ positive_str: str,
107
+ negative_str: str,
108
+ prompt_str: str,
109
+ positive_rate: float,
110
+ negative_rate: float,
111
+ prompt_rate: float,
112
+ token_num: int,
113
+ topk_num: int,
114
+ topp_num: float,
115
+ layer_idx: int,
116
+ device_str: str,
117
+ model_obj: object,
118
+ tokenizer_obj: object,
119
+ ) -> str:
120
+ # parse & sanitize
121
+ __prompt0 = positive_str.strip()
122
+ __prompt1 = negative_str.strip()
123
+ __prompt2 = prompt_str.strip()
124
+ __alpha0 = max(0.0, float(positive_rate))
125
+ __alpha1 = max(0.0, float(negative_rate))
126
+ __alpha2 = max(0.0, float(prompt_rate))
127
+ __count = max(1, int(token_num))
128
+ __topk = max(1, int(topk_num))
129
+ __topp = max(0.0, float(topp_num))
130
+ __index = max(0, int(layer_idx))
131
+ # store hidden states
132
+ __captured = {}
133
+ # stop if inputs are missing
134
+ if not (__prompt0 and __prompt1 and __prompt2):
135
+ return ''
136
+ # tokenize the 2 prompts and pad to same length
137
+ __inputs = preprocess_token_ids(tokenizer=tokenizer_obj, prompts=(__prompt0, __prompt1), device=device_str)
138
+ # forward hook to capture output hidden state
139
+ __hook = functools.partial(capture_hidden_activation, index=__index, captured=__captured)
140
+ # attach to the model
141
+ __handle = model_obj.model.layers[__index].register_forward_hook(__hook)
142
+ with torch.no_grad():
143
+ # inference mode
144
+ model_obj.eval().to(device_str)
145
+ # prefill with a single forward
146
+ __outputs = model_obj(**__inputs, use_cache=True, output_attentions=False, output_hidden_states=False, return_dict=True)
147
+ # stop capturing activations
148
+ __handle.remove()
149
+ # select only the positions where the tokens differ
150
+ __masks = compute_sequence_mask(tokens=__inputs['input_ids'], masks=__inputs['attention_mask'])
151
+ # activation delta at layer L
152
+ __delta = compute_delta_activation(data=__captured[__index], masks=__masks, signs=torch.Tensor([1, -1]), keepdim=False)
153
+ # add the delta on every forward pass
154
+ __hook = functools.partial(add_delta_activation, alpha=__alpha2, beta=0.5 * (__alpha0 + __alpha1), delta=__delta)
155
+ # attach to the model
156
+ __handle = model_obj.model.layers[__index].register_forward_hook(__hook)
157
+ # now process the user input
158
+ __inputs = preprocess_token_ids(tokenizer=tokenizer_obj, prompts=(prompt_str,), device=device_str)
159
+ # generate the new with tampered activations
160
+ with torch.no_grad():
161
+ __outputs = model_obj.generate(
162
+ **__inputs,
163
+ max_new_tokens=__count,
164
+ do_sample=(0.0 < __topp < 1.0) or (__topk > 0),
165
+ top_k=__topk if (__topk > 0) else None,
166
+ top_p=__topp if (0.0 < __topp <= 1.0) else None,
167
+ return_dict_in_generate=True,
168
+ output_hidden_states=False,
169
+ output_attentions=False,
170
+ output_scores=False,
171
+ use_cache=True)
172
+ # stop altering the activations
173
+ __handle.remove()
174
+ # single string
175
+ 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.0"
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