Jac-Zac commited on
Commit
dc186e4
·
1 Parent(s): c30bbc5

Massive speedup update + download plot fixing

Browse files
Files changed (5) hide show
  1. app.py +18 -2
  2. tabs/chat.py +6 -3
  3. tabs/chat_ui.py +133 -85
  4. tabs/compare.py +8 -21
  5. utils/datasets.py +22 -0
app.py CHANGED
@@ -1,4 +1,5 @@
1
  import os
 
2
 
3
  import streamlit as st
4
  from dotenv import load_dotenv
@@ -16,6 +17,22 @@ _TABS = ["Chat", "Compare", "Extract"]
16
  _TAB_ICONS = [":material/chat:", ":material/search:", ":material/tune:"]
17
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  def _remote_model_input(remote_models: list[str]) -> str:
20
  """Return the active remote model id, picking from running NDIF deployments or a custom value."""
21
 
@@ -78,8 +95,7 @@ def _sidebar_controls() -> tuple[bool, str, str, str]:
78
  from utils.runtime import list_remote_models
79
 
80
  with st.sidebar:
81
- st.markdown("# Persona UI")
82
- st.caption("Chat, extract, and compare persona runs.")
83
 
84
  if "sidebar__active_tab" not in st.session_state:
85
  st.session_state["sidebar__active_tab"] = "Chat"
 
1
  import os
2
+ import threading
3
 
4
  import streamlit as st
5
  from dotenv import load_dotenv
 
17
  _TAB_ICONS = [":material/chat:", ":material/search:", ":material/tune:"]
18
 
19
 
20
+ def _preload_default_model() -> None:
21
+ """Background-warm the default local model so the first chat is instant."""
22
+ try:
23
+ import torch
24
+
25
+ torch.set_grad_enabled(False)
26
+ from utils.runtime import cached_model
27
+
28
+ cached_model(DEFAULT_MODEL)
29
+ except Exception:
30
+ pass
31
+
32
+
33
+ threading.Thread(target=_preload_default_model, daemon=True).start()
34
+
35
+
36
  def _remote_model_input(remote_models: list[str]) -> str:
37
  """Return the active remote model id, picking from running NDIF deployments or a custom value."""
38
 
 
95
  from utils.runtime import list_remote_models
96
 
97
  with st.sidebar:
98
+ st.markdown("## Persona UI")
 
99
 
100
  if "sidebar__active_tab" not in st.session_state:
101
  st.session_state["sidebar__active_tab"] = "Chat"
tabs/chat.py CHANGED
@@ -18,18 +18,20 @@ from utils.chat import (
18
  resolve_system_prompt,
19
  )
20
  from utils.chat_export import save_chat_export
21
- from utils.datasets import load_dataset
22
  from utils.helpers import widget_key
23
  from utils.runtime import cached_model
24
 
25
  _LAST_PERSONA_ID_KEY = "chat:last_persona_id"
26
  _LAST_PROMPT_MODE_KEY = "chat:last_prompt_mode"
27
  _LAST_COMPARE_MODE_KEY = "chat:last_compare_mode"
 
 
28
 
29
 
30
  def _load_personas(dataset_source: str) -> list[PersonaData] | None:
31
  try:
32
- dataset, dataset_status = load_dataset(
33
  dataset_source,
34
  personas_file=st.session_state.get("extract__personas_file"),
35
  qa_file=st.session_state.get("extract__qa_file"),
@@ -40,7 +42,6 @@ def _load_personas(dataset_source: str) -> list[PersonaData] | None:
40
  st.info("Check the selected dataset source or upload both JSONL files.")
41
  return None
42
 
43
- personas = list(dataset)
44
  if not personas:
45
  st.warning("No personas found in the selected dataset.")
46
  st.info("Try a different dataset source or upload a non-empty personas file.")
@@ -152,6 +153,8 @@ def render_chat_tab(remote: bool, model_name: str, dataset_source: str) -> None:
152
  context_key,
153
  remote,
154
  last_compare_mode_key=_LAST_COMPARE_MODE_KEY,
 
 
155
  )
156
  if tools.compare_mode:
157
  render_compare_mode(
 
18
  resolve_system_prompt,
19
  )
20
  from utils.chat_export import save_chat_export
21
+ from utils.datasets import load_persona_list
22
  from utils.helpers import widget_key
23
  from utils.runtime import cached_model
24
 
25
  _LAST_PERSONA_ID_KEY = "chat:last_persona_id"
26
  _LAST_PROMPT_MODE_KEY = "chat:last_prompt_mode"
27
  _LAST_COMPARE_MODE_KEY = "chat:last_compare_mode"
28
+ _LAST_PROBE_ENABLED_KEY = "chat:last_probe_enabled"
29
+ _LAST_TOKEN_CONTRAST_KEY = "chat:last_token_contrast"
30
 
31
 
32
  def _load_personas(dataset_source: str) -> list[PersonaData] | None:
33
  try:
34
+ personas, dataset_status = load_persona_list(
35
  dataset_source,
36
  personas_file=st.session_state.get("extract__personas_file"),
37
  qa_file=st.session_state.get("extract__qa_file"),
 
42
  st.info("Check the selected dataset source or upload both JSONL files.")
43
  return None
44
 
 
45
  if not personas:
46
  st.warning("No personas found in the selected dataset.")
47
  st.info("Try a different dataset source or upload a non-empty personas file.")
 
153
  context_key,
154
  remote,
155
  last_compare_mode_key=_LAST_COMPARE_MODE_KEY,
156
+ last_probe_enabled_key=_LAST_PROBE_ENABLED_KEY,
157
+ last_token_contrast_key=_LAST_TOKEN_CONTRAST_KEY,
158
  )
159
  if tools.compare_mode:
160
  render_compare_mode(
tabs/chat_ui.py CHANGED
@@ -21,6 +21,21 @@ GENERATION_DEFAULTS = {
21
  "repetition_penalty": 1.0,
22
  }
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  @dataclass(frozen=True)
26
  class GenerationConfig:
@@ -119,6 +134,8 @@ def render_advanced_settings(
119
  remote: bool,
120
  *,
121
  last_compare_mode_key: str,
 
 
122
  ) -> tuple[GenerationConfig, ChatTools]:
123
  """Render the Advanced expander: tool toggles + generation settings."""
124
  with st.expander("Advanced", expanded=False):
@@ -130,12 +147,23 @@ def render_advanced_settings(
130
  last_compare_mode_key, False
131
  )
132
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  tools_col1, tools_col2, tools_col3 = st.columns(3)
134
  with tools_col1:
135
  probe_enabled = st.toggle(
136
  "Probe tools",
137
- value=False,
138
- key=widget_key(context_key, "probe_enabled"),
139
  help="Trace chat activations and run compatible `.pt` probes on tapped tokens.",
140
  )
141
  with tools_col2:
@@ -147,8 +175,7 @@ def render_advanced_settings(
147
  with tools_col3:
148
  token_contrast = st.toggle(
149
  "Token contrast",
150
- value=False,
151
- key=widget_key(context_key, "token_contrast"),
152
  disabled=not compare_mode,
153
  help=(
154
  "Color each generated token by how characteristic it is of each persona. "
@@ -158,95 +185,122 @@ def render_advanced_settings(
158
  ),
159
  )
160
  st.session_state[last_compare_mode_key] = compare_mode
 
 
 
 
161
 
162
  st.divider()
163
  st.caption("Generation")
 
164
 
165
- config_col1, config_col2 = st.columns([2, 1])
166
- with config_col1:
167
- max_new_tokens = st.slider(
168
- "Max new tokens",
169
- min_value=16,
170
- max_value=512,
171
- value=GENERATION_DEFAULTS["max_new_tokens"],
172
- step=16,
173
- key=widget_key(context_key, "max_new_tokens"),
174
- )
175
- with config_col2:
176
- repetition_penalty = st.slider(
177
- "Repetition penalty",
178
- min_value=0.5,
179
- max_value=2.0,
180
- value=GENERATION_DEFAULTS["repetition_penalty"],
181
- step=0.05,
182
- key=widget_key(context_key, "repetition_penalty"),
183
- )
184
 
185
- use_sampling = st.checkbox(
186
- "Random sampling",
187
- value=False,
188
- key=widget_key(context_key, "use_sampling"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  )
190
 
191
- sampling_disabled = not use_sampling
192
- sampling_col1, sampling_col2, sampling_col3 = st.columns(3)
193
- with sampling_col1:
194
- temperature = st.slider(
195
- "Temperature",
196
- min_value=0.01,
197
- max_value=2.0,
198
- value=GENERATION_DEFAULTS["temperature"],
199
- step=0.01,
200
- disabled=sampling_disabled,
201
- key=widget_key(context_key, "temperature"),
202
- )
203
- with sampling_col2:
204
- top_p = st.slider(
205
- "Top-p",
206
- min_value=0.01,
207
- max_value=1.0,
208
- value=GENERATION_DEFAULTS["top_p"],
209
- step=0.01,
210
- disabled=sampling_disabled,
211
- key=widget_key(context_key, "top_p"),
212
- )
213
- with sampling_col3:
214
- top_k = st.slider(
215
- "Top-k (0 = off)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  min_value=0,
217
- max_value=100,
218
- value=GENERATION_DEFAULTS["top_k"],
219
  step=1,
220
- disabled=sampling_disabled,
221
- key=widget_key(context_key, "top_k"),
222
  )
223
-
224
- seed_disabled = sampling_disabled or remote
225
- seed_enabled = st.checkbox(
226
- "Fix seed",
227
- value=False,
228
- disabled=seed_disabled,
229
- key=widget_key(context_key, "seed_enabled"),
230
  )
231
- seed = None
232
- if seed_enabled:
233
- seed = int(
234
- st.number_input(
235
- "Seed",
236
- min_value=0,
237
- max_value=2_147_483_647,
238
- value=0,
239
- step=1,
240
- disabled=seed_disabled,
241
- key=widget_key(context_key, "seed"),
242
- )
243
- )
244
 
245
- if remote:
246
- st.caption("Seed is local-only and disabled for remote runs.")
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  do_sample = bool(use_sampling)
249
- generation = GenerationConfig(
250
  max_new_tokens=int(max_new_tokens),
251
  do_sample=do_sample,
252
  temperature=float(temperature),
@@ -255,12 +309,6 @@ def render_advanced_settings(
255
  repetition_penalty=float(repetition_penalty),
256
  seed=seed if do_sample and seed is not None and not remote else None,
257
  )
258
- tools = ChatTools(
259
- probe_enabled=probe_enabled,
260
- compare_mode=compare_mode,
261
- token_contrast=token_contrast and compare_mode,
262
- )
263
- return generation, tools
264
 
265
 
266
  def render_chat_message(
 
21
  "repetition_penalty": 1.0,
22
  }
23
 
24
+ _LAST_GEN_PREFIX = "chat:last_gen:"
25
+
26
+
27
+ def _persisted_key(context_key: str, name: str, default) -> str:
28
+ """Per-context widget key, seeded from the last cross-context value."""
29
+ last_key = f"{_LAST_GEN_PREFIX}{name}"
30
+ key = widget_key(context_key, name)
31
+ if key not in st.session_state:
32
+ st.session_state[key] = st.session_state.get(last_key, default)
33
+ return key
34
+
35
+
36
+ def _remember(name: str, value) -> None:
37
+ st.session_state[f"{_LAST_GEN_PREFIX}{name}"] = value
38
+
39
 
40
  @dataclass(frozen=True)
41
  class GenerationConfig:
 
134
  remote: bool,
135
  *,
136
  last_compare_mode_key: str,
137
+ last_probe_enabled_key: str = "",
138
+ last_token_contrast_key: str = "",
139
  ) -> tuple[GenerationConfig, ChatTools]:
140
  """Render the Advanced expander: tool toggles + generation settings."""
141
  with st.expander("Advanced", expanded=False):
 
147
  last_compare_mode_key, False
148
  )
149
 
150
+ probe_key = widget_key(context_key, "probe_enabled")
151
+ if probe_key not in st.session_state:
152
+ st.session_state[probe_key] = st.session_state.get(
153
+ last_probe_enabled_key, False
154
+ )
155
+
156
+ token_contrast_key = widget_key(context_key, "token_contrast")
157
+ if token_contrast_key not in st.session_state:
158
+ st.session_state[token_contrast_key] = st.session_state.get(
159
+ last_token_contrast_key, False
160
+ )
161
+
162
  tools_col1, tools_col2, tools_col3 = st.columns(3)
163
  with tools_col1:
164
  probe_enabled = st.toggle(
165
  "Probe tools",
166
+ key=probe_key,
 
167
  help="Trace chat activations and run compatible `.pt` probes on tapped tokens.",
168
  )
169
  with tools_col2:
 
175
  with tools_col3:
176
  token_contrast = st.toggle(
177
  "Token contrast",
178
+ key=token_contrast_key,
 
179
  disabled=not compare_mode,
180
  help=(
181
  "Color each generated token by how characteristic it is of each persona. "
 
185
  ),
186
  )
187
  st.session_state[last_compare_mode_key] = compare_mode
188
+ if last_probe_enabled_key:
189
+ st.session_state[last_probe_enabled_key] = probe_enabled
190
+ if last_token_contrast_key:
191
+ st.session_state[last_token_contrast_key] = token_contrast
192
 
193
  st.divider()
194
  st.caption("Generation")
195
+ generation = _render_generation_fragment(context_key, remote)
196
 
197
+ tools = ChatTools(
198
+ probe_enabled=probe_enabled,
199
+ compare_mode=compare_mode,
200
+ token_contrast=token_contrast and compare_mode,
201
+ )
202
+ return generation, tools
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
+
205
+ @st.fragment
206
+ def _render_generation_fragment(context_key: str, remote: bool) -> GenerationConfig:
207
+ """Render generation sliders inside a fragment so tweaks don't full-rerun."""
208
+ config_col1, config_col2 = st.columns([2, 1])
209
+ with config_col1:
210
+ max_new_tokens = st.slider(
211
+ "Max new tokens",
212
+ min_value=16,
213
+ max_value=512,
214
+ step=16,
215
+ key=_persisted_key(
216
+ context_key, "max_new_tokens", GENERATION_DEFAULTS["max_new_tokens"]
217
+ ),
218
+ )
219
+ with config_col2:
220
+ repetition_penalty = st.slider(
221
+ "Repetition penalty",
222
+ min_value=0.5,
223
+ max_value=2.0,
224
+ step=0.05,
225
+ key=_persisted_key(
226
+ context_key,
227
+ "repetition_penalty",
228
+ GENERATION_DEFAULTS["repetition_penalty"],
229
+ ),
230
  )
231
 
232
+ use_sampling = st.checkbox(
233
+ "Random sampling",
234
+ key=_persisted_key(context_key, "use_sampling", False),
235
+ )
236
+
237
+ sampling_disabled = not use_sampling
238
+ sampling_col1, sampling_col2, sampling_col3 = st.columns(3)
239
+ with sampling_col1:
240
+ temperature = st.slider(
241
+ "Temperature",
242
+ min_value=0.01,
243
+ max_value=2.0,
244
+ step=0.01,
245
+ disabled=sampling_disabled,
246
+ key=_persisted_key(
247
+ context_key, "temperature", GENERATION_DEFAULTS["temperature"]
248
+ ),
249
+ )
250
+ with sampling_col2:
251
+ top_p = st.slider(
252
+ "Top-p",
253
+ min_value=0.01,
254
+ max_value=1.0,
255
+ step=0.01,
256
+ disabled=sampling_disabled,
257
+ key=_persisted_key(context_key, "top_p", GENERATION_DEFAULTS["top_p"]),
258
+ )
259
+ with sampling_col3:
260
+ top_k = st.slider(
261
+ "Top-k (0 = off)",
262
+ min_value=0,
263
+ max_value=100,
264
+ step=1,
265
+ disabled=sampling_disabled,
266
+ key=_persisted_key(context_key, "top_k", GENERATION_DEFAULTS["top_k"]),
267
+ )
268
+
269
+ seed_disabled = sampling_disabled or remote
270
+ seed_enabled = st.checkbox(
271
+ "Fix seed",
272
+ disabled=seed_disabled,
273
+ key=_persisted_key(context_key, "seed_enabled", False),
274
+ )
275
+ seed = None
276
+ if seed_enabled:
277
+ seed = int(
278
+ st.number_input(
279
+ "Seed",
280
  min_value=0,
281
+ max_value=2_147_483_647,
 
282
  step=1,
283
+ disabled=seed_disabled,
284
+ key=_persisted_key(context_key, "seed", 0),
285
  )
 
 
 
 
 
 
 
286
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
+ if remote:
289
+ st.caption("Seed is local-only and disabled for remote runs.")
290
+
291
+ for name, value in (
292
+ ("max_new_tokens", max_new_tokens),
293
+ ("repetition_penalty", repetition_penalty),
294
+ ("use_sampling", use_sampling),
295
+ ("temperature", temperature),
296
+ ("top_p", top_p),
297
+ ("top_k", top_k),
298
+ ("seed_enabled", seed_enabled),
299
+ ):
300
+ _remember(name, value)
301
 
302
  do_sample = bool(use_sampling)
303
+ return GenerationConfig(
304
  max_new_tokens=int(max_new_tokens),
305
  do_sample=do_sample,
306
  temperature=float(temperature),
 
309
  repetition_penalty=float(repetition_penalty),
310
  seed=seed if do_sample and seed is not None and not remote else None,
311
  )
 
 
 
 
 
 
312
 
313
 
314
  def render_chat_message(
tabs/compare.py CHANGED
@@ -14,7 +14,6 @@ from persona_vectors.plots import (
14
  build_pair_similarity_figure,
15
  plot_layer_similarity,
16
  save_plot_html,
17
- save_plot_png,
18
  )
19
 
20
  from utils.helpers import (
@@ -147,19 +146,13 @@ def _render_save_buttons(
147
  filenames: list[str],
148
  key_suffix: str,
149
  ) -> None:
150
- """Render Save HTML / Save PNG column buttons for one or more figures."""
151
- col1, col2 = st.columns(2)
152
- with col1:
153
- if st.button("Save HTML", key=widget_key("load", "save_html", key_suffix)):
154
  paths = [save_plot_html(fig, fn) for fig, fn in zip(figs, filenames)]
155
  st.success(f"Saved {len(paths)} HTML file(s) to `artifacts/plots`.")
156
- with col2:
157
- if st.button("Save PNG", key=widget_key("load", "save_png", key_suffix)):
158
- try:
159
- paths = [save_plot_png(fig, fn) for fig, fn in zip(figs, filenames)]
160
- st.success(f"Saved {len(paths)} PNG file(s) to `artifacts/plots`.")
161
- except Exception as exc:
162
- st.error(f"Could not save PNG: {exc}")
163
 
164
 
165
  def _render_mask_strategy_select(scope: str) -> MaskStrategy:
@@ -463,15 +456,7 @@ def _render_layered_figure_analysis(
463
  "persona_vector",
464
  persona_key,
465
  )
466
- filename = _filename(
467
- "compare",
468
- scope,
469
- store.model_name,
470
- mask_strategy.value,
471
- variant,
472
- "persona_vector",
473
- persona_key,
474
- )
475
 
476
  if st.button(button_label, type="primary"):
477
  try:
@@ -487,6 +472,8 @@ def _render_layered_figure_analysis(
487
  layers=selected_layers,
488
  title=title_fn(variant),
489
  )
 
 
490
  extra_fig = (
491
  build_pair_similarity_figure(
492
  samples,
 
14
  build_pair_similarity_figure,
15
  plot_layer_similarity,
16
  save_plot_html,
 
17
  )
18
 
19
  from utils.helpers import (
 
146
  filenames: list[str],
147
  key_suffix: str,
148
  ) -> None:
149
+ """Render the Save HTML button for one or more figures."""
150
+ if st.button("Save HTML", key=widget_key("load", "save_html", key_suffix)):
151
+ try:
 
152
  paths = [save_plot_html(fig, fn) for fig, fn in zip(figs, filenames)]
153
  st.success(f"Saved {len(paths)} HTML file(s) to `artifacts/plots`.")
154
+ except Exception as exc:
155
+ st.error(f"Could not save HTML: {exc}")
 
 
 
 
 
156
 
157
 
158
  def _render_mask_strategy_select(scope: str) -> MaskStrategy:
 
456
  "persona_vector",
457
  persona_key,
458
  )
459
+ filename = scope
 
 
 
 
 
 
 
 
460
 
461
  if st.button(button_label, type="primary"):
462
  try:
 
472
  layers=selected_layers,
473
  title=title_fn(variant),
474
  )
475
+ if figure_kind in {"umap", "pca"}:
476
+ main_fig.update_layout(height=700)
477
  extra_fig = (
478
  build_pair_similarity_figure(
479
  samples,
utils/datasets.py CHANGED
@@ -46,6 +46,28 @@ def _uploaded_file_to_temp_path(uploaded_file: Any, stem: str) -> Path:
46
  return temp_path
47
 
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  def load_dataset(
50
  dataset_source: str,
51
  personas_file: Any = None,
 
46
  return temp_path
47
 
48
 
49
+ def load_persona_list(
50
+ dataset_source: str,
51
+ personas_file: Any = None,
52
+ qa_file: Any = None,
53
+ ) -> tuple[list, str]:
54
+ """Like ``load_dataset`` but returns ``(personas_list, status)``.
55
+
56
+ The list is memoized on the cached dataset instance so repeated reruns
57
+ don't pay for re-iteration.
58
+ """
59
+
60
+ dataset, status = load_dataset(dataset_source, personas_file, qa_file)
61
+ cached = getattr(dataset, "_persona_list_cache", None)
62
+ if cached is None:
63
+ cached = list(dataset)
64
+ try:
65
+ dataset._persona_list_cache = cached
66
+ except (AttributeError, TypeError):
67
+ pass
68
+ return cached, status
69
+
70
+
71
  def load_dataset(
72
  dataset_source: str,
73
  personas_file: Any = None,